2. 支付订单领域分析

支付订单是支付系统的核心聚合根,承载着从用户发起支付到资金最终结算的完整过程。本章从业务场景、生命周期和状态机三个维度,深入剖析支付订单的领域边界与行为规则。

2.1 支付订单的核心业务场景

支付订单并非孤立存在,它服务于多个业务场景。理解这些场景有助于我们界定聚合的职责边界。

场景编号 场景名称 触发条件 核心领域事件
S-01 用户主动支付 用户在收银台选择支付方式并确认 PaymentOrderCreated
S-02 自动扣款 订阅到期、免密支付协议触发 AutoDebitInitiated
S-03 支付超时关闭 订单创建后超过 TTL 未完成支付 PaymentOrderExpired
S-04 支付成功回调 第三方支付网关异步通知支付成功 PaymentSucceeded
S-05 支付失败重试 网关返回明确失败码或网络超时 PaymentFailed / PaymentRetried
S-06 部分退款 商户后台发起退款申请 RefundRequested
S-07 全额退款 订单取消或争议判责 FullRefundCompleted
领域边界提示: 支付订单聚合只负责支付本身的状态流转和金额校验,不包含商品信息、用户地址、物流状态等业务数据。这些数据应归属于订单上下文(Order Context)或履约上下文(Fulfillment Context)。

2.2 支付订单的生命周期

支付订单从创建到最终完成(成功或关闭)经历多个阶段。下图展示了完整的生命周期流转路径。

支付订单生命周期 支付订单生命周期流转图 待支付 (Pending) 支付中 (Processing) 支付成功 (Succeeded) 支付失败 (Failed) 已关闭 (Closed) 已退款 (Refunded) 用户确认支付 异步回调成功 网关返回失败 发起退款 超时未重试 TTL 超时 说明:实线为正向流转,虚线为超时关闭路径。支付成功后可多次部分退款,但总退款金额不得超过原订单金额。
生命周期关键点: 支付订单一旦进入“支付成功”状态,金额即被锁定,不可再次支付。退款操作会创建独立的退款记录(Refund Record),而非直接修改支付订单的金额字段。

2.3 支付订单的状态机设计

状态机是支付订单聚合的核心行为模型。它定义了每个状态下允许的事件、转移规则以及边界条件。以下是支付订单的完整状态机定义。

2.3.1 状态定义表

状态码 状态名称 描述 是否终态
PENDING 待支付 订单已创建,等待用户支付或自动扣款 否
PROCESSING 支付中 已向支付网关发起请求,等待异步结果 否
SUCCEEDED 支付成功 支付网关确认资金已成功扣划 是(可退款)
FAILED 支付失败 支付网关返回明确失败或超时 否(可重试)
CLOSED 已关闭 订单超时未支付或达到最大重试次数 是
REFUNDED 已退款 原支付订单已全额退款 是

2.3.2 状态转移规则

状态转移必须满足以下领域规则,任何违反规则的转移都应被聚合根拒绝。

  • 规则 1: 只有 PENDING 状态的订单可以发起支付,进入 PROCESSING。
  • 规则 2: PROCESSING 状态只能转移到 SUCCEEDED 或 FAILED,不允许直接关闭。
  • 规则 3: FAILED 状态允许重试,重试次数上限为 3 次,超过后自动进入 CLOSED。
  • 规则 4: SUCCEEDED 状态可发起退款,退款完成后进入 REFUNDED。
  • 规则 5: CLOSED 和 REFUNDED 为终态,不可再发生任何状态转移。
  • 规则 6: 任何状态转移都必须记录事件日志,用于对账和审计。

2.3.3 状态机代码骨架(Go + Ent)

以下代码展示了使用 Go 语言实现支付订单状态机的核心逻辑。实际项目中,状态机逻辑应封装在聚合根的方法内。

// PaymentOrder 支付订单聚合根
type PaymentOrder struct {
    ID          string
    Amount      decimal.Decimal
    Status      PaymentStatus
    RetryCount  int
    CreatedAt   time.Time
    UpdatedAt   time.Time
}

