LikeShop 多商户标准版订单拆分与合并功能的二开改造方案

简介: 本文针对LikeShop多商户标准版订单体系痛点,提出轻量级二开方案:通过新增主子单架构(主单统一支付/展示,子单独立履约/分账),实现跨店拆单与同店合并,兼容原库存、支付、分账、售后全链路,零破坏、易升级。

多商户平台的订单体系,天然比单商户复杂一个维度。

LikeShop 多商户标准版原生订单以店铺为维度生成,单店铺下单流程是通顺的,但在两个场景下体验和效率不足:一是用户跨店铺选购商品时,必须分店铺分别提交、分别支付,操作链路长,转化损耗大;二是同一商家的多笔订单无法合并发货、合并结算,商家运营效率低,财务对账成本高。

这类需求属于典型的二开场景:不推翻原有订单体系,在原生结构基础上扩展主子单架构,实现跨店拆单与同店合并能力,同时兼容原有库存、支付、分账、售后全链路。

本文基于 LikeShop 多商户标准版(PHP + ThinkPHP 技术栈),给出一套完整的二开改造方案,涵盖架构设计、表结构改造、核心代码实现、关联模块联动与平滑升级方案。

一、改造背景与核心目标

1.1 原生订单的局限

LikeShop 多商户标准版原生订单模型为单级结构,ls_order 表一条记录对应一个店铺的一笔订单,merchant_id 区分所属商家。这套模型在单店铺下单场景下足够简洁,但存在两个明显短板:

  • 用户侧:购物车包含多家店铺商品时,必须分店铺分别提交订单、分别支付,操作步骤多,支付转化率低;
  • 商家侧:同一用户的多笔订单无法合并发货,同一结算周期内订单逐笔对账,运营与财务效率低。

1.2 改造核心目标

  1. 跨店拆单:支持购物车多店铺商品统一提交、一次支付,系统自动按商家拆分为多个子订单,用户侧只感知一个主订单;
  2. 同店合并:支持同一商家的多笔待发货订单合并发货、同一结算周期合并生成结算单;
  3. 向下兼容:不破坏原有订单、库存、支付、分账、售后链路,历史数据可平滑兼容;
  4. 最小侵入:尽量通过新增表与扩展字段实现,少改原生核心文件,便于后续同步官方版本。

二、核心设计思路

2.1 两级订单架构

采用「主订单 + 子订单」双层结构,这是多商户拆单的行业标准方案。

层级 面向对象 核心作用
主订单(父单) 用户 统一展示、统一支付、汇总金额,用户侧只看到一笔订单
子订单 平台/商家 按商家拆分,各自独立履约、分账、售后,商家端仅可见自身子单

核心原则:支付看主单,履约看子单。用户一次支付主订单,系统自动拆分到各子单;发货、退款、结算全部以子单为单位独立进行。

2.2 拆单规则

拆单维度可配置,默认按商家拆分,支持扩展更多维度:

  1. 默认维度:按商家拆分(必选):不同 merchant_id 的商品必然拆分到不同子订单,这是多商户分账与履约的基础;
  2. 扩展维度1:按仓库拆分:同一商家多仓库发货时,按仓库拆分子订单,分别出库、分别走物流;
  3. 扩展维度2:按品类拆分:特殊品类(如生鲜、易碎品)需要单独履约时,按品类拆单;
  4. 扩展维度3:按配送方式拆分:普通快递 + 同城配送混合时,按配送方式拆单。

2.3 合并规则

合并能力主要面向履约与结算场景,不改变订单本身的独立性:

  1. 合并支付:多店铺子订单通过主订单统一完成支付,用户只发起一次支付请求;
  2. 合并发货:同一商家、同一收货地址的多笔待发货子订单,可合并为一个发货单,共用一个物流单号;
  3. 合并结算:同一商家同一结算周期内的所有子订单,合并生成一张结算单,统一对账、统一打款。

三、数据库改造方案

遵循「新增为主、修改为辅」的原则,尽量不破坏原生表结构。

3.1 新增主订单表 ls_order_main

用户侧统一订单,承载总金额、支付信息、收货信息。

