3. 支付领域模型设计:账户、交易、订单、流水等核心实体的 Ent Schema 设计
支付中间件的核心在于对资金、行为与状态的精确建模。本章将基于 Ent 框架,定义支付领域中最关键的四个实体:账户 (Account)、交易 (Transaction)、订单 (Order) 与 流水 (Ledger)。我们将从领域概念出发,逐步推导出符合 DDD 思想的 Ent Schema,并确保模型具备高内聚、低耦合与可追溯性。
设计原则: 支付模型必须遵循“资金守恒”与“状态机驱动”两大铁律。所有实体的变更最终都要通过流水(Ledger)进行审计,确保每一分钱都有迹可循。
3.1 核心实体关系总览
在深入 Schema 之前,先通过一张架构图理解四个实体之间的协作关系。订单是业务入口,交易是履约过程,账户是资金容器,流水是变更记录。
3.2 账户 (Account) Schema
账户是资金的载体,分为用户账户、商户账户、平台账户与冻结账户。设计时需考虑余额精度、账户状态与版本号(乐观锁)。
// ent/schema/account.go
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
)
type Account struct {
ent.Schema
}
func (Account) Fields() []ent.Field {
return []ent.Field{
field.String("account_no").Unique().Comment("账户编号,全局唯一"),
field.Int64("balance").Default(0).Comment("可用余额,单位:分"),
field.Int64("frozen_balance").Default(0).Comment("冻结余额,单位:分"),
field.Int32("status").Default(1).Comment("状态:1-正常,2-冻结,3-注销"),
field.Int32("version").Default(0).Comment("乐观锁版本号"),
field.Time("created_at"),
field.Time("updated_at"),
}
}
func (Account) Indexes() []ent.Index {
return []ent.Index{
index.Fields("account_no"),
}
}
关键设计点:
- 余额单位:统一使用“分”存储,避免浮点数精度问题。
- 乐观锁:通过
version字段实现 CAS(Compare And Swap),防止并发扣款导致超扣。 - 冻结余额:与可用余额分离,支持预授权等场景。
3.3 交易 (Transaction) Schema
交易是支付履约的核心实体,记录一次完整的资金转移过程。它包含交易类型、金额、状态机以及关联的账户信息。
// ent/schema/transaction.go
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
type Transaction struct {
ent.Schema
}
func (Transaction) Fields() []ent.Field {
return []ent.Field{
field.String("tx_id").Unique().Comment("交易流水号,全局唯一"),
field.String("order_id").Comment("关联订单号"),
field.String("payer_account_no").Comment("付款方账户编号"),
field.String("payee_account_no").Comment("收款方账户编号"),
field.Int64("amount").Comment("交易金额,单位:分"),
field.Int32("type").Comment("交易类型:1-支付,2-退款,3-转账"),
field.Int32("status").Default(0).Comment("状态:0-待处理,1-成功,2-失败,3-部分成功"),
field.Time("created_at"),
field.Time("updated_at"),
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
| tx_id | string | 全局唯一交易流水号,通常由雪花算法生成 |
| order_id | string | 关联业务订单,支持一对多(如分账) |
| amount | int64 | 交易金额,单位分,不允许为负 |
| status | int32 | 状态机驱动,仅允许单向流转 |
3.4 订单 (Order) Schema
订单是业务层面的聚合根,它聚合了商品信息、支付结果与退款记录。订单与交易是 1:N 的关系,一笔订单可以对应多次交易(如支付+退款)。
// ent/schema/order.go
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
type Order struct {
ent.Schema
}
func (Order) Fields() []ent.Field {
return []ent.Field{
field.String("order_id").Unique().Comment("业务订单号"),
field.String("merchant_id").Comment("商户ID"),
field.Int64("total_amount").Comment("订单总金额,单位:分"),
field.Int64("paid_amount").Default(0).Comment("已支付金额"),
field.Int64("refund_amount").Default(0).Comment("已退款金额"),
field.Int32("status").Default(0).Comment("状态:0-待支付,1-已支付,2-已退款,3-部分退款"),
field.Time("expire_time").Optional().Comment("订单过期时间"),
field.Time("created_at"),
field.Time("updated_at"),
}
}
注意: 订单的
paid_amount 与 refund_amount 是冗余字段,用于快速查询。其准确性由交易与流水保证,建议通过定时任务或事件驱动进行对账校正。
3.5 流水 (Ledger) Schema
流水是支付系统的审计底稿,记录每一笔资金变动的明细。它采用复式记账法,每笔交易至少产生两条流水:一条借方、一条贷方。
// ent/schema/ledger.go
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
type Ledger struct {
ent.Schema
}
func (Ledger) Fields() []ent.Field {
return []ent.Field{
field.String("ledger_id").Unique().Comment("流水ID"),
field.String("tx_id").Comment("关联交易ID"),
field.String("account_no").Comment("账户编号"),
field.Int64("amount").Comment("变动金额,单位:分"),
field.Int32("direction").Comment("方向:1-借方(增加),2-贷方(减少)"),
field.Int64("balance_before").Comment("变动前余额"),
field.Int64("balance_after").Comment("变动后余额"),
field.String("biz_code").Comment("业务码:PAYMENT, REFUND, FROZEN, UNFROZEN"),
field.Time("created_at"),
}
}
3.6 实体间关联与约束
在 Ent 中,我们通过 Edge 定义实体间的关联关系。以下是核心关联设计:
- Order → Transaction: 1:N 关系,通过
order_id外键关联。 - Transaction → Ledger: 1:N 关系,通过
tx_id外键关联。 - Account → Ledger: 1:N 关系,通过
account_no外键关联。
// 在 Transaction 中定义 Edge 示例
func (Transaction) Edges() []ent.Edge {
return []ent.Edge{
edge.From("order", Order.Type).
Ref("transactions").
Field("order_id").
Unique().
Required(),
edge.To("ledgers", Ledger.Type),
}
}
工程落地建议:
- 所有实体均使用
int64存储金额,避免浮点误差。 - 流水表必须建立
tx_id与account_no的联合索引,支撑对账查询。 - 订单与交易的状态机变更必须通过领域事件驱动,保证最终一致性。