搜 索

如何设计支付账户体系

  • 78阅读
  • 2026年01月03日
  • 0评论
首页 / 支付相关 / 正文

写在前面:为什么账户系统让我掉了不少头发

作为一个在支付行业摸爬滚打多年的架构师,我可以负责任地说:账户系统是整个支付体系里最容易让人秃头的模块。没有之一。

为什么?因为它涉及到钱。而钱这个东西,多一分少一分都是事故。记得我刚入行的时候,前辈语重心长地跟我说:

小伙子,做支付系统,差一分钱找三天是常态,差一块钱能让你过不好整个春节。

所以今天,我想用最通俗的方式,把账户系统的设计思路分享给大家。争取让各位少踩坑,多长发。


一、先搞清楚:你的系统里到底有哪些账户?

在开始写代码之前,我们得先理清一个问题:支付系统里到底有多少种账户?说实话,刚开始做的时候我也是一脸懵——不就是用户账户和商户账户吗?

图样图森破。让我给你画张图:

graph TB subgraph internal ["内部账户-我们自己的钱袋子"] A1["备付金账户"] A2["手续费收入账户"] A3["待清算账户"] A4["在途资金账户"] A5["差错账户"] A6["渠道成本账户"] end subgraph external ["外部账户-别人的钱袋子"] B1["商户账户"] B2["用户账户"] B3["渠道账户"] B4["代理商账户"] end A1 -.-> B2 B2 --> B1 B1 --> A4

1.1 内部账户:公司的钱袋子们

内部账户是我们支付公司自己管理的账户,主要用来记录公司的资产、负债和收入。你可以把它想象成公司财务部的账本。

账户人话翻译干啥用的
备付金账户保险柜客户充的钱都存这,现在统一交给央行管了,安全得很
手续费收入工资卡每笔交易收的手续费都进这里,公司就靠它发工资了
待清算账户中转站交易完成但还没结算给商户的钱,先在这等着
在途资金快递中提现申请已经提了,钱在去银行的路上
差错账户待处理箱对账对不上的钱先扔这,等财务小姐姐来处理
渠道成本过路费给银行、网联等渠道的手续费支出

1.2 外部账户:别人的钱袋子

外部账户代表的是外部用户在我们系统里的资金。虽然钱在我们系统里记着,但本质上是人家的钱,我们只是帮忙管着。

这里有个很重要的概念:外部账户本质上是我们对外部用户的「负债」。用户存了100块,我们就欠他100块,这个要记清楚。


二、账户层级:像俄罗斯套娃一样设计

账户不能一锅粥,得分层管理。我喜欢把它比作公司组织架构——有集团、有部门、有小组、有个人。

graph TD subgraph L1 ["一级账户-集团老大"] G1["资产总账"] G2["负债总账"] G3["收入总账"] end subgraph L2 ["二级账户-部门经理"] D1["备付金"] D2["应收款"] D3["商户负债"] D4["用户负债"] end subgraph L3 ["三级账户-小组长"] T1["工行备付金"] T2["建行备付金"] T3["待结算户"] end subgraph L4 ["四级账户-具体干活的"] S1["商户A待结算"] S2["商户B待结算"] end G1 --> D1 G1 --> D2 G2 --> D3 G2 --> D4 D1 --> T1 D1 --> T2 D3 --> T3 T3 --> S1 T3 --> S2

2.1 每一级账户都有自己的使命

层级类比主要职责示例
一级集团总部对应会计科目,给监管看的资产总账、负债总账、收入总账
二级事业部按业务类型分类,方便统计备付金、应收款、待清算
三级部门具体业务场景,日常记账用工行备付金、网联通道
四级员工最小粒度,对应具体用户/商户张三钱包、李四店铺结算户

为什么要分这么多级?一句话:方便汇总和下钻


三、领域模型设计

下面是账户系统的核心领域模型:

classDiagram class AccountTitle { -String title_code -String parent_title_code -String title_type -String currency -String balance_direction } class Account { -String account_no -String title_code -String balance_direction } class InnerAccount { } class OuterAccount { -String member_id -String account_type } class Product { -String product_code -String revenue_account_no } class FundsChannel { -String channel_code -String channel_type -String pending_account_no -String cash_account_no -String fee_account_no -String vat_account_no } class Member { -String member_id -String member_name } class AccountingEntry { -String entry_id -String transaction_id -String account_no -Integer direction -Decimal amount -Decimal balance_before -Decimal balance_after } Account <|-- InnerAccount Account <|-- OuterAccount AccountTitle "1" --> "0..*" AccountTitle : parent AccountTitle "1" -- "*" Account InnerAccount "0..1" -- "0..*" Product InnerAccount "*" -- "0..1" FundsChannel OuterAccount "*" -- "1" Member Account "1" -- "*" AccountingEntry

