Appearance
账务系统设计:复式记账与日切
信贷域的账务系统是整个公司的"资金事实源":业务系统都可以错,账务不能错。这篇讲四个支柱:复式记账的不变量、科目与账户体系、日切与四层核对、计息规则。业务全景见信贷业务全景与领域模型。
复式记账:借贷必相等
单式记账(给每个账户存一个余额字段加减)最大的问题是不自证。余额错了没有任何内部手段发现。复式记账的答案是:任何一笔资金变动必须同时记两条(或以上)分录。借方合计恒等于贷方合计——这是数学不变量,不是业务约定。
一笔放款 10000 元(我方是贷款人,放出去的钱是资产):
借:贷款本金—张三(资产类) 10000 资产增加记借方
贷:银行存款—备付金户(资产类) 10000 资产减少记贷方
一笔还款 10300 元(本金 10000 + 利息 300):
借:银行存款—备付金户 10300
贷:贷款本金—张三 10000 资产(债权)减少
贷:利息收入—个人贷款利息 300 收入类增加记贷方记账三要素与模型:
- 分录(entry):最小记账单位,含科目方向、金额、借贷标志;一条业务流水产生一组分录(voucher/凭证);
- 账户(account):分录的挂靠点,余额 = 该账户借方累计 − 贷方累计(资产类)或反向(负债/收入类);
- 不变量落地:凭证落库时同事务校验
SUM(借) == SUM(贷),不相等直接拒绝——账务系统的写入路径上唯一的全局断言。
account(账户) 1 ──< entry(分录) >── 1 voucher(凭证) ──▶ 1 业务单据号(支付/借据/还款)
│ │
▼ ▼
余额表(冗余,可重算) 流水表(唯一事实,只插入不更新)两条工程铁律:流水只插入——记错不改流水,用红字冲销分录对冲,审计上叫"留痕"。余额是流水的推导值:允许冗余存储加速,但日终用流水重算校验,冗余错了以流水为准。
落库的事务边界:一组分录 + 凭证必须在同一个本地事务里提交,且借贷校验与插入合一:
sql
-- 同一事务: 先插分录, 再校验本凭证借贷平衡, 不平衡整体回滚
INSERT INTO acc_entry(voucher_id, account_id, dc_flag, amount, biz_no, ...) VALUES (...);
-- dc_flag: D=借 / C=贷
SELECT CASE WHEN SUM(CASE dc_flag WHEN 'D' THEN amount ELSE -amount END) = 0
THEN 1 ELSE 0 END -- 逐凭证断言, 非 0 即回滚
FROM acc_entry WHERE voucher_id = ?;凭证级断言挡住"代码少写一腿"。全局试算平衡(日切项)再挡"事务外的旁路写入"。两级校验之间没有任何空隙。
会计科目与账户体系
科目是账务的目录树,账户是科目挂到具体对象(人/机构/渠道)后的实例:
1000 资产 2000 负债 5000 损益
├─1101 贷款(资产) ├─2101 应付—资方款 ├─5101 利息收入
│ └─1101·01 张三户 ├─2102 备付金—XX银行 ├─5102 罚息收入
├─1102 应收还款(在途) └─2103 暂收挂账(差错池) ├─6101 资金成本(利息支出)
└─1103 银行存款/渠道往来户 └─6102 坏账损失/核销- 客户账户:一张借据一组(本金户/利息户/罚息户),余额即欠款构成。"欠多少钱"账上有独立答案,不依赖借据表;
- 内部户:清算户、备付金户、手续费户、暂收挂账户(对账差错的落脚点,见幂等与对账-资金安全底座);
- 科目总账 vs 分户账:分户账按账户记,总账按科目汇总记。总分之间必须对得上(日切核对项之一)。层次上"业务系统的钱"是视图,"分户账的钱"是账,"总账的钱"是报表——三层各答一个问题。
放款/还款全周期在账户上的投影(一张借据从生到死,账务侧长这样):
| 时点 | 本金户(资产) | 应收利息户(资产) | 利息收入(损益) | 银行存款/渠道往来(资产) |
|---|---|---|---|---|
| 放款 10000 | +10000 | — | — | −10000 |
| 每日计提 10 元 × 30 天 | — | +300 | +300 | — |
| 还款 10300 | −10000 | −300 | — | +10300 |
| 结清后合计 | 0 | 0 | +300 | +300 |
结清后资产户全部归零、损益留下 300——"账平"的直观形态。若任何一格对不上,四层核对中总有一层会报警。
日切与四层核对
日切(EOD,End Of Day)是把连续时间切成会计日的边界动作。切日 → 计提入账 → 试算平衡 → 四层核对 → 冻结快照出报表。
T日 23:55 停止受理(或软切,交易带日期戳) → 切日批
1) 计息/计提批: 逐借据算当日利息罚息 → 生成计提凭证
2) 试算平衡: 全表 SUM(借方) == SUM(贷方) ──不平 → 拦截日终,定位漏记/半记
3) 账账核对: 总账科目余额 vs 分户账汇总 ──总分明细一致性
4) 账证核对: 凭证分录 vs 业务单据(逐笔) ──每笔钱有业务出处
5) 账实核对: 账务余额 vs 渠道/银行对账单 ──账上的钱真实存在
6) 快照冻结 → 生成日终余额表/监管报表四层核对各抓一种坏,缺一不可:
| 核对 | 抓什么错 | 典型根因 |
|---|---|---|
| 试算平衡 | 单边分录/半记 | 记账代码漏了一腿、事务部分提交 |
| 账账(总分) | 明细与汇总脱节 | 分户账落了、总账汇总批漏跑 |
| 账证 | 记了没有业务依据的钱 | 重复回调、重复记账、伪造请求 |
| 账实 | 账实不符 | 渠道掉单、银行在途、差错未挂账 |
切日窗口:现代做法是软切——交易不停,每笔交易携带会计日期戳(切日前受理的算 T 日)。日终批按日期戳扫数。硬停交易的窗口只保留给快照冻结的瞬间。日切失败的纪律:试算不平禁止出报表、禁止开次日。宁可 delaying 开门,不可带病切日——账务的 unavailable 是事故,账务的 wrong 是灾难。
计息:按日计提、算头不算尾、罚息复利
利息是账务系统里唯一"每天自动长出来"的数字,规则细节全是资金敏感点:
- 按日计提(accrual):每天日终为每张借据计提当日利息,
当日利息 = 剩余本金 × 日利率。计提是权责发生制:收入按天确认,不是收到钱才算。所以借据的"应还利息"由计提凭证累加而来,还款只是冲抵已计提数; - 日利率口径:
年化利率 / 360(金融惯例按 360 天计息 365 天计年)。利率用DECIMAL存,计算全链路BigDecimal,严禁 float/double。0.1 在二进制浮点里是循环小数,一天的利息误差乘以亿级借据就是真钱; - 算头不算尾:放款日起息、结清日不计息(还款日当天占用结束了,最后一天利息不收)。反向的"算尾不算头"也存在,取决于合同口径。口径必须写进合同、写进代码、写进对账预期,三处一处不一致就是客诉。天数差一天 × 全量客户,就是监管处罚的金额来源;
- 罚息:逾期后对未还本金按罚息利率(通常为合同利率上浮 30%–50%)计收。且监管有利率上限约束(司法保护上限/LPR 倍数)。上限校验必须做在计息引擎里而不是营销页上;
- 复利:俗称"利滚利",只有罚息部分再计息受严格限制,多数消费贷合同对正常利息不计复利。计息引擎把"计息基数"参数化(本金 / 本金+应收利息),不同产品不同配置。
算一笔带数字的账(年化 7.2%,日利率 = 7.2%/360 = 0.02%):6 月 1 日放款 10000 元,6 月 11 日结清。算头不算尾,计息天数 10 天:
正常利息 = 10000 × 0.0002 × 10 = 20 元 # 结清日(6/11)不计息
若 6/11 未还、逾期至 6/21 结清(罚息上浮 50%):
逾期天数 10 天(6/11~6/21, 同样算头不算尾)
罚息 = 10000 × 0.0002 × 1.5 × 10 = 30 元 # 基数是未还本金
合计 = 本金 10000 + 正常利息 20 + 罚息 30计息伪码(日终批,逐借据):
base = 合同约定计息基数(剩余本金 / 本金+欠息)
rate = status==OVERDUE ? penaltyRate : normalRate
interest = round(base * rate, 2) # BigDecimal, HALF_UP
计提分录: 借 应收利息(资产) / 贷 利息收入(损益)
# 逾期日切换罚息利率的那一天,rate 的选择要按"当日 00:00 的状态"取业界实践参照(样本银行A vs 样本银行B)
样本银行A是中国首家核心系统完全构建在金融云上的银行,即"云上银行",无线下网点[官方/媒体]。其账务底座为本文的复式记账与日切提供一个分布式参照:
| 本文概念 | 样本银行A对应实践 | 事实要点 | 可信度 |
|---|---|---|---|
| 账务库与分录事务 | OceanBase 分布式底座 | 一代核心承袭SOFAStack / OceanBase / XTS 分布式事务;三地五中心下每分片 5 副本跨三城、3 副 Paxos 强一致,RPO=0、RTO<1min | [官方] |
| 日终批(计提/试算/四层核对) | 数据平台离线批 | 数据平台(DaaS)日离线计算 200T+、毫秒级实时风控输出——日切各批就是在该量级平台上扫全量借据 | [官方] |
| 独立"总账系统" | 未见对应物 | 公开资料未见独立总账系统:分布式记账直接落在多副本 DB 上,总分核对更像同一数据平台上的批量校验作业 | 架构解读 |
三条映射要点:
- 分布式不改数学不变量:凭证级
SUM(借)==SUM(贷)断言仍须在写入路径成立,分录+凭证同库同事务。在云上由 Paxos 强一致副本(3 副法定多数)兜底,而不是"最终一致的账"; - "日离线 200T+"给日切规模感。本文"日终批逐借据计息 + 全表试算"在亿级借据下就是 T 级数据扫描。批处理平台(而非联机库)才是日切的主战场;
- 总账/分户账的两层系统在云上核心被压扁:没有独立总账系统。这使"总分核对"退化为数据平台上的校验作业。但四层核对的语义(试算/账账/账证/账实)一项不少——系统形态可以合并,核对语义不可合并。
样本银行B: 样本银行A承袭A系技术栈(SOFAStack/OceanBase)。样本银行B则自研分布式核心 Openhive"开放蜂巢" [官方/媒体]。账务底座走的是 TDSQL 单元化 + TiDB 卸载批量的另一条路线:
| 本文概念 | 样本银行B对应实践 | 事实要点 | 可信度 |
|---|---|---|---|
| 账务库与分录事务 | DCN 单元化 + TDSQL | 核心交易落腾讯 TDSQL,800+ 节点承载全行 OLTP;DCN 按客户号分片、DCN 级双活——同一客户的分录落同一 DCN,凭证级借贷校验仍是同库本地事务;单账户年 IT 成本约 2 元(约同业 1/10)、单日峰值 14 亿笔 | [媒体]/[技术社区] |
| 日终批(计提/试算/核对) | TiDB 承载核心批量 | 2020-24:多 DCN 的 TDSQL 数据近实时同步汇总至 TiDB,联机与批量解耦——微粒贷日终批量 3 小时压到目标半小时 | [技术社区] |
两条映射要点:
- 单元化不改数学不变量:客户号分片先把事务边界收敛进分片。分片键选择本身就是账务事务边界的架构决策。分布式的前提是凭证级
SUM(借)==SUM(贷)断言仍在分片内本地成立,而不是退到跨分片分布式事务; - 日切卸载有两个方向。样本银行A把日终批搬上数据平台做离线批。样本银行B把批量从联机库卸载到 TiDB。方向不同,共同点是日终批不再与联机交易抢同一套资源。本文"软切"解决的是切日窗口,"联机批量解耦"解决的是批量本身占库。两者正交可叠加。
追问链
Q1:为什么余额表不能当唯一事实,非要保留全量流水? 余额是"现在的状态",流水是"怎么到这里的"。对账差错要回放历史(某笔多记/漏记改了哪些天的余额)。监管要按日回溯报表。审计要逐笔穿行测试。这三类场景都要求从流水能重算任意历史时点余额。
余额表只是流水的缓存,缓存可以重建,流水一个字节都不能丢。
Q2:日切和在线交易并发,利息算谁的? 按会计日期戳归属:切日点之前受理成功、资金当日到账的,属于 T 日并参与 T 日计提。跨点的进在途科目(应收还款户),次日确认后计提。所以账证核对里"在途科目余额 > 0 超过 N 天"是巡检项。长期挂账的在途就是掉单信号。
Q3:记错账了怎么办,UPDATE 那条分录? 永不 UPDATE。错误用红字冲销:原样记一组金额为负(红字)的分录把错账冲平,再记正确的蓝字分录。流水表上看得到完整的"错—冲—正"三段,审计可还原。允许 UPDATE 流水的账务系统等于没有审计。
Q4:为什么账务不做强一致分布式事务,而靠"先记账后事件+对账"? 账务是唯一事实源,要求每笔写入本地事务强一致(分录+凭证同库同事务)。对下游(借据/额度/报表)只发事件、最终一致。给整条链路包强一致事务(如 TCC/XA)会把账务 DB 的锁延伸到所有业务方,吞吐与可用性都不可接受。取舍分析见刚性事务与柔性事务与分布式事务解决方案(总纲)。
小结
- 复式记账把"账平不平"从人工检查变成数学不变量,写入路径上强制 SUM(借)==SUM(贷);
- 流水是唯一事实(只插不改,错账红冲),余额是可重算的缓存;
- 日切四层核对:试算平衡抓单边、账账抓总分脱节、账证抓无据记账、账实抓账实不符。试算不平禁止开次日;
- 计息三规则:按日计提(权责发生)、算头不算尾(口径三处一致)、罚息有上限、复利基数参数化。金额计算 BigDecimal 全链路。
2026-10 新增(金融信贷域系列)。