1.1 需求拆解:下单、支付、回滚、对账

订单中心的边界,是靠把四件事的输入输出说清楚才划定的。我们团队第一周没写一行代码,全在吵架:商城说库存不该订单中心管,仓库说库存必须订单中心管,财务说对账不归他管。最后是产品经理拉了一张四列大表,谁负责什么一目了然。这张表就是后来的需求基线。

场景:四件事各自长什么样

下单是起点:用户在商城点「提交订单」,订单中心要创单、冻结库存、返回支付参数。支付是变化点:支付平台异步回调,订单中心要更新订单状态并真正扣减库存。回滚是兜底点:超时未支付或支付失败,冻结的库存要原样还回去。对账是闭合点:每天定时把订单流水、库存变动、支付渠道流水三方对齐,差一分钱都要报警。

四件事里最容易低估的是对账。上线第一个月我们没做对账,月底财务发现 17 单支付成功但库存没扣,原因是回调重试时数据库主从切换把一次更新吞掉了。从那以后对账被提到和下单同等的优先级。

方案:四件事的接口边界

我们把四件事抽象成四个领域动作,分别约定输入、输出和异常语义:

动作 输入 输出 异常语义
下单 用户、商品、数量、地址 订单号、冻结结果、支付参数 重复下单返回原单
支付回调 渠道流水、签名、订单号 状态变更、扣减结果 重复回调直接幂等返回
回滚 订单号、触发原因 解冻结果 已回滚单再回滚无副作用
对账 日期、渠道 差异单清单 差异单阻塞结算

这个表的关键设计是「异常语义」一列:每个动作都必须回答「重复调用会怎样」。下单返回原单而不是报错,回调幂等而不是拒绝,回滚幂等而不是失败。订单系统一半以上的线上故障,都是重复调用没幂等导致的。

配置:领域动作的代码骨架

四个动作落到代码里,就是一个 OrderService 接口加上四个入口方法。这里给可读骨架,完整实现后面章节展开:

public interface OrderService {
    // 下单:创单 + 冻结库存 + 返回支付参数
    CreateOrderResult createOrder(CreateOrderRequest request);
    // 支付回调:验签后更新状态并扣减库存
    void handlePayCallback(PayCallback callback);
    // 回滚:解冻库存并关闭订单
    void rollback(String orderNo, RollbackReason reason);
}

接口刻意只有三个方法,对账是定时任务单独走批处理模块,不混进实时链路。这是踩了坑之后的妥协:早期把对账查询接口和服务方法放一起,一次慢查询把整个下单线程池拖垮,超时告警响了半小时。

踩坑:边界不清的三次代价

第一次是库存归属权之争。下单时冻结库存写在订单中心,但仓库系统也能直接改库存,两个系统同时写一张表,超卖就这么来的。后来规定:库存的可售数只能订单中心改,仓库只能改在途数。这个权责划分跟 MySQL 分库分表实战 里讲的数据归属原则是一回事,谁的数据谁负责写。

第二次是回调语义不统一。支付平台在订单成功后会回调三次(支付成功、通知到达、结算完成),我们一开始把三次回调当三件事处理,状态被改乱。后来约定只认支付成功这一次,其余两次只记日志不改状态。

第三次是对账粒度。最初按天对账,差异单积压一天才被发现。改成小时级对账后,差异单从平均 40 单降到 3 单以内,因为渠道流水还没被下游系统二次加工,对齐起来快。

进阶实战:接口契约评审

四件事的边界落到工程上,就是四份接口契约。我们强制每个接口契约在评审时回答五个问题,答不上来不许开发。这套规矩定下来之后,后期返工率明显下降,值得拿出来单说。

契约问题 必须明确的答案
谁是唯一写入口 库存的可售数只能订单中心写
重试语义是什么 重复调用返回第一次的结果
部分失败怎么办 明确补偿动作或人工处理流程
回调日期口径 以渠道记账时间为准
异常如何外显 错误码与用户可见文案的映射

最早下单接口没有定义重试语义,接入方上半年临时决定超时重试,重复单从偶发变成日均 50 单,我们才补上幂等键。这个跟头摔得早,后面所有接口第一版就把重试语义写进文档。契约文档不是形式主义,是给三个月后的自己留的话。

