Skip to content

订单退款领域建模与 Saga ​

2026-06-02 → 06-04 · v0.6.1
主仓里程碑日记改写。日报、文件清单、自测 SQL 已去掉,只留对外还值得看的决策。

占位接口变真之后,还剩一类问题:订单列表看得见点不动,退款是五个 TODO 拼出来的,申请用 Map<String, Object> 表示。这一版用真退款练 DDD——简单的一次做完,复杂的沉到以后,不装「一次做完所有金融闭环」。

一句话 ​

订单聚合故意做薄,退款申请做成厚聚合;部分退款 Saga 拆成「先落库等审批 → 通过后再退」。

为什么订单薄、退款厚 ​

OrderAggregateRefundApplicationAggregate
当下行为建单 / 标已支付 / 标退款 / 关单6 个状态、4 类合法跃迁、金额与审批人校验
规则复杂度低,硬塞充血方法是空壳高,必须聚在聚合根里才能演进
选型薄:SQL 级状态预检 + UPDATE厚:状态机 + LEGAL_TRANSITIONS

「严格 DDD 就要所有聚合都充血」是执念。依据应该是 业务规则复杂度 + 重复出现的频度。订单以后如果出现 3 处以上复制粘贴的退款判断,再升级为厚聚合(ADR-006 写了这条触发线)。

Saga 为什么要两段 ​

第一版 PartialRefundSaga 是一把 execute 做完。部分退款必须等人审,链路必然中断。

execute
  → 落库 PENDING_REVIEW
  → 标记 WAITING_FOR_APPROVAL
        ↓
管理员 approve
  → 聚合 APPROVED
        ↓
executeAfterApproval
  → startRefunding → 调支付退款 → REFUNDED / REFUND_FAILED

WAITING_FOR_APPROVAL 和「等支付回调」是同一种中断:Saga 停在人为输入上,而不是假装一次远程调用就能结束。

v0.6.1 选择 审批请求里同步触发后半段,管理员刷新就能看到结果。代价是微信退款可能拖几秒。兜底已经有:先把聚合落成 APPROVED,Saga 失败则 REFUND_FAILED,定时任务接管重试。支付抖得厉害再改成事件异步,不必这版推翻。

状态机 ​

PENDING_REVIEW ──→ APPROVED ──→ REFUNDING ──→ REFUNDED
     │                              ↓
     │                        REFUND_FAILED ──→ 可再进 REFUNDING
     └────────→ REJECTED

合法跃迁集中在聚合根的静态表里,canTransitionTo() 强制校验。订单号也不再满天飞 UUID,统一成 ACT_yyyyMMdd_xxxxxx / REF_yyyyMMdd_xxxxxx。

几个当时没做、并且做对了的选择 ​

  1. 复杂业务必须有沉淀处。 通知、按进度算金额、大额分流、对账,都标了 v0.6.2,并且同时写进待办文档。只埋代码注释,三个月后一定丢。
  2. 单例 Saga 的 setState 不并发安全。 后台同时点同一笔退款的概率接近 0,这版只加警告,不搞过早重构。
  3. 审批入口不放 Controller。 RefundApprovalApplicationService 统一恢复 Saga 上下文。以后改异步,只改这一处。

和本栏目其它文的关系 ​

Powered by VitePress