4. 订单聚合设计(上):订单聚合根(Order)与订单项(OrderItem)的实体建模
在支付订单系统中,订单聚合是整个领域模型的核心。本章将聚焦于订单聚合根 Order 与订单项 OrderItem 的实体建模,明确两者的职责边界、生命周期与持久化映射关系。
4.1 聚合设计原则回顾
根据 Evans 的 DDD 定义,聚合是一组相关对象的集合,外部只能通过聚合根访问内部实体。订单聚合的设计需遵循以下原则:
| 原则 | 说明 | 订单聚合中的应用 |
|---|---|---|
| 一致性边界 | 聚合内所有对象在事务中保持一致 | 订单状态变更时,所有订单项必须同步锁定 |
| 聚合根唯一引用 | 外部只能通过聚合根 ID 引用聚合 | 外部服务只持有 orderId,不直接引用 OrderItem |
| 内部实体本地标识 | 内部实体在聚合内唯一即可 | OrderItem 使用 orderId + itemSeq 作为本地标识 |
| 最小化聚合规模 | 避免加载过多无关对象 | 订单项数量通常 ≤ 50,超过则拆分为子订单 |
实践提示:订单聚合的边界决定了事务粒度。将订单项纳入订单聚合,意味着一次订单提交操作必须保证所有订单项的状态一致,不可出现部分成功部分失败的情况。
4.2 订单聚合根(Order)实体建模
Order 作为聚合根,承担着对外交互、生命周期管理和一致性保障的核心职责。其属性设计应聚焦于订单全局信息,而非商品明细。
4.2.1 核心属性
| 属性 | 类型 | 说明 |
|---|---|---|
orderId |
OrderId(值对象) | 全局唯一标识,由订单号生成服务分配 |
buyerId |
BuyerId(值对象) | 买家标识,关联用户上下文 |
sellerId |
SellerId(值对象) | 卖家标识,关联商户上下文 |
orderStatus |
OrderStatus(枚举) | 待支付、已支付、已取消、已退款等 |
totalAmount |
Money(值对象) | 订单总金额,由订单项金额汇总计算 |
paymentAmount |
Money(值对象) | 实际支付金额,可能包含优惠分摊 |
createdAt |
DateTime | 订单创建时间 |
version |
int | 乐观锁版本号,用于并发控制 |
4.2.2 聚合根行为
- 创建订单:接收
CreateOrderCommand,校验买家/卖家合法性,生成订单项列表,计算总金额。 - 提交支付:校验订单状态为待支付,锁定订单项库存,更新状态为已支付。
- 取消订单:校验订单状态为待支付,释放库存,更新状态为已取消。
- 申请退款:校验订单状态为已支付,生成退款请求,更新状态为退款中。
关键约束:聚合根的所有状态变更必须通过显式方法完成,禁止直接修改属性。例如:
order.submitPayment() 而非 order.setOrderStatus()。
4.3 订单项(OrderItem)实体建模
OrderItem 是订单聚合的内部实体,代表一笔订单中的单个商品购买记录。它不对外暴露,仅通过 Order 聚合根访问。
4.3.1 核心属性
| 属性 | 类型 | 说明 |
|---|---|---|
itemSeq |
int | 订单内序号,从 1 开始递增 |
productId |
ProductId(值对象) | 商品标识,关联商品上下文 |
skuId |
SkuId(值对象) | SKU 标识,关联库存上下文 |
quantity |
int | 购买数量,必须大于 0 |
unitPrice |
Money(值对象) | 商品单价 |
subtotal |
Money(值对象) | 小计金额 = 单价 × 数量 |
itemStatus |
ItemStatus(枚举) | 正常、已退款、部分退款 |
4.3.2 实体行为
- 计算小计:根据单价和数量自动计算,不允许外部直接设置。
- 锁定库存:在订单提交支付时,由聚合根统一调用每个订单项的库存锁定方法。
- 标记退款:当发生退款时,更新
itemStatus为已退款或部分退款。
注意:订单项不应包含商品描述、图片 URL 等展示信息。这些信息属于商品上下文,订单项仅持有
productId 和 skuId,通过领域服务或读模型获取展示数据。
4.4 订单聚合结构图
下图展示了订单聚合的内部结构,以及聚合根与内部实体、值对象之间的关系:
4.5 实体与值对象的代码示例
以下代码展示了 Order 聚合根和 OrderItem 实体的核心实现,采用 Java 语言描述:
// 订单聚合根
public class Order {
private OrderId orderId;
private BuyerId buyerId;
private SellerId sellerId;
private OrderStatus orderStatus;
private Money totalAmount;
private Money paymentAmount;
private LocalDateTime createdAt;
private int version;
private List<OrderItem> items;
// 聚合根行为:提交支付
public void submitPayment(Money amount) {
if (this.orderStatus != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("订单状态不允许支付");
}
// 校验支付金额
if (!this.totalAmount.equals(amount)) {
throw new IllegalArgumentException("支付金额与订单总金额不一致");
}
// 锁定所有订单项库存
for (OrderItem item : this.items) {
item.lockInventory();
}
this.orderStatus = OrderStatus.PAID;
this.paymentAmount = amount;
}
// 聚合根行为:取消订单
public void cancel() {
if (this.orderStatus != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("订单状态不允许取消");
}
for (OrderItem item : this.items) {
item.releaseInventory();
}
this.orderStatus = OrderStatus.CANCELLED;
}
}
// 订单项实体(内部实体)
public class OrderItem {
private int itemSeq;
private ProductId productId;
private SkuId skuId;
private int quantity;
private Money unitPrice;
private Money subtotal;
private ItemStatus itemStatus;
// 计算小计
public void calculateSubtotal() {
this.subtotal = this.unitPrice.multiply(this.quantity);
}
// 锁定库存
public void lockInventory() {
// 调用库存上下文服务
InventoryService.lock(this.skuId, this.quantity);
}
// 释放库存
public void releaseInventory() {
InventoryService.release(this.skuId, this.quantity);
}
}
4.6 持久化映射策略
订单聚合在持久化时,通常采用以下映射方式:
| 数据库表 | 映射对象 | 主键 | 说明 |
|---|---|---|---|
orders |
Order 聚合根 | order_id |
存储订单全局信息 |
order_items |
OrderItem 实体 | order_id + item_seq |
存储订单项明细,外键关联 orders 表 |
实践建议:使用 ORM 框架(如 JPA/Hibernate)时,将
OrderItem 的 @ManyToOne 关联设置为 cascade = ALL 和 orphanRemoval = true,确保聚合内实体生命周期与聚合根一致。
4.7 本章小结
- 订单聚合根负责全局状态管理和一致性边界,外部通过
orderId引用。 - 订单项实体是聚合内部对象,持有商品标识和数量信息,不对外暴露。
- 值对象如
Money、OrderId提供不可变的值语义,增强模型表达力。 - 持久化映射采用主表 + 明细表结构,通过外键维护聚合完整性。