互联网系统的"记账"通常是一张流水表加一个余额字段,扣钱就是 UPDATE balance = balance - x。券商系统不敢这么干。客户保证金动辄百亿规模,监管要求每一分钱都能还原出"从哪个科目来、到哪个科目去",审计要能穿透到原始凭证。支撑这个要求的是一套从 15 世纪用到现在的技术:复式记账。这篇讲它在券商核心系统里具体怎么落地——不是会计课,是表结构和校验逻辑。
复式记账的唯一规则
规则一句话:任何一笔经济业务,至少在两个科目上登记,借方合计等于贷方合计。背后是一个恒等式:
$$ \text{资产} = \text{负债} + \text{所有者权益} $$
每次记账都只是在这个等式内部搬动数字,等式本身永不破坏。如果哪天的账记完发现等式不平了,不用查业务逻辑,一定是记账代码出了 bug——这就是复式记账最大的工程价值:它给账务系统提供了一个可以随时全量验证的不变量。单式流水没有这个性质,余额错了就是错了,没有任何内在校验能发现它。
三层结构:流水、分录、科目余额
券商系统里复式记账落成了三层数据,职责各不相同:
业务流水层(fund_journal / position_journal):交易驱动的原始记录。"客户 123456 的资金账户买入成交,扣款 50230.50 元"。这一层是面向业务的,字段是业务语义(委托号、成交号、业务类型),谁都能看懂,但只记单边——它是分录的原料,本身不满足复式。
会计分录层(voucher / entry):由流水按映射规则生成的借贷分录。上面那条买入流水会展开成类似这样的分录(科目名为示意):
借:客户资金存款——客户 123456 50,230.50
贷:清算备付金——中国结算 50,230.50
注意借贷方向的含义:客户资金存款是券商的负债科目(券商欠客户的钱),客户买入扣款是负债减少记借方;清算备付金是券商在中登的资产科目,清算时钱划给中登导致资产减少记贷方。一借一贷,金额相等,恒等式维持。
科目余额层(gl_balance):每个会计科目按日汇总借贷发生额,滚动出科目余额。这一层是财务报表和监管报表的直接数据源。
分录映射:规则表而不是 if-else
流水到分录的转换是账务核心最容易长成屎山的部分——业务类型几百种(普通买入、融资买入、红利入账、利息结息、佣金扣收、司法扣划……),每种对应一组借贷科目。硬编码 if-else 的下场是科目口径改了要发版。
工程上正确的做法是规则表驱动:分录模板表存(业务类型 × 币种 × 分支机构 → 借方科目、贷方科目、金额取数规则),分录生成器只做查表和渲染。科目体系调整(监管报表口径变化几乎年年有)就是改配置加审批,不碰代码。模板表本身要有版本和生效日期,历史凭证按当时的模板口径解释,重跑历史日期的分录生成必须能还原当时的规则——这是"核算可重放"的前提。
试算平衡:日终的账务守门员
三层各自有自己的校验,日终批量里跑三道:
- 凭证级:每张凭证生成时校验借贷相等,不平直接拒绝。这是廉价的第一道闸。
- 科目级试算平衡:全部科目的借方余额合计 = 贷方余额合计。这个校验抓的是"分录生成器漏了某个业务类型"或"模板配错导致单边入账"这类系统性错误。会计恒等式保证了正常账永远平,所以不平即 bug,没有灰色地带。
- 连续性校验:期初 + 发生 = 期末。抓的是余额表被旁路 UPDATE 的脏写(是的,生产上总有人图省事直接改余额,错账处理那篇会讲为什么必须走红冲蓝补)。
三道校验的成本很低(几十万科目行级的加减法),但它们是日终流程的硬栅栏:试算不平衡,清算后续任务不允许放行,必须人工介入。我在实际系统里见过试算不平拦住的真实事故:分录模板改版时漏配了"融资利息计提"的贷方科目,当天几百万利息计提全部单边入账。如果没有这道栅栏,差额会混进客户保证金科目,发现时已经跨了好几个交易日,追平成本是另一个量级。
工程实现上的三个细节
金额用定点数,不用浮点。 会计分录的金额字段必须是 DECIMAL/NUMERIC,参与求和的每一环都不能经过 double。float64 在 $10^{15}$ 量级就开始丢分位,百亿规模的保证金科目汇总一天跑下来,浮点误差足以让试算平衡误报。这条在通用编程里是建议,在账务系统里是纪律。
分录 append-only。 凭证一旦入账不允许 UPDATE 和 DELETE,错误用红字冲销(反向分录)再补记正确的——红冲蓝补有专篇,这里只立规矩:任何"改历史分录"的需求都是伪需求,真需求是"冲销重做"。
凭证号连续且无洞。 凭证编号(按日或按月编号)必须严格连续,断号意味着有凭证被删或生成失败,审计上会直接认定账套不完整。实现上用独立的序号发生器(数据库序列或发号表),不要用 max+1——并发下会撞号,撞号重试就会留下洞。
和"余额缓存列"的关系
上一篇说资金账户表上有余额字段做查询缓存,这篇说余额应该能从分录推导——两者矛盾吗?不矛盾,这是典型的派生数据双写:流水/分录是事实源(source of truth),账户余额是面向交易热路径的物化视图。双写必须同事务,日终再用"按分录重算余额 vs 缓存余额"的全量比对兜底。对账体系那篇会讲这个比对挂在哪个任务上。
复式记账在券商系统里不是会计的合规外衣,而是账务正确性的结构性保障:恒等式不变量 + 试算平衡栅栏 + append-only 凭证,三件事加起来,让"账错了"从"客户投诉才发现"变成"当天晚上批量就拦下来"。设计账务核心时先想清楚这三层怎么落,后面所有业务——清算、核算、对账——都站在它上面。