这一篇是全系列的高潮。好消息是:不需要假设,我们用一套完整落地的 DDD 订单域来举例,我们直接看真代码。
你平时排查支付渠道问题,那就选你最有感觉的链路:支付回调 ,外加它牵出的订单状态流转(支付、超时、取消、退款)。
两个主角:
-
三层版 :patient 模块,
OrderController → OrderServiceImpl → OrderMapper; -
DDD 版 :mpc 中台 obs 模块,
OrderBaseSalesRuntimeContext → OrderBasedSalesEntity + 状态机 + Handler。
第一站:三层版现状
OrderServiceImpl.createOrder 的"五合一"上一篇已经解剖过了(业务校验、远程调用、业务分支、Redisson 锁、重试策略)。这里只补一刀:这套写法下,"订单状态"是一个谁都能改的字段 。支付回调改它、定时任务改它、客服接口也改它,每一处都是一句 order.setStatus(...)。想问"订单到底有哪些状态、什么时候能变",答案散在十几个文件里。
第二站:DDD 版------同样的业务拆成四个角色
角色一:状态机描述符------先立"交通规则"
OrderDescriptorLoader.java 用纯声明的方式写清了订单领域的全部规则:8 个状态、10 个事件、每条"状态+事件→新状态"的转移:
java
domainDescriptor.description("购买快速问诊订单领域")
.type(Constants.ORDER_DOMAIN_DESCRIPTOR)
.label("purchase_quickmed_service")
.version("v1");
// 注册状态机:支付中 + 支付成功事件 → 服务中
domainDescriptor.registerStateMachine(paying, paymentSuccess, running);
// 支付失败 / 支付超时,回到待支付
domainDescriptor.registerStateMachine(paying, paymentFail, nothing);
domainDescriptor.registerStateMachine(paying, payingTimeout, nothing);
// 每条转移绑定干活的 Handler
domainDescriptor.registerEventTrigger(paying, paymentSuccess, "salesOrderPaymentSuccHandler");
整个订单生命周期长这样(真实状态名就用的 nothing/paying/running/...):
主流程是支付链路,支线是退款链路;注意退款失败、部分退款成功都会退回"服务中",而不是停在退款中。

这份描述符就是订单领域的"宪法":所有合法的状态流转全在这里,别处不许私改状态。
角色二:领域实体------规则长在订单身上
OrderBasedSalesEntity.java 继承框架的 GenericDomain<SalesOrder>,自带业务判断:
java
// 订单自己知道"我能不能被支付"
public Result<String> isOrderPayable() {
if (UNPAID != getValueObject().getPaymentStatus()) {
return Result.error("订单状态不是未支付,不可重复支付。状态:" + ...);
}
return Result.success();
}
// 订单自己会算"我已经退了多少钱"
public BigDecimal totalRefundAmount() { ... }
// 订单自己知道"我的分布式锁 key 长什么样"
public String getOrderLockKey() { ... }
角色三:应用层编排------领班只安排,不做饭
支付回调进来,OrderBaseSalesRuntimeContext.paymentNotify 的骨架:
java
@Transactional(rollbackFor = Exception.class)
public Result<PaymentNotifyDto> paymentNotify(PaymentResultEventValueObject param) {
OrderPayment payment = orderPaymentRepo.getById(param.getPaymentId());
// 幂等判断:已成功的回调直接返回(幂等管的是回调本身,不是订单状态)
if (PaymentSucceedEnum.YES == payment.getSucceed()) {
return Result.success();
}
OrderBasedSalesEntity orderEntity = orderEntityRepo.findOrder(orderId, ...).get();
// 锁交给适配器
return iDomainLockAdapter.logicWithLock(orderEntity.getOrderLockKey(), () -> {
// 按渠道回调结果选事件------注意:选事件,不是改状态
EventModel event = param.getPaymentResult()
? EventModelFactory.INSTANCE.getEventModel(SalesOrderEventConstants.payment_success)
: EventModelFactory.INSTANCE.getEventModel(SalesOrderEventConstants.payment_fail);
event.setValueObject(JSONUtil.parseObj(param));
orderEntity.onEvent(event); // ← 整个业务就这一行
...
});
}
应用层干的事:查单、幂等判断、加锁、选事件、触发。它不写任何"状态从 X 变 Y"的规则。
角色四:基础设施适配器------技术细节隔离在墙外
分布式锁不在业务代码里手写 Redisson,而是走 IDomainLockAdapter 接口,实现里统一封装"重试 6 次抢锁、持有 5 秒、释放前校验 isHeldByCurrentThread"。你排查过锁超时问题,应该一眼看出这个封装的价值:所有加锁业务共享同一套经过验证的锁策略,不用每个 Service 各写各的 tryLock,也不会再出现"忘了校验持锁线程就 unlock 抛 IllegalMonitorStateException"的老坑。
串起来:onEvent 的六步流水线
orderEntity.onEvent(event) 这一行背后,框架(GenericDomain.java)替你跑了一条标准流水线。六步里最值钱的是第 ① 步------非法的状态+事件组合,根本走不到业务代码:

第三站:直观对比------加一个新事件要动几处?
场景:产品说"医生可以驳回订单,驳回后进'已驳回'状态"。两边的动作清单差别就在这:
三层版靠"程序员的记性"保证一致性,DDD 版靠"结构"保证一致性------非法状态下的驳回根本进不来,verify 这关就过不去,测试只需要覆盖合法路径。

DDD 版加"驳回"的实际动作就两步:
java
domainDescriptor.registerStateMachine(running, doctorReject, rejected);
domainDescriptor.registerEventTrigger(running, doctorReject, "salesOrderDoctorRejectHandler");
再写一个 salesOrderDoctorRejectHandler,只写"驳回后做什么"。完事。
迁移策略:千万别推倒重来
mpc 的做法值得抄作业:
-
不动老代码。patient 的三层服务照常跑,没有为了 DDD 而重写;
-
新建领域模块 。领域骨架放在独立子模块(
chinaunicom-medical-ihm-domain),和业务模块物理隔离; -
从最痛的业务开始。挑状态流转最复杂、规则最常变的(订单/支付)先行试点,验证收益后再推广;
-
新旧共存。两套架构在同一工程里各司其职,按业务复杂度选型,不搞一刀切。
这种"在新地盘上建新房子,老房子住到自然退役"的策略,业内叫绞杀者模式(Strangler Pattern)------名字吓人,思路很朴实。