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 判断。状态转换逻辑应封装在聚合根内部,并通过领域服务校验外部条件(如支付网关回调的合法性)。
支付订单状态机流转图 PENDING_PAYMENT PAID PARTIALLY_REFUNDED REFUNDED CANCELLED COMPLETED 支付成功 取消/超时 部分退款 全额退款 剩余退款 确认完成 确认完成 终态:CANCELLED、REFUNDED、COMPLETED 不允许任何状态变更

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),不经过聚合根的方法,以提高查询性能。
订单聚合根内部结构图 聚合根边界(事务一致性) Order(聚合根) orderId, status, totalAmount OrderLineItem(实体) lineItemId, productId, quantity Money(值对象) amount, currency 包含 1..N 使用 领域事件(发布) 外部限界上下文 异步通知 事务边界内 强一致性

4.6 设计要点总结

维度 设计决策 理由
聚合根选择 Order 作为唯一聚合根 订单头与行项目生命周期一致,避免跨聚合根事务
状态机 枚举驱动 + 状态转换表 显式表达合法转换,防止非法状态跃迁
行项目身份 订单内序号(本地标识) 无需全局唯一 ID,减少生成成本与存储开销
金额计算 聚合根内实时计算 保证 totalAmount 与行项目 finalAmount 总和一致
外部引用 通过值对象或 ID 引用 降低聚合间耦合,支持事件驱动最终一致性
下一步实践:在完成订单聚合根建模后,建议结合具体的持久化方案(如 Event Sourcing 或传统 ORM)实现聚合根的仓储(Repository),并编写单元测试覆盖状态机的所有合法与非法转换路径。