4. 订单上下文(Order Context)分析
订单上下文是支付订单领域聚合建模的核心子域,承载着订单从创建到终态的全生命周期管理。本节将深入分析订单的状态流转、核心业务规则以及聚合边界划分,为后续的服务边界与通信机制设计奠定基础。
4.1 订单生命周期状态机
支付订单的状态变迁遵循严格的有限状态机(FSM)模型,每个状态转换都对应明确的业务操作与领域事件。以下是订单的核心状态定义与转换规则:
| 状态 | 状态码 | 描述 | 允许的下游状态 |
|---|---|---|---|
| 创建 | CREATED | 订单已生成,但尚未发起支付 | 待支付、已取消 |
| 待支付 | PENDING | 已发起支付请求,等待支付网关回调 | 已支付、已取消 |
| 已支付 | PAID | 支付成功,资金已冻结或到账 | 已退款、部分退款 |
| 已取消 | CANCELLED | 订单被用户或系统取消,未产生资金变动 | 终态 |
| 已退款 | REFUNDED | 全额退款完成,资金已退回用户 | 终态 |
| 部分退款 | PARTIAL_REFUND | 部分金额已退款,剩余金额仍有效 | 已退款 |
关键约束: 订单状态转换必须满足幂等性与顺序一致性。例如,已取消的订单不可再转为待支付;已退款的订单不可重复退款。状态机应通过领域层守卫(Guard)强制执行。
4.2 核心业务规则
订单上下文必须强制执行以下业务规则,以保证资金安全与数据一致性:
- 金额一致性规则: 订单总金额必须等于所有订单项金额之和,且不得为负数或零(除零元体验单外)。
- 支付超时规则: 待支付订单超过指定时间(如30分钟)未完成支付,系统应自动取消订单并释放库存。
- 取消前置规则: 只有处于创建或待支付状态的订单允许取消;已支付订单必须走退款流程。
- 退款金额约束: 累计退款金额不得超过订单实际支付金额,部分退款时剩余金额必须大于零。
- 幂等性规则: 同一支付请求或退款请求重复提交时,系统应返回相同结果,不产生重复扣款或重复退款。
实践建议: 在订单聚合根中维护一个
version 字段(乐观锁),每次状态变更时校验版本号,可有效防止并发场景下的状态覆盖问题。
4.3 订单聚合边界与实体划分
在订单上下文中,聚合(Aggregate)的边界划分直接影响事务一致性与性能。推荐将以下实体组织在同一个订单聚合内:
| 实体/值对象 | 类型 | 职责 |
|---|---|---|
| Order(订单) | 聚合根 | 持有订单全局标识、状态、金额、用户ID,负责状态变更的守卫逻辑 |
| OrderItem(订单项) | 实体 | 描述单个商品/服务的购买信息,包含商品ID、单价、数量、小计 |
| PaymentRecord(支付记录) | 实体 | 记录每次支付请求的流水,包括支付渠道、交易号、金额、时间 |
| RefundRecord(退款记录) | 实体 | 记录每次退款操作的流水,包括退款原因、金额、渠道返回信息 |
| OrderAddress(订单地址) | 值对象 | 收货地址的快照,不可变,随订单创建时生成 |
边界警告: 不要将支付网关的会话信息(如 token、sessionId)放入订单聚合内。这些属于支付上下文(Payment Context)的职责,跨聚合引用会导致强耦合与性能瓶颈。
4.4 领域事件与跨上下文通信
订单上下文通过领域事件(Domain Event)与其他上下文解耦通信。关键事件包括:
OrderCreated— 订单创建成功后发布,触发库存预留、优惠券锁定等后续流程。OrderPaid— 支付成功后发布,触发发货、积分赠送等流程。OrderCancelled— 订单取消后发布,触发库存释放、优惠券回滚。OrderRefunded— 退款完成后发布,触发财务对账、用户通知。
事件驱动设计要点: 领域事件应包含事件ID、发生时间、订单ID、事件类型以及必要的业务载荷(如退款金额)。事件发布应采用事务性发件箱模式(Transactional Outbox),确保事件不会因本地事务回滚而丢失。
4.5 关键代码示例:订单状态变更守卫
以下代码展示了订单聚合根中状态变更的守卫逻辑,确保状态转换的合法性:
public class Order : AggregateRoot
{
public OrderId Id { get; private set; }
public OrderStatus Status { get; private set; }
public Money TotalAmount { get; private set; }
private List<OrderItem> _items;
private List<PaymentRecord> _payments;
private List<RefundRecord> _refunds;
// 状态转换映射表
private static readonly Dictionary<OrderStatus, HashSet<OrderStatus>> _allowedTransitions =
new()
{
{ OrderStatus.Created, new() { OrderStatus.Pending, OrderStatus.Cancelled } },
{ OrderStatus.Pending, new() { OrderStatus.Paid, OrderStatus.Cancelled } },
{ OrderStatus.Paid, new() { OrderStatus.Refunded, OrderStatus.PartialRefund } },
{ OrderStatus.PartialRefund, new() { OrderStatus.Refunded } },
{ OrderStatus.Cancelled, new() },
{ OrderStatus.Refunded, new() }
};
public void TransitionTo(OrderStatus newStatus)
{
if (!_allowedTransitions[Status].Contains(newStatus))
throw new DomainException($"不允许从 {Status} 转换到 {newStatus}");
var previousStatus = Status;
Status = newStatus;
// 发布领域事件
AddDomainEvent(new OrderStatusChangedEvent(Id, previousStatus, newStatus));
}
public void Cancel()
{
TransitionTo(OrderStatus.Cancelled);
AddDomainEvent(new OrderCancelledEvent(Id));
}
public void MarkPaid()
{
TransitionTo(OrderStatus.Paid);
AddDomainEvent(new OrderPaidEvent(Id));
}
}
设计要点: 状态转换守卫逻辑放在聚合根内部,确保任何外部调用都无法绕过业务规则直接修改状态。领域事件在状态变更后立即发布,由事件总线异步分发到其他上下文。