3.1 模型说明

实体说明
AccountTitle会计科目,定义账户的模板和分类规则
Account账户基类,包含账号、科目、余额方向等通用属性
InnerAccount内部账户,系统自有账户
OuterAccount外部账户,关联会员,区分账户类型
FundsChannel资金渠道,包含待清算、现金、手续费、增值税四个账户
Product产品,关联收入账户用于分润
AccountingEntry会计分录,记录每一笔账务变动

3.2 上代码:账户数据模型(Go版)

// account.go - 账户核心模型

package account

import (
    "time"
    "github.com/shopspring/decimal"
)

// AccountTitle 会计科目
type AccountTitle struct {
    TitleCode        string `gorm:"primaryKey;size:32"`
    ParentTitleCode  string `gorm:"size:32;index"`
    TitleName        string `gorm:"size:128"`
    TitleType        string `gorm:"size:16"`          // ASSET/LIABILITY/INCOME/EXPENSE
    Currency         string `gorm:"size:3;default:CNY"`
    BalanceDirection int    `gorm:"default:1"`        // 1:借方 2:贷方
}

// Account 账户基类
type Account struct {
    AccountNo        string          `gorm:"primaryKey;size:32"`
    AccountName      string          `gorm:"size:128"`
    TitleCode        string          `gorm:"size:32;index"`
    BalanceDirection int             `gorm:"default:1"`
    Balance          decimal.Decimal `gorm:"type:decimal(20,2)"`
    FrozenAmount     decimal.Decimal `gorm:"type:decimal(20,2)"`
    Status           int             `gorm:"default:1"`
    CreatedAt        time.Time
    UpdatedAt        time.Time
}

// InnerAccount 内部账户
type InnerAccount struct {
    Account
    AccountCategory string `gorm:"size:32"` // RESERVE/FEE_INCOME/PENDING...
}

// OuterAccount 外部账户
type OuterAccount struct {
    Account
    MemberID    string `gorm:"size:32;index"`
    AccountType string `gorm:"size:16"` // SETTLEMENT/DEPOSIT/WALLET
}

// FundsChannel 资金渠道
type FundsChannel struct {
    ChannelCode      string `gorm:"primaryKey;size:32"`
    ChannelName      string `gorm:"size:64"`
    ChannelType      string `gorm:"size:16"`          // BANK/NUCC/CUPS
    PendingAccountNo string `gorm:"size:32"`          // 待清算账户
    CashAccountNo    string `gorm:"size:32"`          // 现金账户
    FeeAccountNo     string `gorm:"size:32"`          // 手续费账户
    VatAccountNo     string `gorm:"size:32"`          // 增值税账户
    Status           int    `gorm:"default:1"`
}

// Product 产品
type Product struct {
    ProductCode      string `gorm:"primaryKey;size:32"`
    ProductName      string `gorm:"size:64"`
    RevenueAccountNo string `gorm:"size:32"`          // 收入账户
}

四、记账:复式记账法其实没那么玄乎

说到记账,很多程序员就头大。别怕,核心就一句话:

有借必有贷,借贷必相等

翻译成人话:钱不会凭空出现也不会凭空消失,从一个口袋拿出来,必然要装进另一个口袋。

graph LR subgraph tx ["用户充值100元"] U["用户银行卡"] R["备付金账户"] W["用户钱包"] end U -->|扣款100| R R -->|记录欠款100| W subgraph entry ["会计分录"] D["借: 备付金 +100"] C["贷: 用户账户 +100"] end D --- C

4.1 借贷方向速记表

账户类型借方 Debit贷方 Credit
资产类 我有的增加 钱进来了减少 钱出去了
负债类 我欠的减少 还钱了增加 又欠了
收入类 我赚的冲销 退钱了增加 又赚了
成本类 我花的增加 花钱了冲销 省回来了

记住一个口诀:资产借增贷减,负债贷增借减

4.2 记账核心代码(Go版)

// ledger.go - 记账引擎核心

package ledger

import (
    "context"
    "fmt"
    "time"
    
    "github.com/shopspring/decimal"
    "gorm.io/gorm"
)

const (
    Debit  = 1 // 借
    Credit = 2 // 贷
)

// AccountingEntry 会计分录
type AccountingEntry struct {
    EntryID        string          `gorm:"primaryKey;size:64"`
    TxnID          string          `gorm:"size:64;index"`
    AccountNo      string          `gorm:"size:32;index"`
    Direction      int             `gorm:"index"`
    Amount         decimal.Decimal `gorm:"type:decimal(20,2)"`
    BalanceBefore  decimal.Decimal `gorm:"type:decimal(20,2)"`
    BalanceAfter   decimal.Decimal `gorm:"type:decimal(20,2)"`
    Summary        string          `gorm:"size:128"`
    AccountingDate string          `gorm:"size:10;index"`
    CreatedAt      time.Time
}

