4. 订单聚合建模(一):订单实体设计、订单状态机与订单行项目值对象

在支付订单系统中,订单(Order)是核心聚合根,承载着交易的生命周期与业务约束。本章聚焦于订单实体的属性设计、状态机流转规则以及订单行项目(Order Line Item)的值对象建模,为后续的聚合边界划分与仓储实现奠定基础。

4.1 订单实体设计

订单实体具有全局唯一标识(OrderId),其属性分为核心业务属性与审计/扩展属性两类。以下表格列出了订单实体的关键字段及其领域含义:

字段名 类型 领域含义 约束
orderId OrderId(值对象) 全局唯一订单标识 不可变,由领域服务生成
buyerId BuyerId(值对象) 买家标识 非空
orderStatus OrderStatus(枚举) 当前订单状态 初始为 PENDING_PAYMENT
totalAmount Money(值对象) 订单总金额(含优惠) ≥ 0,由行项目金额汇总计算
orderLineItems List<OrderLineItem> 订单行项目集合 至少包含一个行项目
createdAt DateTime 订单创建时间 不可变
updatedAt DateTime 最后更新时间 每次状态变更时更新
领域建模提示:订单实体中的 totalAmount 不应作为独立存储字段,而应通过 OrderLineItem 集合的金额实时计算得出,以保证数据一致性。这是领域驱动设计中“不信任外部输入”原则的体现。

4.2 订单状态机(待支付、已支付、已取消、已退款)

订单状态机是订单聚合的核心行为规则。它定义了订单从创建到终态(已取消或已退款)的合法流转路径。以下是四种核心状态及其转换规则:

  • 待支付(PENDING_PAYMENT):订单创建后的初始状态。允许执行支付操作或取消操作。
  • 已支付(PAID):支付成功后进入的状态。允许执行退款操作,不允许再次支付或取消。
  • 已取消(CANCELLED):在待支付状态下取消订单后的终态。不允许任何后续操作。
  • 已退款(REFUNDED):在已支付状态下执行退款后的终态。不允许再次退款或恢复支付。
状态机约束:不允许从“已支付”直接跳转到“已取消”,也不允许从“已取消”或“已退款”回退到任何非终态。所有状态变更必须通过订单聚合根上的领域方法(如 pay()、cancel()、refund())触发,并校验当前状态是否合法。
订单状态机流转图 待支付 PENDING_PAYMENT 已支付 PAID 已取消 CANCELLED 已退款 REFUNDED 支付 取消 退款 状态机规则 仅允许单向流转 终态不可回退

4.3 订单行项目值对象

订单行项目(OrderLineItem)在领域中被建模为值对象(Value Object),因为它没有独立的生命周期,完全从属于订单聚合根。行项目的相等性由其所有属性共同决定,而非通过ID标识。

行项目值对象包含以下属性:

属性 类型 说明
productId ProductId(值对象) 商品标识
productName String 商品名称(快照,防止商品信息变更影响历史订单)
unitPrice Money(值对象) 下单时的单价
quantity int 购买数量,必须大于0
subtotal Money(值对象) 小计金额 = unitPrice × quantity
建模陷阱提醒:不要将行项目设计为实体(Entity)。行项目没有独立ID,也不会在订单外部被单独引用。如果业务需要追踪单个商品的售后状态(如部分退款),应通过引入“退款申请”聚合来处理,而非将行项目提升为实体。
订单聚合结构图 订单聚合根 Order (Entity) orderId: OrderId buyerId: BuyerId orderStatus: OrderStatus totalAmount: Money orderLineItems: List<OLI> + pay() / cancel() / refund() 行项目值对象 OrderLineItem (VO) productId: ProductId productName: String unitPrice: Money quantity: int subtotal: Money (无独立ID) 包含 1..* 聚合规则 行项目只能通过 订单聚合根访问 外部不可直接引用

4.4 领域方法示例

以下代码展示了订单聚合根中状态变更方法的典型实现,体现了封装不变条件的DDD原则:

public class Order : AggregateRoot<OrderId>
{
    private List<OrderLineItem> _orderLineItems;
    public IReadOnlyList<OrderLineItem> OrderLineItems => _orderLineItems.AsReadOnly();

    public OrderStatus Status { get; private set; }
    public Money TotalAmount { get; private set; }

    public void Pay()
    {
        if (Status != OrderStatus.PENDING_PAYMENT)
            throw new DomainException("只有待支付订单可以支付");

        Status = OrderStatus.PAID;
        // 触发领域事件:OrderPaidDomainEvent
    }

    public void Cancel()
    {
        if (Status != OrderStatus.PENDING_PAYMENT)
            throw new DomainException("只有待支付订单可以取消");

        Status = OrderStatus.CANCELLED;
    }

    public void Refund()
    {
        if (Status != OrderStatus.PAID)
            throw new DomainException("只有已支付订单可以退款");

        Status = OrderStatus.REFUNDED;
    }
}
实践建议:状态变更方法应返回 void 并抛出领域异常,而非返回布尔值。这样调用方必须显式处理异常,避免静默失败导致状态不一致。同时,每个状态变更方法都应触发对应的领域事件,便于其他限界上下文(如库存、通知)进行异步响应。