4. 支付订单聚合建模(一):订单聚合根设计
聚合根(Aggregate Root)是领域驱动设计中管理一致性与事务边界的核心。在支付订单系统中,订单(Order)作为聚合根,承担着维护订单生命周期、协调子实体(如订单行项目)以及确保状态变更合法性的职责。本节将深入设计订单聚合根的结构、状态机与行项目模型。
4.1 订单聚合根的核心职责
订单聚合根并非简单的数据容器,而是一个具有行为与规则的领域对象。其核心职责包括:
- 生命周期管理:通过状态机驱动订单从创建到完成或关闭的流转。
- 一致性边界:保证订单头与订单行项目在同一个事务中变更,避免部分更新。
- 业务规则校验:如订单总额必须等于行项目金额之和、支付金额不能超过订单总额等。
- 事件发布:在状态变更时触发领域事件(如 OrderPaidEvent),供其他限界上下文消费。
设计原则:聚合根内部的所有变更必须通过聚合根的方法进行,外部不能直接修改订单的内部状态或行项目集合。这保证了数据的一致性与封装性。
4.2 订单状态机设计
支付订单的状态机是订单聚合根的行为骨架。一个典型的支付订单状态机包含以下状态与合法转换:
| 当前状态 | 触发动作 | 目标状态 | 说明 |
|---|---|---|---|
| PENDING_PAYMENT | 支付成功回调 | PAID | 等待用户支付,超时则自动取消 |
| PENDING_PAYMENT | 用户取消 / 超时 | CANCELLED | 未支付前可取消 |
| PAID | 部分退款 | PARTIALLY_REFUNDED | 仅支持一次或多次部分退款 |
| PAID | 全额退款 | REFUNDED | 退款后订单生命周期结束 |
| PARTIALLY_REFUNDED | 剩余金额退款 | REFUNDED | 从部分退款转为全额退款 |
| PAID / PARTIALLY_REFUNDED | 订单完成(如确认收货) | COMPLETED | 终态,不可再退款 |
| CANCELLED / REFUNDED / COMPLETED | — | — | 终态,不允许任何状态变更 |
状态机实现提示:建议使用状态模式(State Pattern)或枚举驱动的状态转换表,避免在业务代码中出现大量的 if-else 判断。状态转换逻辑应封装在聚合根内部,并通过领域服务校验外部条件(如支付网关回调的合法性)。
4.3 订单行项目(Order Line Item)设计
订单行项目是订单聚合根下的实体,每个行项目代表一笔具体的商品或服务购买。行项目没有全局唯一标识,其身份由 订单ID + 行项目序号 共同确定。
4.3.1 行项目核心属性
| 属性 | 类型 | 说明 |
|---|---|---|
| lineItemId | String | 行项目序号(在订单内唯一) |
| productId | String | 商品/服务标识 |
| productName | String | 商品名称(冗余,便于查询) |
| quantity | int | 购买数量 |
| unitPrice | Money | 单价(值对象) |
| subtotal | Money | 小计金额(quantity * unitPrice) |
| discountAmount | Money | 行级优惠金额 |
| finalAmount | Money | 行级实付金额(subtotal - discountAmount) |
4.3.2 行项目业务规则
- 金额一致性:行项目的 finalAmount 必须等于 subtotal 减去 discountAmount,且不能为负数。
- 数量约束:quantity 必须为正整数,且不能超过库存可用量(由库存限界上下文校验)。
- 不可直接修改:行项目一旦添加到订单中,其 productId、unitPrice 等核心属性不可修改;如需变更,应取消原订单并重新下单。
- 退款关联:退款操作必须指定到具体的行项目,支持按行项目部分退款。
常见设计误区:不要将行项目设计为独立的聚合根。行项目脱离订单没有独立生命周期,将其作为实体由订单聚合根统一管理,可以避免分布式事务带来的复杂性,并保证订单金额计算的原子性。
4.4 聚合根与行项目的代码骨架
以下代码展示了订单聚合根与行项目实体的核心结构(Java 风格伪代码):
// 订单聚合根
public class Order {
private OrderId orderId;
private OrderStatus status;
private Money totalAmount;
private Money paidAmount;
private List<OrderLineItem> lineItems;
private LocalDateTime createdAt;
// 行为方法
public void addLineItem(OrderLineItem item) {
// 校验:订单必须处于 PENDING_PAYMENT 状态
if (this.status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("Only pending payment order can add line items");
}
this.lineItems.add(item);
recalculateTotalAmount();
}
public void markPaid(Money amount) {
// 校验状态与金额
if (this.status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("Order is not in pending payment state");
}
if (!amount.equals(this.totalAmount)) {
throw new IllegalArgumentException("Paid amount does not match total amount");
}
this.status = OrderStatus.PAID;
this.paidAmount = amount;
// 发布领域事件
DomainEventPublisher.publish(new OrderPaidEvent(this.orderId));
}
private void recalculateTotalAmount() {
this.totalAmount = this.lineItems.stream()
.map(OrderLineItem::getFinalAmount)
.reduce(Money.ZERO, Money::add);
}
}
// 订单行项目实体
public class OrderLineItem {
private String lineItemId;
private String productId;
private String productName;
private int quantity;
private Money unitPrice;
private Money discountAmount;
public Money getSubtotal() {
return unitPrice.multiply(quantity);
}
public Money getFinalAmount() {
return getSubtotal().subtract(discountAmount);
}
}
4.5 聚合设计中的边界与一致性
在支付订单系统中,订单聚合根的设计需要明确以下边界:
- 事务边界:一个事务内只修改一个订单聚合根。如果需要同时修改订单和用户账户余额,应通过领域事件异步处理,或使用 Saga 模式。
- 引用边界:订单聚合根内部通过值对象(如 Money、OrderId)引用外部实体,避免直接持有其他聚合根的引用。
- 查询边界:查询订单列表或行项目详情时,应使用单独的查询模型(CQRS),不经过聚合根的方法,以提高查询性能。
4.6 设计要点总结
| 维度 | 设计决策 | 理由 |
|---|---|---|
| 聚合根选择 | Order 作为唯一聚合根 | 订单头与行项目生命周期一致,避免跨聚合根事务 |
| 状态机 | 枚举驱动 + 状态转换表 | 显式表达合法转换,防止非法状态跃迁 |
| 行项目身份 | 订单内序号(本地标识) | 无需全局唯一 ID,减少生成成本与存储开销 |
| 金额计算 | 聚合根内实时计算 | 保证 totalAmount 与行项目 finalAmount 总和一致 |
| 外部引用 | 通过值对象或 ID 引用 | 降低聚合间耦合,支持事件驱动最终一致性 |
下一步实践:在完成订单聚合根建模后,建议结合具体的持久化方案(如 Event Sourcing 或传统 ORM)实现聚合根的仓储(Repository),并编写单元测试覆盖状态机的所有合法与非法转换路径。