从一张订单表打天下到 11 张分表协同作战,拆解电商多商户平台订单系统的核心架构决策,每张表为什么存在、每层数据为什么不放在一起。

引言

大多数开发者的第一个订单系统长这样:一张 order 表塞进所有字段,状态字段加几个,物流信息拼进去,退款金额也放进去。跑起来没问题,直到——来了多商户、跨店拆单、部分退款、大促秒杀。

订单系统的复杂度不是"存一条记录",而是在支付、履约、清算、售后四个维度的交叉点上,每张表只做一件事但协同工作。本文不罗列表字段,而是拆解每个设计决策背后的两难选择:为什么选 A 不选 B?

一、为什么拆成父子订单?

这是订单系统设计的第一问:一个订单号到底够不够?

方案 A:单层订单

一张 order 表,transaction_no 关联支付,status 从待付到完成一条线走完。看起来简洁。

问题出在多商户场景:用户在 A 店买手机、B 店买手机壳,一起付了款。如果用同一个订单号:

  • 哪个商家发货?两个商家看到同一个订单,无法各自履约。
  • 用户想退手机壳的款,整单退款还是部分?一条记录只能有一个退款金额。
  • 财务怎么和商家结算?一笔钱两个商家分,没法以订单为粒度清算。

方案 B:父子订单

trade_main (父交易总单)
  ├── 职责:支付锚点、合并支付的唯一凭证
  ├── 不参与履约、不参与结算
  │
  └── sub_order × N (子履约订单)
       ├── 职责:独立发货、独立售后、独立财务清算
       └── order_item × N (单品明细)

选择 B 的核心原因只有三个:

  1. 支付原子性:一次支付回调级联所有子单,合单支付对用户完全透明。
  2. 履约独立性:每家店各自发货、各自处理售后,互不干扰。
  3. 对账锚点:退款上限 ≤ 父订单总额,防止超退。

这不是"父子关系看起来更结构化"的设计美学问题,而是多商户平台的资金安全底线。没有父订单做总控,部分退款+多商家结算的组合可以轻易出现对不上账的情况。

自营单店的简化

如果你不做多商户平台,只是自营电商,直接砍掉 trade_mainsub_order,用一张 order_main 就够了。架构要为业务复杂度买单,不为过度设计买单。

二、11 张订单表,每张为什么存在?

父子订单解决了"谁负责什么"的问题,接下来要解决"数据放哪里"的问题。11 张表不是设计癖好,是按职责域自然生长出来的。

顶层交易拆分层

trade_main + sub_order 上面已经讲了。

交易明细层:order_item

最小 SKU 交易粒度。一个 sub_order 里可以有多个 order_item(买了同商家下的不同 SKU),但同一个 SKU 买 3 件只有 1 条 order_item,通过 buy_num 记录数量。

为什么不拆?拆分到单件粒度会导致:

  • 部分退款时算优惠分摊变成噩梦
  • 表行数爆炸(每件一行 vs 每种一行)

order_item 存什么?单品实付、分摊优惠、退款金额、售后状态、佣金分摊。这些是动态业务数据,每天在变。

快照冷热层:snapshot_main vs snapshot_detail

热表:order_snapshot_main (标题、主图、规格摘要)
  └── 扛 90% 流量:订单列表、分页、筛选、商家后台批量查询

冷表:order_snapshot_detail (完整商品 JSON、富文本详情)
  └── 扛 10% 流量:订单详情页、售后仲裁取证

核心规则:订单列表全程不 JOIN snapshot_detail

原因直白:snapshot_detail 存的是完整商品 JSON 和富文本,一条记录可能几十 KB。如果订单列表页每次分页都加载这些大字段,索引再好也救不回来。

这也是为什么不能直接用快照替代 order_item ——快照是只读归档,下单瞬间固化,不存储退款、售后、佣金这些一直变的东西。你不可能在"筛选已退款订单"时去翻快照 JSON 找退款状态。

履约层:order_logistics

物流包裹表。一个 sub_order 可以拆成多个包裹发(超重、分仓),但包裹只是物流载体,不参与资金清算

资金售后层:payment_record + refund + refund_item

支付退款的完整流水。为什么独立?因为对账需要追溯——第三方支付单号、回调报文、每笔退款的来源去向,必须有一条不可篡改的时间线。

三、sub_order 什么时候拆,order_item 什么时候不拆?

拆单的核心标准:履约能力是否相同

满足以下任意一条,必须生成多条 sub_order

  1. 跨不同店铺
  2. 同商家但多仓库分仓
  3. 履约模式不同:现货、预售、冷链、大件、同城配送
  4. 包裹超重需要分多包裹
  5. 跨境、危险品、奢侈品特殊品类