// PaymentStatus 支付状态枚举
type PaymentStatus string

const (
    StatusPending    PaymentStatus = "PENDING"
    StatusProcessing PaymentStatus = "PROCESSING"
    StatusSucceeded  PaymentStatus = "SUCCEEDED"
    StatusFailed     PaymentStatus = "FAILED"
    StatusClosed     PaymentStatus = "CLOSED"
    StatusRefunded   PaymentStatus = "REFUNDED"
)

// StartPayment 发起支付:PENDING -> PROCESSING
func (o *PaymentOrder) StartPayment() error {
    if o.Status != StatusPending {
        return fmt.Errorf("cannot start payment from status: %s", o.Status)
    }
    o.Status = StatusProcessing
    o.UpdatedAt = time.Now()
    // 记录领域事件
    o.AddEvent(PaymentStarted{OrderID: o.ID})
    return nil
}

// MarkSucceeded 支付成功:PROCESSING -> SUCCEEDED
func (o *PaymentOrder) MarkSucceeded() error {
    if o.Status != StatusProcessing {
        return fmt.Errorf("cannot mark succeeded from status: %s", o.Status)
    }
    o.Status = StatusSucceeded
    o.UpdatedAt = time.Now()
    o.AddEvent(PaymentSucceeded{OrderID: o.ID})
    return nil
}

// MarkFailed 支付失败:PROCESSING -> FAILED
func (o *PaymentOrder) MarkFailed() error {
    if o.Status != StatusProcessing {
        return fmt.Errorf("cannot mark failed from status: %s", o.Status)
    }
    o.Status = StatusFailed
    o.RetryCount++
    o.UpdatedAt = time.Now()
    o.AddEvent(PaymentFailed{OrderID: o.ID, RetryCount: o.RetryCount})
    return nil
}

// RetryPayment 重试支付:FAILED -> PROCESSING
func (o *PaymentOrder) RetryPayment() error {
    if o.Status != StatusFailed {
        return fmt.Errorf("cannot retry from status: %s", o.Status)
    }
    if o.RetryCount >= 3 {
        return fmt.Errorf("max retry count exceeded: %d", o.RetryCount)
    }
    o.Status = StatusProcessing
    o.UpdatedAt = time.Now()
    o.AddEvent(PaymentRetried{OrderID: o.ID, RetryCount: o.RetryCount})
    return nil
}

// Close 关闭订单:PENDING/FAILED -> CLOSED
func (o *PaymentOrder) Close() error {
    if o.Status != StatusPending && o.Status != StatusFailed {
        return fmt.Errorf("cannot close from status: %s", o.Status)
    }
    o.Status = StatusClosed
    o.UpdatedAt = time.Now()
    o.AddEvent(PaymentOrderClosed{OrderID: o.ID})
    return nil
}

// Refund 退款:SUCCEEDED -> REFUNDED
func (o *PaymentOrder) Refund() error {
    if o.Status != StatusSucceeded {
        return fmt.Errorf("cannot refund from status: %s", o.Status)
    }
    o.Status = StatusRefunded
    o.UpdatedAt = time.Now()
    o.AddEvent(PaymentRefunded{OrderID: o.ID})
    return nil
}
领域设计警告: 状态机逻辑必须放在聚合根内部,不允许外部服务直接修改状态字段。任何状态变更都必须通过聚合根的方法,以确保业务规则和事件记录的一致性。

2.3.4 状态机可视化

下图以状态机图的形式,清晰展示了所有允许的状态转移路径。

支付订单状态机 支付订单状态机(FSM) PENDING PROCESSING SUCCEEDED FAILED CLOSED REFUNDED StartPayment() MarkSucceeded() MarkFailed() RetryPayment() Close() TTL 超时 Close() Refund() 说明:实线为正向转移,虚线为重试和超时路径。终态为 CLOSED 和 REFUNDED,不可再转移。
状态机设计原则: 状态转移必须是原子操作,且每次转移都伴随领域事件的发布。在 Ent 框架中,建议使用 hooks 或 interceptors 来确保状态变更与事件持久化在同一个数据库事务中完成。