CREATE TABLE `ls_order_main` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `main_order_sn` varchar(32) NOT NULL COMMENT '主订单号',
  `user_id` bigint(20) NOT NULL COMMENT '下单用户ID',

  `total_amount` decimal(10,2) NOT NULL COMMENT '商品总金额',
  `total_freight` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '总运费',
  `total_discount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '总优惠金额',
  `pay_amount` decimal(10,2) NOT NULL COMMENT '实付总金额',

  `pay_status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '支付状态:0未付 1已付',
  `pay_time` datetime DEFAULT NULL COMMENT '支付时间',
  `pay_type` tinyint(1) DEFAULT NULL COMMENT '支付方式',

  `receiver_name` varchar(32) DEFAULT NULL,
  `receiver_mobile` varchar(16) DEFAULT NULL,
  `receiver_address` varchar(255) DEFAULT NULL,

  `sub_order_count` int(11) NOT NULL DEFAULT 0 COMMENT '子订单数量',
  `status` tinyint(1) NOT NULL DEFAULT 0 COMMENT '主单状态:0待付款 1待发货 2待收货 3已完成 4已取消',

  `create_time` datetime NOT NULL,
  `update_time` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_main_order_sn` (`main_order_sn`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='主订单表';

3.2 原订单表扩展字段

原生 ls_order 作为子订单表使用,新增关联字段,不影响原有字段语义。

ALTER TABLE ls_order 
ADD COLUMN `main_order_sn` varchar(32) DEFAULT '' COMMENT '主订单号,单店下单为空' AFTER `order_sn`,
ADD COLUMN `merge_ship_id` bigint(20) DEFAULT NULL COMMENT '合并发货ID' AFTER `express_no`,
ADD INDEX `idx_main_order_sn` (`main_order_sn`);

兼容说明:原生单店铺下单生成的订单,main_order_sn 为空,业务逻辑中判空即可走原有流程,不影响历史订单。

3.3 新增优惠分摊表 ls_order_discount_detail

跨店优惠(如平台满减、平台优惠券)需要按比例分摊到各子订单,退款时按分摊比例回退。

CREATE TABLE `ls_order_discount_detail` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `main_order_sn` varchar(32) NOT NULL,
  `order_id` bigint(20) NOT NULL COMMENT '子订单ID',

  `discount_type` varchar(32) NOT NULL COMMENT '优惠类型:platform_coupon/full_reduction/point',
  `discount_amount` decimal(10,2) NOT NULL COMMENT '该子单分摊优惠金额',
  `discount_rule` varchar(255) DEFAULT NULL COMMENT '优惠规则快照',

  `create_time` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_main_order_sn` (`main_order_sn`),
  KEY `idx_order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单优惠分摊明细表';

3.4 新增合并发货表 ls_order_merge_ship

记录多笔子订单合并发货的关联关系。

CREATE TABLE `ls_order_merge_ship` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `merchant_id` bigint(20) NOT NULL,
  `merge_sn` varchar(32) NOT NULL COMMENT '合并发货单号',

  `express_company` varchar(32) DEFAULT NULL,
  `express_no` varchar(32) DEFAULT NULL,
  `ship_time` datetime DEFAULT NULL,
  `operator_id` bigint(20) DEFAULT NULL,

  `create_time` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_merge_sn` (`merge_sn`),
  KEY `idx_merchant_id` (`merchant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单合并发货表';

四、核心业务流程与代码实现

4.1 跨店下单自动拆单

这是整个改造的核心流程:用户提交购物车多店铺商品 → 按商家分组 → 生成主订单 → 批量生成子订单 → 锁定库存。

<?php
// app/extra/service/OrderSplitService.php

namespace app\extra\service;

use app\common\model\Order;
use app\common\model\OrderGoods;
use app\extra\model\OrderMain;
use think\Db;
use think\Exception;

class OrderSplitService
{
   
    /**
     * 跨店下单:统一拆单
     * @param int $userId 用户ID
     * @param array $cartItems 购物车选中商品,含merchant_id
     * @param array $address 收货地址
     * @return array 主订单信息
     * @throws Exception
     */
    public function submitMultiShop(int $userId, array $cartItems, array $address): array
    {
   
        Db::startTrans();
        try {
   
            // 1. 按商家分组商品
            $grouped = [];
            foreach ($cartItems as $item) {
   
                $grouped[$item['merchant_id']][] = $item;
            }

            // 2. 生成主订单号
            $mainOrderSn = generate_order_sn('M');

            // 3. 计算主订单总金额
            $totalAmount = array_sum(array_column($cartItems, 'total_price'));
            $totalFreight = $this->calcTotalFreight($grouped);
            $totalDiscount = $this->calcTotalDiscount($cartItems);
            $payAmount = $totalAmount + $totalFreight - $totalDiscount;

            // 4. 创建主订单
            $mainOrder = OrderMain::create([
                'main_order_sn' => $mainOrderSn,
                'user_id' => $userId,
                'total_amount' => $totalAmount,
                'total_freight' => $totalFreight,
                'total_discount' => $totalDiscount,
                'pay_amount' => $payAmount,
                'receiver_name' => $address['name'],
                'receiver_mobile' => $address['mobile'],
                'receiver_address' => $address['detail'],
                'sub_order_count' => count($grouped),
                'status' => 0,
                'pay_status' => 0,
            ]);

            // 5. 逐商家生成子订单
            $subOrders = [];
            foreach ($grouped as $merchantId => $goods) {
   
                $subOrderSn = generate_order_sn();
                $subAmount = array_sum(array_column($goods, 'total_price'));
                $subFreight = $this->calcShopFreight($merchantId, $goods);
                $subDiscount = $this->allocateDiscount($subAmount, $totalAmount, $totalDiscount);

                $subPay = $subAmount + $subFreight - $subDiscount;

                $subOrder = Order::create([
                    'order_sn' => $subOrderSn,
                    'main_order_sn' => $mainOrderSn,
                    'user_id' => $userId,
                    'merchant_id' => $merchantId,
                    'total_amount' => $subAmount,
                    'freight_amount' => $subFreight,
                    'discount_amount' => $subDiscount,
                    'pay_amount' => $subPay,
                    'order_status' => 0, // 待付款
                ]);

                // 写入订单商品
                $this->createOrderGoods($subOrder['id'], $goods);

                // 锁定库存
                StockService::lockStock($subOrder['id'], $goods);

                // 记录优惠分摊
                $this->saveDiscountDetail($mainOrderSn, $subOrder['id'], $subDiscount);

                $subOrders[] = $subOrder;
            }

            Db::commit();
            return [
                'main_order_sn' => $mainOrderSn,
                'pay_amount' => $payAmount,
                'sub_count' => count($subOrders),
            ];

        } catch (Exception $e) {
   
            Db::rollback();
            throw $e;
        }
    }

    /**
     * 按金额比例分摊优惠
     */
    private function allocateDiscount($subAmount, $totalAmount, $totalDiscount)
    {
   
        if ($totalAmount <= 0) return 0;
        $ratio = $subAmount / $totalAmount;
        return round($totalDiscount * $ratio, 2);
    }
}

4.2 主订单统一支付与回调处理

用户只针对主订单发起支付,支付回调成功后,批量更新所有子订单的支付状态,并触发后续业务。

<?php
// 支付回调处理扩展
public function handleMainOrderNotify(string $mainOrderSn, string $payType)
{
   
    Db::startTrans();
    try {
   
        $mainOrder = OrderMain::where('main_order_sn', $mainOrderSn)->find();
        if (!$mainOrder || $mainOrder['pay_status'] == 1) {
   
            // 幂等:已支付直接返回成功
            return true;
        }

        // 1. 更新主订单支付状态
        $mainOrder->save([
            'pay_status' => 1,
            'pay_time' => time(),
            'pay_type' => $payType,
            'status' => 1, // 待发货
        ]);

        // 2. 批量更新子订单支付状态
        $subOrders = Order::where('main_order_sn', $mainOrderSn)->select();
        foreach ($subOrders as $sub) {
   
            if ($sub['order_status'] == 0) {
   
                $sub->save([
                    'order_status' => 1,
                    'pay_time' => time(),
                    'pay_type' => $payType,
                ]);

                // 触发原生订单支付后事件:扣库存、通知商家、生成分账快照
                event('OrderPaid', $sub);
            }
        }

        Db::commit();
        return true;
    } catch (Exception $e) {
   
        Db::rollback();
        throw $e;
    }
}

4.3 同店订单合并发货

同一商家、同一收货地址的多笔待发货订单,可合并为一个发货单,共用物流信息。

<?php
public function mergeShip(int $merchantId, array $orderIds, string $expressCompany, string $expressNo)
{
   
    Db::startTrans();
    try {
   
        // 1. 校验:都属于该商家、状态都是待发货、收货地址一致
        $orders = Order::whereIn('id', $orderIds)
            ->where('merchant_id', $merchantId)
            ->where('order_status', 1)
            ->select();

        if (count($orders) != count($orderIds)) {
   
            throw new Exception('存在不可合并发货的订单');
        }

        // 2. 创建合并发货记录
        $mergeSn = generate_order_sn('MS');
        $merge = OrderMergeShip::create([
            'merchant_id' => $merchantId,
            'merge_sn' => $mergeSn,
            'express_company' => $expressCompany,
            'express_no' => $expressNo,
            'ship_time' => time(),
        ]);

        // 3. 批量更新子订单发货状态
        Order::whereIn('id', $orderIds)->update([
            'order_status' => 2,
            'express_company' => $expressCompany,
            'express_no' => $expressNo,
            'merge_ship_id' => $merge['id'],
            'ship_time' => time(),
        ]);

        Db::commit();
        return $merge;
    } catch (Exception $e) {
   
        Db::rollback();
        throw $e;
    }
}

五、关联模块联动改造

拆单与合并不只是订单表的事,必须联动所有上下游模块,才能保证链路完整。

5.1 库存模块

  • 拆单时按子单维度分别锁定对应商家的 SKU 库存,不跨商家扣库存;
  • 订单取消、退款时,按子单维度回补库存;
  • 合并发货不影响库存,仅同步物流状态。

5.2 分账与结算模块

  • 分账以子订单为单位,每个子订单独立生成分账快照,对应各自商家;
  • 结算时支持合并模式:同一商家同一结算周期内的所有子订单,合并生成一张结算单,统一计算佣金、统一打款;
  • 退款按子单维度回退分账,不影响同主单下其他子订单。

5.3 售后模块

  • 售后申请以子订单为单位,用户针对具体店铺的具体商品申请售后;
  • 某一子单退款,不影响同主单下其他子订单的正常履约;
  • 主订单所有子订单全部退款完成后,主订单状态更新为「已退款」。

5.4 商家权限

  • 商家后台只能看到自身 merchant_id 对应的子订单,和原生数据隔离逻辑完全一致;
  • 商家端看不到主订单概念,也看不到其他商家的子订单,天然符合多商户数据隔离要求。

5.5 用户端展示

  • 用户订单列表默认展示主订单,点击展开可查看各子订单的物流、售后状态;
  • 单店铺下单时,也可选择直接展示子订单,保持原有体验不变。

六、平滑升级与历史数据兼容

6.1 升级步骤

  1. 执行新增表与字段的 SQL 脚本;
  2. 部署新增的二开代码,原有订单相关代码保留不动;
  3. 先开启「购物车跨店结算」开关做灰度,初期仅部分用户可见;
  4. 验证支付、发货、售后、分账全链路正常后全量放开。

6.2 历史数据兼容

  • 历史订单 main_order_sn 为空,业务逻辑中判空即走原生单订单流程,无需数据迁移;
  • 商家端、平台端原有订单查询逻辑,增加 main_order_sn 为空的兼容条件即可;
  • 统计报表默认同时统计主订单与独立子订单,保证数据口径一致。

七、常见坑与避坑建议

1. 拆单重复生成

用户重复点击提交,可能导致多次生成主订单和子订单。
避坑:提交接口加分布式锁,以用户ID + 购物车批次为Key;主订单号唯一索引兜底。

2. 优惠分摊精度问题

按比例分摊时,四舍五入会导致「各子单分摊之和 ≠ 总优惠」,出现几分钱误差。
避坑:最后一个子订单用「总优惠 - 其他子单分摊之和」倒算,把误差放在最后一子单。

3. 支付回调状态不一致

主单更新成功但部分子单更新失败,出现状态不一致。
避坑:整个回调操作放在同一数据库事务中,失败全部回滚;增加定时任务扫描状态不一致的订单,自动补偿。

4. 库存扣减原子性

拆单时多个子单分别扣库存,中间某一个失败,前面已扣的要回滚。
避坑:所有子单库存操作放在同一事务内;使用条件更新 SQL 扣库存,保证原子性。

5. 商家数据越权

合并发货时校验不严,可能导致商家操作其他商家的订单。
避坑:所有商家端操作都强制带上 merchant_id 条件,在框架层统一注入,不依赖前端传参。

结语

多商户订单拆合的改造,表面上是加一张主订单表,本质上是把「用户视角」和「商家视角」分层处理——用户要统一体验,商家要独立履约,平台要分账合规。

LikeShop 多商户标准版原生的订单、库存、分账体系已经很成熟,二开时不建议推翻重构,通过新增主子单架构、复用原有业务事件的方式,可以用最小的侵入成本实现拆单与合并能力,同时保留后续同步官方版本的空间。

对二开团队来说,核心原则只有一条:能扩展就不修改,能复用就不重写。架构上做好分层,后续再加拼团、满减、分销等营销玩法,也能平滑地嵌套进这套拆合体系里。

目录
相关文章
|
1天前
|
运维 网络协议 安全
泛域名 SSL 边界辨析:同证书下子域名覆盖的坑点
本文详解SSL泛域名证书(RFC6125)核心规则:`*.example.com`仅匹配单层子域(如www.example.com),不覆盖裸域(example.com)或多级子域(dev.api.example.com),且不支持双星号。附常见误区、安全风险与选型建议。(239字)
|
1天前
|
数据采集 人工智能 前端开发
GEO优化中官网到底重不重要?权重与落地做法
本文深度解析官网在GEO(生成式引擎优化)中的真实价值:非求曝光,而在“校准”——作为事实基准、校验依据与语义锚点。基于国内七大主流AI平台实测数据,拆解官网的准入、直接引用与校准三重权重,并提供可量化、可执行的六项改造与自查方法。
28 0
|
1天前
|
设计模式 人工智能 Devops
Qoder CN 大使计划正式开启|无限创造,等你一起
阿里Qoder CN大使计划启动!面向开发者、内容创作者及技术决策者,招募AI原生应用先锋。加入可获内测特权、算力补贴、官方认证徽章、流量扶持及技术导师辅导,共建AI编程生态。速报名抢占红利!
40 0
|
1天前
|
SQL 存储 Java
LikeShop 多商户高级版商城满减满赠活动的自定义规则二开实现
本文针对LikeShop多商户平台满减满赠二开需求,提出“独立活动体系+统一计算入口+事件联动分账”架构方案。涵盖双层级活动模型、多档阶梯规则、精准分摊与退款回退、审核流程及平滑升级,全程低侵入、高兼容,确保账务准确与运营灵活。
18 0
|
1天前
|
SQL Java 数据库
LikeShop 单商户 Java 版商城售后退换货流程的自定义二开落地
本文针对LikeShop单商户Java版售后模块的扩展需求,提出“独立售后单+状态机驱动+事件联动”的轻量二开方案。覆盖仅退款、退货退款、换货三类场景,支持部分退换、多级审核、物流跟踪与运费规则,全程不侵入原订单逻辑,保障资金、库存一致性,兼顾平滑升级与高可维护性。
19 0
|
1天前
|
SQL 安全 测试技术
LikeShop 多商户标准版商城自动化测试与上线前校验流程搭建
多商户系统测试复杂度远超单商户,涉及数据隔离、分账计算、状态流转、权限控制等多重风险。本文基于LikeShop实践,提出“四层测试金字塔+六大核心场景自动化+上线前标准化校验”落地方案,助力团队降低资金与合规风险,提升发版稳定性。
18 0
|
2天前
|
安全 数据库 索引
LikeShop 单商户标准版积分体系:签到、消费返积分与积分兑换逻辑
本文详解私域积分体系的高可用设计:摒弃简单“余额字段”思维,构建含账户、流水、规则、有效期的微型账务系统。LikeShop标准版以“流水驱动余额”为核心,覆盖签到、消费返、退款回退、积分抵现、兑换五大场景,确保可追溯、可对账、可回滚,规避超发、负扣、失效等典型风险。
30 0
|
2天前
|
SQL 安全 测试技术
LikeShop 单商户高级版二开项目的 Git 分支管理与版本发布规范
针对LikeShop单商户高级版二开场景,我们提出「官方基线隔离、二开分层迭代、发布可追溯、回滚有预案」的分支管理规范。通过新增`official/base`基线分支,严格分离官方代码与二开逻辑,配合6类职责明确的分支及四级版本号(如4.2.1.3),解决合入冲突、回滚失序、多人覆盖等高频痛点,显著降低长期维护成本。
24 0
|
2天前
|
SQL BI 数据库
LikeShop 多商户高级版营销中心:拼团、砍价、秒杀活动的底层实现
LikeShop多商户营销中心采用「基础活动抽象+子类型扩展+统一库存分账联动」架构,统一管控拼团、砍价、秒杀三大活动。通过独立库存池、原子扣减、状态机驱动、订单标记与分账联动等设计,解决超卖、分账错、状态不同步等高频痛点,显著降低维护成本。
21 0
|
2天前
|
监控 Java 关系型数据库
LikeShop 多商户高级版商城系统日志采集与监控告警体系搭建
本文针对多商户平台资金链路复杂、故障影响面广等特性,基于LikeShop高级版实践,从日志规范、采集方案、监控分层、告警体系四方面,提出轻量可落地的可观测性建设方案,覆盖入门到生产级演进路径。
32 0

热门文章

最新文章