3. 支付领域模型设计:账户、交易、订单、流水等核心实体的 Ent Schema 设计

支付中间件的核心在于对资金、行为与状态的精确建模。本章将基于 Ent 框架,定义支付领域中最关键的四个实体:账户 (Account)、交易 (Transaction)、订单 (Order) 与 流水 (Ledger)。我们将从领域概念出发,逐步推导出符合 DDD 思想的 Ent Schema,并确保模型具备高内聚、低耦合与可追溯性。

设计原则: 支付模型必须遵循“资金守恒”与“状态机驱动”两大铁律。所有实体的变更最终都要通过流水(Ledger)进行审计,确保每一分钱都有迹可循。

3.1 核心实体关系总览

在深入 Schema 之前,先通过一张架构图理解四个实体之间的协作关系。订单是业务入口,交易是履约过程,账户是资金容器,流水是变更记录。

支付核心实体关系图 支付核心实体关系图 Order 业务发起方 Transaction 履约执行方 Account 资金容器 Ledger 审计底稿 创建 操作 记录 核心约束:一笔 Transaction 必须对应至少两条 Ledger 记录(借/贷),保证资金守恒。

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"),
    }
}
复式记账流水示例 复式记账流水示例(支付 100 元) Transaction: TX001 借方流水 (Debit) 账户: 收款方 | 金额: +100 元 贷方流水 (Credit) 账户: 付款方 | 金额: -100 元 校验规则:同一笔交易下,所有借方流水金额之和 = 所有贷方流水金额之和(绝对值)。

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 的联合索引,支撑对账查询。
  • 订单与交易的状态机变更必须通过领域事件驱动,保证最终一致性。