order_item 的拆分标准则简单得多:不同 SKU 才拆,同 SKU 多件不拆。

还有一道选择题:sub_order 是下单时生成还是支付后生成?

策略 做法 优点 缺点
下单实时生成 提交订单事务内创建全部 待支付阶段商家即可见,金额实时分摊 大促下单接口 DB 压力高
支付后 MQ 异步生成 下单只建 trade_main 下单轻量吞吐高,可匹配就近仓库 未支付时商家看不到,需要定时补偿

大厂高并发场景基本选后者。前者适合中小平台,运维简单。

四、状态机 + 延时消息:谁来驱动订单流转?

订单不是只有"创建"和"完成"两个状态。完整的状态机必须覆盖支付异常、超时关单、部分退款、售后流转。

UNPAID
  ├── 支付成功 → UNSHIPPED → (发货) → WAIT_RECV → (确认) → COMPLETED
  ├── 15分钟超时 → CLOSED
  └── 支付异常 → PAY_ERROR → 人工介入

状态流转的引擎是什么?定时任务扫表?

方案是 RocketMQ 延时消息

延迟 动作
15 分钟 超时自动关单
10 分钟 推送支付提醒
7 天 自动确认收货
24 分钟 商家超时未发货撤销接单

对比定时任务扫表:定时任务需要维护游标、处理分页、扫描大量已处理数据,延时消息是精确的"到点触发 ",不浪费计算资源。但延时消息并非万能——超长延迟(如 30 天)受限于 MQ 存储,仍需兜底的定时任务。

五、下单一致性:RocketMQ 事务消息 vs Seata

下单涉及 6 步校验器链:活动校验 → SKU/库存 → 地址 → 优惠券计算 → 金额计算 → 钱包校验。每一步都可能失败,但前几步可能已经扣了库存、冻了钱包。

这个场景下,Seata 的三个问题:

  • AT 模式:每个参与者要额外写一条 undo log,核心链路上多一次 DB 写。下单对延迟敏感,不值得。
  • TCC 模式:6 个服务全部改造 Try/Confirm/Cancel 接口,工程量大,且 Confirm/Cancel 必须幂等——幂等本身就是一个分布式难题。
  • 一个隐性问题:钱包服务可能已经在扣款链路中,此时 Seata 的参与者并非全部可控。

选择 RocketMQ 事务消息不是因为它"更好",而是项目已经用它做消息驱动,复用现有基础设施成本最低

流程:

发送 half 消息 → 执行本地事务(建父子单+扣库存+冻钱包)
  ├── 成功 → commit MQ → 发延时关单消息
  └── 失败 → rollback → 释放库存+解冻钱包

加上定时补偿任务做兜底,保证了最终一致性。

六、履约、包裹、清算的三层隔离

最容易混淆的三个概念,面试高频踩坑点:

层次 最小单元 驱动
履约 sub_order 待履约 → 已发货 → 运输中 → 签收 → 售后 → 关闭
包裹 order_logistics 物流载体,仅存运单轨迹,不控制履约状态
清算结算 sub_order → 汇总至商家 佣金、退款抵扣、财务对账,多条子单汇总结算单

一条子单可以对应多个包裹(分批发货),但包裹数量不影响清算金额。财务结算是按商家维度汇总所有 sub_order,不是按包裹。这三层如果不隔离,就会出现"物流多发了几个包裹,财务以为是多笔订单"的乌龙。

总结

回顾订单系统的核心设计决策:

  1. 父子订单是业务必需的复杂度,不是过度设计。 多商户平台的资金安全需要父单做总控,履约独立性需要子单做单元。
  2. 分表不是为了分而分,是按职责域自然拆解。 快照只读 / 资金流水 / 售后工单 / 物流包裹——每张表有自己独立的查询场景和更新频率,塞进一张表只会互相拖慢。
  3. 冷热分离是用空间换查询体验。 列表页 90% 流量不该碰大 JSON。snapshot_mainsnapshot_detail 的分层,是用户在订单列表上下滑动的体感来源。
  4. 延时消息 + 状态机替代定时任务扫表。 精确触发 + 零扫描开销,但长延迟仍需兜底。
  5. 一致性方案选 RocketMQ 事务消息而非 Seata,是"我有基础设施所以用这个"而不是"这个更好"。 架构决策永远是场景适配题,不是技术优劣题。

订单系统设计没有银弹。每张表的存在、每层数据的隔离,背后都是一个如果不做就会出的生产事故。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。