2. 支付订单聚合模型设计:订单聚合根的定义、订单项实体、支付凭证值对象的设计原则

在支付领域驱动设计(DDD)中,聚合(Aggregate)是保证数据一致性与事务边界的最小单元。支付订单聚合模型的设计直接决定了后续OpenAPI接口的粒度、参数结构以及测试用例的覆盖范围。本节将围绕订单聚合根、订单项实体和支付凭证值对象三个核心概念,阐述其设计原则与建模要点。

2.1 订单聚合根的定义与设计原则

聚合根(Aggregate Root)是聚合中唯一能被外部引用的对象,所有对聚合内部对象的访问都必须通过聚合根。在支付订单上下文中,订单聚合根承担着整个支付流程的入口与状态机控制器的角色。

2.1.1 聚合根的核心职责

  • 全局唯一标识:每个订单聚合根拥有一个全局唯一的订单ID(如 orderId),作为外部系统(商户、网关、清算)引用的唯一凭证。
  • 状态机管理:维护订单生命周期状态(待支付、支付中、已支付、支付失败、已退款等),并确保状态转换的合法性。
  • 事务边界:聚合根内部的所有变更(订单项、支付凭证、金额计算)必须在同一个事务中完成,保证最终一致性。
  • 版本控制:引入乐观锁(如 version 字段)防止并发更新导致的数据覆盖。

2.1.2 聚合根的设计原则

原则 说明 反例
单一职责 聚合根只负责订单核心流程,不承载支付凭证的持久化细节 在订单表中直接存储银行卡号明文
最小化引用 聚合根通过ID引用其他聚合,避免直接持有对象引用 订单聚合根直接持有用户聚合的完整对象
强一致性边界 聚合内所有对象在同一事务中更新,不跨聚合做分布式事务 订单支付成功后,异步通知库存系统扣减
事件溯源友好 聚合根的状态变更应能通过领域事件完整还原 直接修改数据库字段而不记录事件
设计提示:在OpenAPI接口设计中,聚合根ID(orderId)应作为所有订单相关接口的路径参数或必填查询参数,例如 GET /api/v1/orders/{orderId},确保接口粒度与聚合边界对齐。

2.2 订单项实体的建模

订单项(OrderItem)是订单聚合内的实体,拥有自己的本地标识(itemId),但该标识仅在聚合内唯一。订单项描述了用户购买的具体商品或服务,是金额计算与分账的最小单元。

2.2.1 订单项的核心属性

  • 商品标识:productId(外部商品ID)与 skuId(库存单位ID)。
  • 数量与单价:quantity、unitPrice(以分为单位,避免浮点精度问题)。
  • 金额快照:subtotal(小计金额)、discountAmount(优惠分摊)、taxAmount(税费)。
  • 分账信息:settlementRate(分账比例)或 settlementTarget(分账方ID)。

2.2.2 订单项的设计原则

原则 描述 实现要点
不可变性 订单项一旦创建,其商品ID、单价、数量不应被修改 使用构造器模式,不提供setter方法
金额自包含 每个订单项独立计算小计与优惠,不依赖外部计算 在实体内部封装 calculateSubtotal() 方法
聚合内唯一 订单项ID仅在所属订单聚合内保证唯一 使用 orderId + 自增序号 作为本地标识
// 订单项实体示例(Java伪代码)
public class OrderItem {
    private String itemId;          // 聚合内唯一
    private String productId;       // 外部商品ID
    private String skuId;           // 库存单位ID
    private int quantity;           // 数量
    private long unitPrice;         // 单价(分)
    private long subtotal;          // 小计(分)
    private long discountAmount;    // 优惠分摊(分)

    // 构造器:创建时即完成金额计算
    public OrderItem(String productId, String skuId, int quantity, long unitPrice) {
        this.productId = productId;
        this.skuId = skuId;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
        this.subtotal = quantity * unitPrice;
        this.discountAmount = 0;
    }

    // 不允许setter,只提供getter
    public long getSubtotal() { return subtotal; }
    public long getDiscountAmount() { return discountAmount; }
}

2.3 支付凭证值对象的设计原则

支付凭证(PaymentVoucher)是订单聚合内的值对象,没有全局标识,其相等性由所有属性值决定。支付凭证记录了支付交易的凭证信息,包括支付渠道、交易流水号、支付金额、支付时间等。

2.3.1 值对象的核心特征

  • 不可变性:一旦创建,支付凭证的属性不可修改;如需变更,只能整体替换。
  • 无标识:不拥有独立的ID,其身份由属性组合决定。
  • 自包含验证:在构造时完成所有业务规则校验(如金额匹配、渠道有效性)。

2.3.2 支付凭证的属性设计

属性 类型 说明
channelCode String 支付渠道编码(如 WXPAY、ALIPAY、UNIONPAY)
transactionId String 支付渠道返回的交易流水号
paidAmount Long 实际支付金额(分),必须等于订单总金额
paidAt LocalDateTime 支付完成时间
voucherStatus Enum 凭证状态(SUCCESS、FAILED、REFUNDED)
注意:支付凭证作为值对象,不应包含任何业务逻辑(如状态转换),所有状态变更应由聚合根通过替换凭证对象来实现。例如,当支付成功时,聚合根创建一个新的 PaymentVoucher 实例替换旧的待支付凭证。

2.4 聚合模型整体结构图

下图展示了支付订单聚合模型中聚合根、实体与值对象之间的层级关系与交互边界:

支付订单聚合模型结构图 支付订单聚合模型结构图 订单聚合根 (Order) orderId, status, version, totalAmount 订单项实体 (OrderItem) itemId, productId, skuId quantity, unitPrice, subtotal discountAmount, taxAmount 订单项实体 (OrderItem) itemId, productId, skuId quantity, unitPrice, subtotal discountAmount, taxAmount 支付凭证值对象 (PaymentVoucher) channelCode, transactionId paidAmount, paidAt, voucherStatus (不可变,无全局标识) 说明:聚合根通过ID引用外部聚合;订单项为实体,拥有本地标识;支付凭证为值对象,整体替换。 所有变更必须通过聚合根入口,保证事务边界与数据一致性。

2.5 设计原则总结

模型元素 类型 标识 可变性 事务边界
订单 聚合根 全局唯一ID 状态可变 聚合内所有对象
订单项 实体 聚合内唯一ID 创建后不可变 跟随聚合根
支付凭证 值对象 无标识 完全不可变 整体替换
关键结论:支付订单聚合模型的设计应遵循“聚合根控制边界、实体承载明细、值对象封装凭证”的分层原则。在OpenAPI接口参数配置中,聚合根ID作为路径参数,订单项作为数组参数,支付凭证作为嵌套对象参数,三者共同构成完整的支付订单请求/响应结构。这种设计不仅保证了领域模型的清晰性,也为后续的接口测试提供了明确的边界划分。