2. 交易领域建模

交易系统的复杂度并不来自“下单”这个动作本身,而来自一次交易被拆成多个领域对象后,它们各自有独立生命周期、又必须保持一致性。领域建模要解决的核心问题是:哪些概念是独立聚合根,哪些只是聚合内的实体或值对象,以及每个聚合根的状态如何流转。如果这一步没做清楚,后续会出现“订单状态和支付状态互相覆盖”“退款单和订单状态不一致”“优惠券核销了但订单取消了”等典型问题。

一、核心领域对象与职责边界

在移动交易场景中,通常至少存在以下核心对象:

  • 订单(Order):交易的业务主单,记录用户买了什么、应付多少、收货信息、业务状态。它是用户视角的“这笔交易”。
  • 支付单(Payment):一次支付尝试的凭证,绑定支付渠道、渠道流水号、实付金额。一个订单可能对应多次支付尝试(失败重试、换渠道)。
  • 退款单(Refund):一次退款请求,绑定原支付单、退款金额、退款渠道流水。一个支付单可对应多笔部分退款。
  • 优惠券(Coupon):营销资产,有领取、锁定、核销、释放、过期等状态,与订单是“使用关系”而非从属关系。
  • 账户流水(AccountLedger):资金变动的账本记录,只追加不修改,是资金对账的最终依据。

关键判断:订单、支付单、退款单、优惠券、账户流水应各自作为独立聚合根。原因是它们生命周期不同、并发修改点不同、且需要独立查询与对账。若把支付单塞进订单聚合内部,订单聚合会因支付回调频繁加锁,且退款单无法独立演进。它们之间通过ID 引用关联,而不是对象嵌套。

二、聚合根划分原则

划分聚合根时遵循三条可操作原则:

  1. 一致性边界最小化:一个事务只修改一个聚合根。订单创建时锁定优惠券,应通过领域事件或应用服务协调,而不是在一个事务里同时改订单和优惠券。
  2. 引用用 ID,不用对象:订单里只存 paymentId、couponId,不持有支付单对象,避免聚合膨胀和并发冲突。
  3. 状态由本聚合维护:支付单状态只能由支付聚合修改,订单不能直接改支付单状态,只能根据支付结果事件推进自己的状态。
判断一个对象是否该独立成聚合根,可以问:它是否需要独立加锁?是否需要独立查询列表?是否生命周期长于宿主?三者有其一,通常就应独立。

三、状态机设计

状态机的价值在于把“合法流转”显式化,禁止非法跳转。下面给出各核心对象的状态定义与关键流转。

订单状态:CREATED(已创建待支付)→ PAID(已支付)→ FULFILLING(履约中)→ COMPLETED(已完成);CREATED 超时未支付 → CANCELLED(已取消);PAID 后发生退款 → REFUNDING → REFUNDED。注意:订单不因支付失败而改变状态,支付失败只影响支付单,订单仍停留在 CREATED 等待重试或超时。

支付单状态:INIT → PAYING(已发起渠道)→ SUCCESS / FAILED;SUCCESS 后可进入 PARTIAL_REFUNDED / REFUNDED。支付单状态必须由渠道回调驱动,且回调需幂等。

退款单状态:APPLIED → PROCESSING → SUCCESS / FAILED。退款成功后才允许写账户流水。

优惠券状态:AVAILABLE → LOCKED(下单锁定)→ USED(支付成功核销);LOCKED 在订单取消或超时后回到 AVAILABLE;超过有效期进入 EXPIRED。

状态流转必须满足两个工程约束:幂等(同一事件重复到达不产生二次流转)和可追溯(每次流转记录前置状态、事件、时间)。

四、对象关系与状态流转全景

下图把五个聚合根、它们之间的引用关系,以及订单与支付单的状态推进路径放在同一张图里,便于对照理解“谁驱动谁”。

交易核心聚合根关系与状态流转 订单 Order 聚合根 · 业务主单 支付单 Payment 聚合根 · 支付尝试 退款单 Refund 聚合根 · 退款请求 优惠券 Coupon 聚合根 · 营销资产 账户流水 Ledger 聚合根 · 只追加账本 orderId 引用 paymentId 引用 couponId 锁定/核销 支付成功写流水 退款成功写负向流水 订单状态机 CREATED PAID COMPLETED CANCELLED 超时 支付单状态机 INIT PAYING SUCCESS FAILED 退款单状态机 APPLIED PROCESSING SUCCESS

五、落地时的关键实现要点

把上面的模型落到代码时,有三个容易踩坑的点。

第一,状态流转要收敛到聚合根内部方法。不要让外部服务直接 order.setStatus(PAID),而应提供 order.markPaid(paymentId, paidAt),在方法内校验前置状态、记录流转日志、抛出非法流转异常。这样状态机才是“活的”。

第二,跨聚合一致性用领域事件 + 最终一致。支付回调到达后,支付聚合先落库 SUCCESS,再发布 PaymentSucceededEvent;订单聚合消费该事件推进到 PAID,优惠券聚合消费该事件把券从 LOCKED 改为 USED,账本聚合写入正向流水。每个消费者都要幂等,用事件 ID 去重。

第三,金额与状态要能对账。订单应付金额 = 商品金额 − 优惠金额;支付单实付金额应等于订单应付金额;退款单累计退款金额不得超过支付单实付金额。这些约束建议在聚合根方法内校验,而不是只靠数据库字段。

常见错误:把优惠券状态直接写在订单表里。优惠券有独立生命周期,订单取消后券要释放回 AVAILABLE,若状态寄生在订单上,释放逻辑会变得难以追踪。

六、建模成果检查清单

完成本节建模后,可用下表快速自检。表中“示例值”仅用于说明字段形态,不代表真实业务数据。

聚合根关键字段(示例)状态字段一致性约束
OrderorderId, userId, payAmount, couponIdstatuspayAmount ≥ 0;取消前未支付
PaymentpaymentId, orderId, channel, channelNostatus成功回调幂等;金额等于订单应付
RefundrefundId, paymentId, refundAmountstatus累计退款 ≤ 实付金额
CouponcouponId, userId, thresholdstatusLOCKED 只能被同一订单核销或释放
AccountLedgerledgerId, bizId, direction, amount—(只追加)同一 bizId 不重复入账

这张表的意义在于:每个聚合根的状态字段和约束一旦明确,后续的接口设计、数据库索引、对账任务和异常补偿都有据可依。领域建模不是画完图就结束,而是让每个状态流转都能被代码校验、被日志追溯、被对账验证。