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)进行去重,状态机仅作为最终一致性校验。

下图展示了状态机与幂等处理的协同关系:

状态机与幂等处理协同 幂等校验层(Redis / 数据库唯一键) 状态机决策:当前状态是否允许转换? 允许 不允许 执行状态变更 + 发布事件 直接返回成功(幂等)
常见陷阱:不要在聚合根外部直接修改订单状态。例如,在应用服务层通过 order.setStatus(OrderStatus.PAID) 的方式绕过聚合根方法,会导致状态机校验失效、领域事件丢失,最终破坏业务一致性。

4.5 本章小结