3. 支付订单聚合设计:聚合根、状态机与生命周期

在支付领域,订单聚合是承载交易核心状态与行为的实体。本章围绕聚合根设计、状态机与生命周期三个维度,构建一个可落地、可验证的支付订单聚合模型。以下内容将直接映射到后续代码实现与领域事件设计。

3.1 聚合根设计原则

支付订单聚合根(Order Aggregate Root)是领域模型的入口,负责维护订单内部一致性。设计时需遵循以下原则:

  • 唯一标识:每个聚合根通过 OrderId(值对象)全局唯一标识,不可变。
  • 内部一致性:所有订单状态变更必须经过聚合根方法,不允许外部直接修改子实体。
  • 边界明确:聚合内只包含订单主表、支付记录、退款记录等强一致性实体;外部关联(如用户、商品)通过 ID 引用。
  • 事务范围:一次聚合根操作对应一个工作单元,保证原子性。
聚合元素 类型 说明
OrderId 值对象 全局唯一订单号,包含时间戳+业务标识+随机数
OrderRoot 聚合根 持有订单状态、金额、支付方式、生命周期方法
PaymentRecord 实体 每次支付尝试的记录,包含支付渠道、流水号、结果
RefundRecord 实体 退款记录,关联原支付记录,支持部分退款
OrderItem 值对象 订单行项目,包含商品ID、数量、单价
设计提示:聚合根不宜过大。如果订单需要频繁查询商品详情,应将商品信息拆出为独立聚合,通过领域服务协调。

3.2 订单状态机

支付订单状态机是领域行为的核心,它定义了订单从创建到完成的合法状态转换路径。状态机必须满足:确定性(每个事件只产生一个目标状态)、可追溯(每次转换记录事件与时间戳)、防重入(已终态不可回退)。

以下为支付订单的核心状态与转换事件:

当前状态 触发事件 目标状态 约束条件
待支付 支付发起 支付中 订单未超时,金额>0
支付中 支付成功回调 已支付 渠道流水号唯一,金额匹配
支付中 支付失败回调 待支付 重试次数未超限
支付中 超时未回调 已关闭 超过支付有效期
已支付 发起退款 退款中 退款金额≤可退金额
退款中 退款成功回调 已退款 退款流水号唯一
退款中 退款失败回调 已支付 可重新发起退款
已支付 / 已退款 订单完成 已完成 所有子项履约完毕
常见陷阱:不要将“支付中”视为临时状态而忽略持久化。支付中状态可能持续数秒到数分钟,期间需要支持查询、超时处理、重试等操作,必须作为正式状态纳入状态机。
支付订单状态机(核心转换路径) 待支付 支付中 已支付 退款中 已退款 已关闭 已完成 支付发起 支付成功 超时关闭 发起退款 退款成功 订单完成 说明: 状态机保证每个事件只产生一个目标状态;已关闭、已完成、已退款为终态,不可回退。

3.3 订单生命周期

订单生命周期描述了订单从创建到最终归档的完整过程。每个阶段都有明确的领域行为、事件与数据约束。生命周期分为以下阶段:

  1. 创建阶段:聚合根初始化,状态为“待支付”,生成订单ID,记录创建时间与过期时间。
  2. 支付阶段:接收支付指令,创建支付记录实体,调用支付网关,等待异步回调。支持多次支付尝试(重试)。
  3. 履约阶段:支付成功后,订单进入“已支付”,触发后续履约流程(如发货、服务开通)。
  4. 退款阶段:支持全额或部分退款,每次退款生成独立的退款记录,状态迁移至“退款中”直至“已退款”。
  5. 完成/关闭阶段:所有子项履约完毕进入“已完成”;超时未支付或用户主动取消进入“已关闭”。
  6. 归档阶段:终态订单超过保留期后,迁移至历史表或冷存储,聚合根生命周期结束。
生命周期事件示例:每个阶段转换都应发布领域事件,例如 OrderCreated、OrderPaid、OrderRefunded、OrderCompleted。这些事件是后续分析与补偿的基础。
订单生命周期泳道图 用户端 订单服务 支付网关 创建订单 发起支付 处理支付 调用渠道 异步回调 更新状态 查看结果 履约/归档 生命周期关键点: 创建→支付→履约→归档,每个阶段都有明确的领域事件与数据约束;异步回调是状态一致性的核心挑战。

3.4 聚合根代码骨架

以下为 Go 语言实现的支付订单聚合根核心结构与方法,展示状态机与生命周期的代码映射:

