
上线钱包最快的方法,是在一行记录里放一个数字:一个 balance 字段,钱进来就加,钱出去就减。演示时它能用,试点时它也能用。然后,大约在最初一万笔真实交易前后,有人跑了一份报表,数字对不上,谁也说不清钱去了哪里。一旦涉及真实价值,这就不只是一个缺陷,而是信任的终结,而信任恰恰是金融产品真正售卖的唯一东西。
防止这种情况的方法已有六百年历史。复式记账并不是附加在软件上的会计装饰,而是让货币系统可证明的数据模型。这就是为什么每一个清算级账本都建立在它之上,也是为什么你的钱包账本也必须如此。
其他一切所依赖的不变量
在复式记账账本中,钱既不会被创造,也不会被消灭,它只会移动。每笔交易都会过账一组分录,其借方与贷方之和恰好为零。一笔存款借记代理的备付金账户,同时以相同金额贷记客户的钱包。一笔转账借记付款方,贷记收款方,并为手续费贷记一个收入账户。在整个系统中,在任何时刻:
所有借方之和永远等于所有贷方之和。账目轧差为零。始终如此。
这一个属性承担了巨大的工作。它意味着不存在任何能够创造或消灭价值的代码路径:一个余额只能作为另一个余额变化的镜像而变化。它意味着财务总监可以在账目上签字,监管机构可以信任这些账目,因为系统的正确性来自其结构,而不是来自侥幸。它还意味着对账不再是一场法证调查,而是一个可以断言、可以测试的属性。
再对比单一余额的做法。在那里,余额是一个没有来源的事实。如果它错了,你无法判断是某笔贷记被执行了两次、某笔借记丢失了,还是竞态条件让两次写入交错在了一起。你有一个数字,却没有来龙去脉。复式记账账本首先是一个保存来龙去脉的系统。
账户、分录,以及为什么要把钱建模为流动
思维上的转变在于:不再把余额看作要存储的东西,而是把它看作推导出来的东西。余额是某个账户上所有已过账分录的总和。钱包是账户。代理备付金是账户。手续费收入、挂账、结算、无人认领资金,全都是账户。每一次价值流动都是一笔包含两条或更多分录、合计为零的交易。
这带来一个深远的结果:账本是只追加的。你从不原地修改余额,而是过账新的分录。历史永远不会被改写。这正是系统可审计的原因:每一笔曾经流动过的钱都留下了永久、有序的轨迹,任何过去时刻的任何余额,都可以通过重放截至该时刻的分录来重建。
那些被遗忘、日后要付出代价的部分
不变量是必要的,但并不充分。还需要三个属性,才能把一个纸面上正确的账本,变成一个经得起真实网络和真实攻击者考验的账本。
幂等性
网络会中断。客户端发出请求,响应在途中丢失,客户端于是理所当然地重试。如果没有保护,第二个请求会过账第二笔交易,客户就被扣了两次钱。解决办法是幂等键:每个逻辑操作携带一个唯一令牌,账本记录已经执行过的令牌,带着已知令牌的重试请求会返回原始结果,而不是再次过账。
这听起来简单,实则微妙。检查和写入必须是原子的:如果两次重试同时到达,必须恰好只有一次胜出,另一次看到的是结果,不能存在两者同时推进的窗口。这里一旦做错,你就构建出了一个在负载下会双花的系统,而这正是测试中不会出现、上线当天却会出现的故障。
冲正,而不是删除
总会出问题:争议、差错、欺诈投诉。人的本能是删掉有问题的交易。永远不要删除。被删除的交易就是审计轨迹上的一个窟窿,而监管机构会把窟窿解读为掩盖。正确的做法是过账一笔冲正:一笔新交易,其分录是原交易的镜像,原交易保持不变。对余额的净影响为零;历史同时展示错误及其更正,并附有原因和操作员。一个能够撤销任何业务(个人转账、缴费、购物付款、代理现金、备付金、储蓄)的统一冲正引擎,就是可审计系统与一场争吵之间的区别。
事前检查
余额、限额和 KYC 状态要在交易过账之前校验,并且就在过账它的同一个原子操作之内:绝不能在之后,也绝不能放在可以被绕过的应用里。客户不能超过的限额不是一个被禁用的按钮,而是在资金即将流动之处强制执行的约束。应用只是便利,账本才是权威。任何重要的规则都必须驻留在资金所在之处。
并发:正确设计的葬身之地
两个请求在同一瞬间打到同一个钱包上:一笔转账和一笔取现,各自单独看都合法,加在一起却超过了余额。朴素的实现会在两个请求中都读取余额,都发现余额充足,于是两笔都过账,账户被透支。这是经典的金融竞态条件,在你真正面对并发之前,它是看不见的。
防御办法是把账户作为串行化的单位:对某个账户过账时加锁,或使用原子条件写入,使两个请求被排序,后一个能看到前一个的效果,只有一个成功。难点在于做到这一点而不把整个账本串行化:按账户加锁,而不是全局加锁,让系统在保持正确的同时依然快速。这类问题,正是区分做过账本的工程师和即将领教账本为何困难的工程师的分水岭。
对账变成一种属性,而不是一场救火演习
在单一余额系统里,对账是每晚的一场调查:导出所有数据,与支付通道比对,追查差异,祈祷天亮前能找到。在复式记账系统里,对账在很大程度上是一个可以直接断言的同义反复:内部账户在结构上就合计为零,剩下的工作是把外部结算(银行、移动货币运营商和银行卡处理机构实际划转的资金)与账本早已跟踪的挂账和结算账户进行匹配。差异会以具体的未匹配分录形式出现,并附带一个明确的问题,而不是一个没有来龙去脉的缺失数字。
一个实例:一笔转账的过账
假设一位客户向朋友转账 1,000,手续费 25。在复式记账账本中,这一笔转账就是一笔包含三条分录的交易。金额仅作示意。
| 账户 | 借方 | 贷方 |
|---|---|---|
| 付款方钱包 | 1,025 | |
| 收款方钱包 | 1,000 | |
| 手续费收入 | 25 | |
| 合计 | 1,025 | 1,025 |
借方等于贷方,因此这笔交易合计为零。关于它你可能提出的每个问题,在分录中都有答案:付款方付了多少、收款方收到多少、运营方赚了多少、发生在何时。如果这笔转账后来被冲正,一笔镜像交易会以相反方向过账同样的三行,两者都保留在历史中。
代理网点现金存入的过账
代理交易说明了这个模型为何能够扩展。当客户在代理网点存入 2,000 现金时,代理的电子备付金转到客户名下,代理获得一笔佣金。假设佣金为 20:
| 账户 | 借方 | 贷方 |
|---|---|---|
| 代理备付金 | 2,000 | |
| 客户钱包 | 2,000 | |
| 佣金费用 | 20 | |
| 应付代理佣金 | 20 | |
| 合计 | 2,020 | 2,020 |
现金本身从不进入账本,因为它是在柜台上实物交接的。账本记录的是从代理转到客户的电子价值,以及为这项服务向代理付费的义务。佣金支付时,另一笔平衡的交易会结清这笔应付款。没有任何估算,也没有任何事后在电子表格里的计算。
设计会计科目表
你创建哪些账户,决定了账本能回答哪些问题。一个钱包平台通常需要:
- 客户、代理、商户和企业钱包,每个各一个账户;
- 按产品拆分的手续费收入账户,以便看清每项服务的收益;
- 面向代理和合作方的佣金费用与应付佣金;
- 按外部合作方设立的结算账户:每家银行、交换平台、收费方和支付通道;
- 用于最终去向尚未确认资金的挂账账户,例如等待合作方回复的转账;
- 一个资金保障镜像账户,跟踪托管账户中应有的金额,使每日与银行对账单的比对成为一次检查。
一个实用的检验标准:如果财务部门提出一个问题,而回答它需要一张电子表格,那你很可能缺了一个账户。
正确地测试账本
对于一个绝不能出错的系统,普通的单元测试是不够的。真正重要的测试检验的是属性,而不是个例:
- 每笔交易合计为零,在数千种随机生成的交易类型和金额上验证。
- 重放等于状态:从分录重建每个余额,得到的恰好是已存储的余额。
- 重试绝不重复:同一请求发送多次,包括并发发送,只过账一次。
- 竞争绝不透支:对同一账户发起相互竞争的借记,绝不会使其低于限额。
- 冲正可以复原:冲正任何一笔交易,都会使每个受影响的余额回到之前的值。
这些测试应纳入发布流水线,并且还应以只读方式在生产数据上运行,让不变量在真实账目上持续得到检验,而不仅仅是在测试数据上。
从单一余额系统迁移
许多团队在上线之后才认识到这些原则。迁移可以在不中断服务的情况下进行:
- 冻结现有余额的结构,并在截止时间点拍下快照。
- 在新账本中创建期初余额交易,把快照中的每个余额对一个权益或迁移账户过账。
- 让两个系统并行运行一段时间,每笔新交易同时过账到两边,并每天比对余额。
- 逐一调查每个差异,直到并行运行连续数天都干净无误。
- 把读取切换到新账本,然后下线旧的余额字段。
并行运行通常会暴露旧系统中的历史错误。要预留时间如实解决它们,而不是强迫新账本去匹配一个错误的数字。
回报
按这种方式构建账本,一系列难事就会变得容易。对账单是对分录的一次查询。审计是一次重放。争议有书面轨迹可查。新渠道(USSD 转账、应用转账、API 扣款)只是通往同一个有保障核心的不同入口,这意味着它们彼此之间不可能出现偏差。规则只存在于一个地方,只执行一次,对所有人一视同仁。
如果图快去构建,你继承的就是相反的东西:一个解释不了的数字,一个信不过的对账,以及一个上线日,那天你发现最便宜的构建方式,原来也是持有成本最高的方式。
不止一种货币
处理汇款或区域支付的钱包会遇到第二个维度:货币。规则不变,但要按币种分别适用。每个账户只持有一种货币,每笔交易在它涉及的每一种货币上都必须合计为零。因此,一次换汇由两条平衡的腿组成,中间由一个兑换账户连接:客户的本币转入运营方的本币头寸,美元则从运营方的美元头寸转给收款方。兑换账户随时显示运营方的外汇敞口,这正是资金部门管理风险所需要的。把多种货币混在一个账户里,或者在不记录两条腿的情况下即时换汇,正是多币种账本悄无声息地亏钱的方式。
对账单、月结与审计
由于余额由分录推导而来,对账单就只是某个账户在一段时间内的分录,加上必须与期间分录相符的期初和期末余额。月结变成一项检查而不是一个项目:试算平衡合计为零,资金保障与银行相符,挂账有据可查,手续费和佣金总额与产品报表一致。审计人员可以任选一个余额,追溯到每一笔具体交易,每笔都带有时间戳、渠道、参考号,以及在有人参与时的操作员。正是这种可追溯性,让第一次审计成为例行公事,而不是一场惊吓。
妥善管理挂账账户
挂账账户是资金在最终去向尚不明确时的等候之处:一笔发往其他网络、等待确认的转账,一笔参考号对不上任何客户的存款,一笔没有附带明细的合作方付款。它的作用在于,在问题尚未解决时保持账目平衡。一旦它变成钱被遗忘的地方,就会变得危险。好的做法很简单:挂账中的每一项都带有参考号和挂账原因,每一项都有负责人和账龄,余额每天复核,超过几天的一律上报。只增不减的挂账余额,说明上游某处出了故障。
常见问题
复式记账比直接更新余额慢吗?
用两三条分录代替一次更新,增加的成本很小,而且余额可以与分录一并维护,以便快速读取。真正的性能工作在于按账户加锁而不是全局加锁,设计良好的账本正是这样做的。
账本采用复式记账后,我们还需要会计吗?
需要。账本保证的是内部一致性;决定产品如何核算、调查外部差异、向监管机构报告的,仍然是人。改变的是,他们把时间花在专业判断上,而不是寻找缺失的数字。
账本分录可以被修改吗?
不可以。更正就是新的交易,通常是冲正,并记录原因和操作员。可修改的账本无法审计。
PesaBridge 在核心层强制执行这一不变量。应用、USSD 和开发者 API 不会重新实现资金流转,它们呈现的是账本早已保证的规则,因此每个渠道之所以正确,是因为核心是正确的。