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

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

目录
相关文章
|
2天前
|
运维 网络协议 安全
泛域名 SSL 边界辨析:同证书下子域名覆盖的坑点
本文详解SSL泛域名证书(RFC6125)核心规则:`*.example.com`仅匹配单层子域(如www.example.com),不覆盖裸域(example.com)或多级子域(dev.api.example.com),且不支持双星号。附常见误区、安全风险与选型建议。(239字)
|
2天前
|
设计模式 人工智能 Devops
Qoder CN 大使计划正式开启|无限创造,等你一起
阿里Qoder CN大使计划启动!面向开发者、内容创作者及技术决策者,招募AI原生应用先锋。加入可获内测特权、算力补贴、官方认证徽章、流量扶持及技术导师辅导,共建AI编程生态。速报名抢占红利!
49 0
|
2天前
|
数据采集 人工智能 前端开发
GEO优化中官网到底重不重要?权重与落地做法
本文深度解析官网在GEO(生成式引擎优化)中的真实价值:非求曝光,而在“校准”——作为事实基准、校验依据与语义锚点。基于国内七大主流AI平台实测数据,拆解官网的准入、直接引用与校准三重权重,并提供可量化、可执行的六项改造与自查方法。
36 0
|
3天前
|
安全 数据库 索引
LikeShop 单商户标准版积分体系:签到、消费返积分与积分兑换逻辑
本文详解私域积分体系的高可用设计:摒弃简单“余额字段”思维,构建含账户、流水、规则、有效期的微型账务系统。LikeShop标准版以“流水驱动余额”为核心,覆盖签到、消费返、退款回退、积分抵现、兑换五大场景,确保可追溯、可对账、可回滚,规避超发、负扣、失效等典型风险。
40 0
|
6天前
|
SQL 前端开发 Java
LikeShop 二开代码规范:目录结构、钩子使用与版本升级兼容方案
本文为电商系统二开团队提供可落地的代码规范指南,聚焦目录隔离、钩子扩展与升级兼容三大维度。强调“最小侵入、物理隔离、扩展优先、兼容升级”原则,覆盖PHP(ThinkPHP)与Java(SpringBoot)双技术栈,助团队告别升级冲突,实现平滑迭代。
50 0
|
7天前
|
JSON 安全 定位技术
LikeShop 连锁点餐系统二开:门店自提与第三方配送对接
本文详解LikeShop连锁点餐系统两大高频二开需求:门店自提(LBS就近推荐+POS核销同步+预计取餐时间)与第三方配送对接(达达/美团SDK集成、状态回调、幂等处理),强调扩展不侵入核心、配置按门店隔离、状态迁移走统一状态机,助开发者安全高效定制履约能力。
57 0
|
7天前
|
存储 定位技术 数据安全/隐私保护
LikeShop 社区团购系统自提点管理:库存核销、团长佣金与提现对账
本文详解社区团购中自提点的三大核心管理模块:库存核销(四状态流转+幂等设计)、团长佣金(分层配置+下单快照)、提现对账(周期结算+明细留痕),结合LikeShop实战代码与六大避坑指南,助力系统稳健落地。
70 0
|
7天前
|
移动开发 小程序 前端开发
LikeShop 单商户标准版 UniApp 端二开:页面组件复用与自定义主题适配
本文详解LikeShop单商户版UniApp二开的正确姿势:避免直接改页面,善用已有的三层组件化体系(基础/业务/页面区块)与SCSS主题变量机制。从目录结构梳理、组件复用实践到主题适配技巧,附核心代码示例,助团队降本增效、提升可维护性。
49 0
|
8天前
|
负载均衡 Java 调度
LikeShop 上门家政 Java 版师傅派单:区域匹配、订单推送与改派逻辑
LikeShop家政系统Java版聚焦派单这一核心环节,通过区域分层匹配、多通道推送、超时自动改派及全程状态留痕,解决跨区派单、接单率低、改派混乱等痛点。架构清晰、逻辑严谨、可配置可审计,助力商家提升运营效率与用户体验
58 0
|
8天前
|
SQL JSON 前端开发
LikeShop 单商户标准版商品管理二开:分类、SKU 规格与库存联动实现
LikeShop单商户商品模块是系统地基,涵盖分类、SPU/SKU、库存等核心。本文详解二开关键:多级分类树实现、JSON灵活存SKU规格、行锁+条件更新防超卖、多渠道库存联动及批次/盘点扩展,强调库存安全三原则——精准扣减、及时回补、全程留痕。
58 0

热门文章

最新文章