另一个值得记的细节是接口的版本策略。订单中心对外提供 v1 接口,字段只增不改,废弃字段保留半年。有次我们想缩减一个返回字段的位置,接入方的前端直接拿到 undefined。从此约定:任何破坏性变更走新版本号,老版本进入观察期,观察期内监控调用量,归零之后才下线。

速查清单

需求拆解阶段的四条硬指标:

检查项 达标标准
每个动作有幂等语义 文档写明重复调用结果
共享数据单一写入口 库存可售数只订单中心改
对账频率小时级 差异单 24 小时内发现
契约五问答齐 评审表逐条签字

小结

四件事拆开看都不复杂,合在一起难在边界。记住三条:每个动作都要定义重复调用的语义;库存这类共享数据只能有一个写入口;对账频率越高,差异越小越好查。下一篇我们算选型的账,看单体起步到底省在哪里。

常见误解:需求拆解的三个陷阱

第一个陷阱是「把所有需求都做进订单中心」。有段时间产品提的每个和订单沾边的需求都想塞进来:发票管理、物流追踪、售后工单。边界一虚,系统就臃肿。我们的对策是问一句「这个能力挂了会不会影响下单支付主链路」,答不会就坚决拆出去单独建系统。发票最后归到财务系统,物流归履约系统,订单中心只存它们的通知回执。这个标准执行下来,订单中心的接口数量一直稳定在二十多个,没有膨胀。

第二个陷阱是「忽视非功能性需求」。需求文档里全是功能,可用性、一致性、延迟这类指标没人写。我们的做法是对每个核心功能补一张非功能需求表:可用性目标几个九、数据一致性口径以谁为准、接口延迟 P99 目标多少、峰值流量预估多少。这些指标不写清楚,验收时就是一笔糊涂账。把指标写进需求并不难,难的是养成习惯,我们把它做成了需求评审的固定环节,缺表的 PRD 直接退回。

第三个陷阱是「对账需求最后才想」。财务对账的需求往往在系统上线后才被提出,那时候数据口径早散落在各处。经验是在设计阶段就把财务拉进评审,问他们「月底要什么报表、按什么口径对账、差异怎么定义」。订单中心如果第一天就按对账友好的口径记账,后面的日子会好过得多。这个经验在我们第二次做支付链路时直接落到了设计文档里,节省了一个月的返工。

团队协作:需求到代码的传导

需求拆解的结论怎么传导到代码?我们靠两份文档加一个仪式。两份文档是「接口契约」和「非功能需求表」,仪式是上线前的边界走查。边界走查很简单:参与者包括开发、测试与产品,把实际数据库表、状态定义、错误码表打开,对照需求逐条确认。这个半小时的走查抓出过好几次「需求理解偏差」,上线后再发现都是大事故。

最有意思的是错误码表带来的连锁效应。最初错误码由开发随手给,同一个「库存不足」有三个码,前端展示三种文案。统一错误码后,客服系统可以直接按码检索同类工单,运营报表也能统计「库存不足」的频次指导采购。一个看似很小的规范化动作,让下游多个系统的数据突然可用了。

需求拆解这关把住之后,技术设计就有了一个稳固的靶子。我们的体会是:前一周的吵架,省下来的是后三个月的返工。

顺带一个实践:把四件事的边界画成一张图贴在团队墙上,新人入职第一天看的不是代码而是这张图。边界图让每个人在写代码前就知道这件事归不归自己。有次新同事改库存扣减代码前先跑去问负责人这归不归订单中心,就是这张图在起作用。需求拆解的功夫做到位,后面写代码的每一步都顺:每个人都知道自己该改哪里、改完谁来验。再补充一个细节:四件事的负责人有明确的人才画像——下单偏交易系统设计、回滚偏一致性工程、对账偏数据处理,团队招聘与分配都参考这份画像。把对的人放到对的事上,是需求拆解之外容易被忽略的一层。需求拆解做到最后,其实是把「事」与「人」同时摆清楚,缺了哪一半,方案都落不了地。需求拆解篇就写到这里,带着这份边界感进入下一章的选型讨论,选型的每一笔账都会好算得多。

延伸阅读