// OrderRoot 支付订单聚合根
type OrderRoot struct {
    ID          OrderID
    Status      OrderStatus
    TotalAmount Money
    PaidAmount  Money
    RefundedAmount Money
    Items       []OrderItem
    Payments    []*PaymentRecord
    Refunds     []*RefundRecord
    CreatedAt   time.Time
    UpdatedAt   time.Time
    ExpireAt    time.Time
}

// PlaceOrder 创建订单(聚合根工厂方法)
func PlaceOrder(items []OrderItem, expireDuration time.Duration) (*OrderRoot, error) {
    if len(items) == 0 {
        return nil, ErrEmptyOrder
    }
    total := calculateTotal(items)
    id := NewOrderID()
    now := time.Now()
    return &OrderRoot{
        ID:          id,
        Status:      OrderStatusPending,
        TotalAmount: total,
        Items:       items,
        CreatedAt:   now,
        UpdatedAt:   now,
        ExpireAt:    now.Add(expireDuration),
    }, nil
}

// StartPayment 发起支付(状态机转换:待支付 → 支付中)
func (o *OrderRoot) StartPayment(payMethod PaymentMethod) (*PaymentRecord, error) {
    if o.Status != OrderStatusPending {
        return nil, ErrInvalidStatus
    }
    if time.Now().After(o.ExpireAt) {
        o.Status = OrderStatusClosed
        return nil, ErrOrderExpired
    }
    record := NewPaymentRecord(o.ID, payMethod, o.TotalAmount)
    o.Payments = append(o.Payments, record)
    o.Status = OrderStatusPaying
    o.UpdatedAt = time.Now()
    return record, nil
}

// ConfirmPayment 确认支付回调(状态机转换:支付中 → 已支付)
func (o *OrderRoot) ConfirmPayment(paymentID string, channelTradeNo string) error {
    if o.Status != OrderStatusPaying {
        return ErrInvalidStatus
    }
    record := o.findPayment(paymentID)
    if record == nil {
        return ErrPaymentNotFound
    }
    record.Confirm(channelTradeNo)
    o.Status = OrderStatusPaid
    o.PaidAmount = o.TotalAmount
    o.UpdatedAt = time.Now()
    return nil
}

// RequestRefund 发起退款(状态机转换:已支付 → 退款中)
func (o *OrderRoot) RequestRefund(amount Money, reason string) (*RefundRecord, error) {
    if o.Status != OrderStatusPaid {
        return nil, ErrInvalidStatus
    }
    refundable := o.TotalAmount.Sub(o.RefundedAmount)
    if amount.GreaterThan(refundable) {
        return nil, ErrRefundExceed
    }
    record := NewRefundRecord(o.ID, amount, reason)
    o.Refunds = append(o.Refunds, record)
    o.Status = OrderStatusRefunding
    o.UpdatedAt = time.Now()
    return record, nil
}
实现要点:聚合根方法必须返回明确的错误类型,调用方根据错误决定是否重试或补偿。状态转换前先校验当前状态,避免非法跳转。

3.5 状态机与事件溯源

在支付场景中,状态机与事件溯源(Event Sourcing)结合可以显著提升可追溯性与审计能力。每次状态转换都生成一条领域事件,事件存储作为订单状态的唯一真相源。以下为事件与状态的对应关系:

领域事件 源状态 目标状态 事件载荷
OrderCreated — 待支付 订单ID、金额、商品列表、过期时间
PaymentStarted 待支付 支付中 支付记录ID、支付方式、金额
PaymentSucceeded 支付中 已支付 支付记录ID、渠道流水号、支付时间
PaymentFailed 支付中 待支付 支付记录ID、失败原因、可重试次数
RefundRequested 已支付 退款中 退款记录ID、退款金额、原因
RefundSucceeded 退款中 已退款 退款记录ID、渠道退款流水号
OrderCompleted 已支付/已退款 已完成 完成时间、履约结果
OrderClosed 待支付/支付中 已关闭 关闭原因(超时/用户取消)
事件溯源注意事项:事件结构一旦发布应避免修改;如需扩展字段,使用新版本事件并保留兼容性。事件存储建议使用独立数据库或消息队列,避免与业务数据库耦合。

3.6 本章小结

  • 聚合根设计强调边界明确与内部一致性,支付订单聚合根包含订单主实体、支付记录与退款记录。
  • 状态机定义了8个核心状态与8条合法转换路径,确保状态迁移的确定性与可追溯性。
  • 生命周期分为创建、支付、履约、退款、完成/关闭、归档六个阶段,每个阶段对应明确的领域行为与事件。
  • 代码骨架展示了聚合根方法如何封装状态机逻辑,外部只能通过聚合根方法改变状态。
  • 事件溯源可作为状态机的增强方案,提供完整的审计链路与事件重放能力。