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