3. 聚合建模实战:订单聚合根设计、订单项实体、支付记录值对象、地址值对象
在支付订单系统中,领域驱动设计(DDD)的聚合建模是核心环节。本节将围绕“订单”这一核心聚合,详细拆解其内部组成:订单聚合根、订单项实体、支付记录值对象以及地址值对象。通过明确的边界划分与职责定义,确保数据一致性、业务完整性与高性能缓存策略的落地。
3.1 聚合根设计:订单(Order)
订单聚合根是整个订单生命周期的唯一入口与一致性边界。所有对订单内部状态的修改,必须通过聚合根暴露的方法进行,禁止外部直接操作内部实体或值对象。
| 设计要素 | 说明 | 示例 |
|---|---|---|
| 唯一标识 | 全局唯一订单号(OrderId),通常由分布式ID生成器产生 | 202503211234567890 |
| 生命周期状态 | 待支付、已支付、已取消、已退款、已完成 | 状态机驱动流转 |
| 内部集合 | 持有订单项实体集合(OrderItems) | List<OrderItem> |
| 值对象引用 | 持有支付记录值对象、地址值对象 | PaymentRecord, Address |
| 业务方法 | 创建订单、支付、取消、修改收货地址 | pay(), cancel(), changeAddress() |
设计原则:聚合根内部的所有变更必须保证最终一致性。例如,当订单支付成功时,聚合根应同时更新订单状态、生成支付记录,并触发库存扣减事件,而非由外部服务分别操作。
3.2 订单项实体(OrderItem)
订单项是订单聚合内的实体,拥有自己的本地标识(OrderItemId),但其生命周期完全依赖于订单聚合根。订单项代表用户购买的具体商品及数量。
- 核心属性:商品ID(SkuId)、商品名称、单价、购买数量、实付金额、优惠分摊金额。
- 业务约束:数量必须大于0;实付金额 = 单价 × 数量 - 优惠分摊。
- 变更场景:仅在订单创建时添加,或在订单拆分/合并时调整,不允许独立于订单根进行修改。
缓存视角:订单项通常作为订单聚合根的嵌套集合整体缓存。在Redis中,使用Hash结构存储订单聚合根,其中订单项序列化为JSON字符串作为一个field。避免将每个订单项独立缓存,以减少缓存碎片与网络开销。
3.3 支付记录值对象(PaymentRecord)
支付记录是不可变的值对象,用于记录一次支付行为的完整快照。一旦生成,其属性不应被修改;若需变更支付方式或金额,应生成新的支付记录。
| 属性 | 类型 | 说明 |
|---|---|---|
| paymentId | String | 支付流水号(第三方支付平台返回) |
| payAmount | BigDecimal | 实际支付金额(单位:分) |
| payMethod | Enum | 支付、支付宝、银行卡等 |
| payTime | Long | 支付完成时间戳 |
| tradeStatus | Enum | 成功、失败、退款中、已退款 |
不可变性陷阱:值对象在Java中应声明为final类,所有字段使用final修饰,且不提供setter方法。在Redis缓存中,支付记录作为订单Hash的一个field存储,更新时直接覆盖整个值对象,而非修改内部字段。
3.4 地址值对象(Address)
地址同样作为值对象存在,用于描述订单的收货信息。由于地址变更频繁且不涉及业务一致性(不影响订单金额),将其设计为值对象可简化聚合边界。
- 属性组成:收件人姓名、联系电话、省份、城市、区县、详细地址、邮政编码。
- 业务规则:地址变更时,直接替换整个值对象,无需保留历史版本(除非业务要求物流轨迹追溯)。
- 缓存策略:地址与订单聚合根一同缓存,避免单独查询地址表。若地址数据量极大,可考虑在Redis中单独设置过期时间较短的缓存,但需与订单缓存保持一致性。
3.5 聚合建模整体结构图
以下SVG图展示了订单聚合根、订单项实体、支付记录值对象、地址值对象之间的包含关系与边界。
3.6 聚合内数据一致性策略
在支付订单系统中,聚合内的一致性通过以下机制保障:
- 事务边界:聚合根内部的所有操作(如创建订单+添加订单项+生成支付记录)应在同一个数据库事务中完成。
- 缓存更新:聚合根数据变更后,先更新数据库,再删除或更新Redis缓存(Cache-Aside模式)。
- 事件驱动:聚合根完成状态变更后,发布领域事件(如OrderPaidEvent),由订阅者处理库存扣减、积分赠送等跨聚合操作。
性能调优建议:对于高频读取的订单聚合,在Redis中使用Hash结构存储所有字段,避免序列化整个对象。例如:
HSET order:202503211234567890 status PAID totalAmount 9900 items "[{\"skuId\":\"1001\",\"qty\":2}]" payment "{\"payMethod\":\"WECHAT\",\"payAmount\":9900}" address "{\"city\":\"Beijing\"}"
这样可在不反序列化整个对象的情况下,单独获取订单状态或地址信息,显著降低缓存读取延迟。
3.7 聚合建模与缓存选型对照
| 聚合元素 | 推荐缓存结构 | 缓存Key设计 | 过期策略 |
|---|---|---|---|
| 订单聚合根 | Redis Hash | order:{orderId} | TTL 30分钟 + 主动更新 |
| 订单项集合 | Hash内嵌JSON | order:{orderId} → field: items | 随聚合根过期 |
| 支付记录 | Hash内嵌JSON | order:{orderId} → field: payment | 随聚合根过期 |
| 地址 | Hash内嵌JSON | order:{orderId} → field: address | 随聚合根过期 |