Skip to content

账务系统设计:复式记账与日切 ​

信贷域的账务系统是整个公司的"资金事实源":业务系统都可以错,账务不能错。这篇讲四个支柱:复式记账的不变量、科目与账户体系、日切与四层核对、计息规则。业务全景见信贷业务全景与领域模型。

复式记账:借贷必相等 ​

单式记账(给每个账户存一个余额字段加减)最大的问题是不自证。余额错了没有任何内部手段发现。复式记账的答案是:任何一笔资金变动必须同时记两条(或以上)分录。借方合计恒等于贷方合计——这是数学不变量,不是业务约定。

一笔放款 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
结清后合计00+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 新增(金融信贷域系列)。