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 支付订单的生命周期
支付订单从创建到最终完成(成功或关闭)经历多个阶段。下图展示了完整的生命周期流转路径。
生命周期关键点: 支付订单一旦进入“支付成功”状态,金额即被锁定,不可再次支付。退款操作会创建独立的退款记录(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 状态机可视化
下图以状态机图的形式,清晰展示了所有允许的状态转移路径。
状态机设计原则: 状态转移必须是原子操作,且每次转移都伴随领域事件的发布。在 Ent 框架中,建议使用
hooks 或 interceptors 来确保状态变更与事件持久化在同一个数据库事务中完成。