// LedgerService 记账服务
type LedgerService struct {
    db *gorm.DB
}

// Post 核心记账方法
func (s *LedgerService) Post(ctx context.Context, req *PostingRequest) error {
    // 1. 检查借贷是否平衡
    if err := s.checkBalance(req.Entries); err != nil {
        return fmt.Errorf("借贷不平衡: %w", err)
    }
    
    // 2. 幂等检查
    if exists := s.checkExists(ctx, req.BizOrderNo); exists {
        return nil
    }
    
    // 3. 开事务执行
    return s.db.Transaction(func(tx *gorm.DB) error {
        for _, entry := range req.Entries {
            if err := s.processEntry(tx, entry); err != nil {
                return err
            }
        }
        return nil
    })
}

// checkBalance 校验借贷平衡
func (s *LedgerService) checkBalance(entries []EntryRequest) error {
    var debit, credit decimal.Decimal
    for _, e := range entries {
        if e.Direction == Debit {
            debit = debit.Add(e.Amount)
        } else {
            credit = credit.Add(e.Amount)
        }
    }
    if !debit.Equal(credit) {
        return fmt.Errorf("借方%s != 贷方%s", debit, credit)
    }
    return nil
}

五、常见支付场景怎么记账?

5.1 场景一:用户充值100块

sequenceDiagram participant U as 用户 participant S as 支付系统 participant B as 银行 U->>S: 充值100元 S->>B: 请求扣款 B-->>S: 扣款成功 Note over S: 开始记账 S->>S: 借 备付金 +100 S->>S: 贷 用户账户 +100 S->>S: 借 渠道成本 +1 S->>S: 贷 备付金 -1 Note over S: 记账完成 S-->>U: 充值成功

会计分录:

账户借方贷方说人话
备付金账户100.00 钱到账了
用户钱包 100.00我们欠用户100
渠道成本1.00 银行手续费
备付金账户 1.00手续费从备付金出

5.2 场景二:用户消费100块

graph LR A["用户钱包 -100"] --> B["商户账户 +99.4"] A --> C["手续费收入 +0.6"]
账户借方贷方说明
用户账户100.00 用户花钱了
商户账户 99.40商户收款
手续费收入 0.60我们赚了

5.3 场景三:商户提现1000块

sequenceDiagram participant M as 商户 participant S as 支付系统 participant B as 银行 Note over S: 阶段1 发起提现 M->>S: 提现1000元 S->>S: 借 商户账户 1002 S->>S: 贷 在途资金 1000 S->>S: 贷 手续费收入 2 S->>B: 发起打款 Note over S: 阶段2 到账确认 B-->>S: 打款成功 S->>S: 借 在途资金 1000 S->>S: 贷 备付金 1000 S-->>M: 提现成功

六、日终对账:每天都要做的体检

graph TD A["开始日终对账"] --> B{"检查借贷平衡"} B -->|平衡| C["渠道对账"] B -->|不平衡| E["生成差错记录"] C --> D{"核对流水"} D -->|一致| F["账户余额核验"] D -->|长款| E D -->|短款| E F --> G{"余额校验"} G -->|正常| H["生成日终报表"] G -->|异常| E E --> I["等待人工处理"] H --> J["日终完成"]

6.1 差错类型

类型人话解释怎么处理
长款银行说收了,我们没记补记账,挂待清算
短款我们记了,银行没收到挂差错账户,核实后处理
金额不符都有记录,金额对不上调差额
借贷不平借贷加起来不等大事!马上查bug

七、系统架构总览

graph TB subgraph biz ["业务层"] A["充值"] B["支付"] C["提现"] D["退款"] end subgraph engine ["记账引擎"] E["记账入口"] F{"借贷平衡检查"} G["事务执行"] H["拒绝记账"] I["更新余额"] J["写入分录"] end subgraph recon ["对账系统"] K["日终调度"] L["借贷核验"] M["渠道对账"] N["余额校验"] O["生成报表"] end A --> E B --> E C --> E D --> E E --> F F -->|平衡| G F -->|不平衡| H G --> I G --> J K --> L K --> M K --> N L --> O M --> O N --> O

八、血泪教训

  1. 金额一定用Decimal0.1 + 0.2 != 0.3 这个坑你不会想踩。
  2. 记账必须在事务里。要么全成功,要么全失败。
  3. 幂等性是刚需。同一笔单重复请求不能重复入账。
  4. 每笔账都要可追溯。记账前余额、记账后余额全都要记。
  5. 日终对账是救命的。再好的系统也会出bug。

问题

如何支持多币种?
如何支持非金额记账?
如何解决记账性能问题?

参考资料

Accounting for Developers
会计学原理
记账系统的go语言实现

评论区
暂无评论
avatar