4. 订单聚合设计(上):订单聚合根(Order)的职责、状态机
在支付订单系统中,订单(Order) 是核心业务实体,也是领域驱动设计中最重要的聚合根(Aggregate Root)。聚合根是外部访问聚合内实体的唯一入口,它负责维护整个聚合的业务一致性和不变量。本章将深入分析订单聚合根的职责划分与状态机设计,为后续的幂等处理和缓存协同奠定基础。
4.1 订单聚合根的核心职责
订单聚合根并非简单的数据容器,它承载着以下关键职责:
- 生命周期管理:控制订单从创建到完成的整个生命周期,确保状态转换的合法性。
- 业务不变量守护:保证订单金额、商品数量、支付状态等核心数据在任何操作前后都满足业务规则。
- 子实体协调:管理订单项(OrderItem)、支付记录(PaymentRecord)、物流信息(ShippingInfo)等子实体的创建与更新。
- 领域事件发布:在关键状态变更时(如支付成功、订单取消)发布领域事件,驱动其他限界上下文(如库存、通知)的后续处理。
- 外部访问门面:所有对订单数据的修改都必须通过聚合根的方法进行,不允许直接操作子实体。
设计原则:聚合根内部的数据修改必须通过显式的业务方法(如
pay()、cancel()),而不是暴露 setter 方法。这能有效防止外部代码绕过业务规则直接修改状态。
4.2 订单状态机设计
订单状态机是聚合根行为建模的核心。一个设计良好的状态机能够清晰表达业务流转,并防止非法状态跃迁。在支付订单系统中,我们定义以下三种核心状态:
| 状态 | 枚举值 | 业务含义 | 允许的操作 |
|---|---|---|---|
| 待支付 | PENDING_PAYMENT |
订单已创建,等待用户完成支付 | 支付、取消 |
| 已支付 | PAID |
用户已完成支付,订单进入后续履约流程 | 退款、发货(由其他聚合处理) |
| 已取消 | CANCELLED |
订单在支付前被用户或系统取消 | 无(终态) |
状态转换必须遵循严格的规则,不允许出现“已支付”直接跳转到“待支付”或“已取消”跳转到“已支付”等非法路径。下图展示了订单状态机的完整流转:
4.3 聚合根行为与状态机的协同
聚合根的方法实现必须与状态机严格对应。以下是一个典型的订单聚合根代码结构示例:
public class Order {
private OrderId orderId;
private OrderStatus status;
private Money totalAmount;
private List<OrderItem> items;
private PaymentRecord paymentRecord;
// 私有构造器,通过工厂方法创建
private Order() {}
// 创建订单(工厂方法)
public static Order create(OrderId orderId, List<OrderItem> items) {
Order order = new Order();
order.orderId = orderId;
order.status = OrderStatus.PENDING_PAYMENT;
order.items = new ArrayList<>(items);
order.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
// 发布 OrderCreatedEvent
DomainEventPublisher.publish(new OrderCreatedEvent(orderId));
return order;
}
// 支付操作
public void pay(PaymentRecord paymentRecord) {
if (this.status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("订单状态不允许支付");
}
if (!this.totalAmount.equals(paymentRecord.getAmount())) {
throw new IllegalArgumentException("支付金额与订单金额不匹配");
}
this.paymentRecord = paymentRecord;
this.status = OrderStatus.PAID;
// 发布 OrderPaidEvent
DomainEventPublisher.publish(new OrderPaidEvent(this.orderId, paymentRecord.getPaymentId()));
}
// 取消操作
public void cancel(CancelReason reason) {
if (this.status == OrderStatus.PAID) {
// 已支付订单取消需要触发退款流程
this.status = OrderStatus.CANCELLED;
DomainEventPublisher.publish(new OrderCancelledEvent(this.orderId, reason));
DomainEventPublisher.publish(new RefundRequiredEvent(this.orderId, this.totalAmount));
} else if (this.status == OrderStatus.PENDING_PAYMENT) {
this.status = OrderStatus.CANCELLED;
DomainEventPublisher.publish(new OrderCancelledEvent(this.orderId, reason));
} else {
throw new IllegalStateException("订单已取消,无法重复取消");
}
}
}
设计要点:
- 聚合根的方法名使用业务术语(
pay、cancel),而非技术术语(setStatus)。 - 每个业务方法内部先校验状态合法性,再执行状态变更,最后发布领域事件。
- 金额比较使用
Money值对象,避免浮点数精度问题。
4.4 状态机与幂等性的关系
状态机设计直接影响幂等处理的实现难度。在分布式系统中,支付回调可能重复到达,状态机需要配合幂等机制确保安全:
- 待支付 → 已支付:如果重复收到支付成功回调,聚合根应检测到当前状态已经是
PAID,直接返回成功,不重复执行业务逻辑。 - 待支付 → 已取消:取消请求重复到达时,同样通过状态判断实现幂等。
- 已支付 → 已取消(退款):退款请求的幂等需要结合退款流水号(RefundId)进行去重,状态机仅作为最终一致性校验。
下图展示了状态机与幂等处理的协同关系:
常见陷阱:不要在聚合根外部直接修改订单状态。例如,在应用服务层通过
order.setStatus(OrderStatus.PAID) 的方式绕过聚合根方法,会导致状态机校验失效、领域事件丢失,最终破坏业务一致性。