随着电商类的在线交易平台越来越多,涉及到资金操作相关逻辑也惨杂其中。然而大多数的程序员并不具备账务领域的专业知识,相关资料也很匮乏,完全靠自己一步一印的踩坑(我也是其中一名)。现将自己这些年接触账务领域的点点滴滴记录下来,旨在给需要的小伙伴扫盲。
一、账务是什么
账务就是管控一切资金行为。系统里的账务产生的数据,会提供给公司里的会计或者用户进行对账。而我看到最多的情况就是他们对着对着就账不平了,有的资金错误问题我们发现了会计没发现,有的问题会计发现了我们没发现。甚至有用户(大卖家)经常过来质问“我的钱怎么算出来亏了”,我知晓后不怀好意的揣测:可能这个用户随着在平台上做生意,生活慢慢改善了,房子车子都买了,最后在系统上对账盘利润发现是负的,直接懵逼。讨说法无果后,这个用户只好持续“亏损”状态下挣更多的钱。
说到这里可能都很好奇,为什么钱会消失?当时的我也很好奇,研发人员就负责功能实现,你要啥就给你实现啥,咋就这么多问题呢?下面就从我所亲身经历的项目来尝试探讨账务的本质。
二、初识账务
初工作时被安排到一个新项目组里,负责实现新系统里的订单交易功能(这么重要的功能就丢给我这个菜鸟)。当时的设计非常粗暴,扣款加款都依赖于订单状态,归属于账务领域的表只有:余额表、资金明细表。如下图:
就这样以最简单的方式把功能实现了,也顺利上线运营了,感觉自己挺牛逼的。过了一段时间偶然发现代码里有个瑕疵(严重bug),订单完成后在某个场景下钱无法加到卖家余额里。排查发现有个卖家账户里少加了几千元,他不知道,公司会计不知道,我是第一个知道,整个公司的生死存亡就看我代码写的怎样了(惶恐中)。
在这样的模型下,强耦合了订单和余额的关系,所有余额的扣减如果要相对安全准确,就必须和订单表绑定在一个事务里,但后续遇到其他订单或者付款单据时,就得要把余额与所有业务付款单据都进行事务绑定,极其混乱。不久之后在当前SOA大背景下进行了账务中心的拆分,形成领域聚合。
三、领域聚合
账务中心拆分出来自成领域服务,需要与订单解耦,此时需要引入账务领域的支付单来代替订单。同时增加了网关单专门与微信等第三方支付渠道对接,这样带来的好处是职责分离,支付单专门对接业务,网关单只对接支付渠道。如下图:
聚合成账务中心后解决了几个方面的问题:①账务中心抽象出统一的资金行为,支持任意业务单的付款使用;②账务的业务聚合起来由专职团队维护,不会再让我这样的菜鸟程序员有写bug的机会;③可以输出统一的对账数据,避免资金数据散落。
在不具备账务领域专业知识的背景下,到这步改造已经基本结束了,后续只需根据业务需求再补充一些充值、提现、退款等功能场景。然而异常场景带来的对账问题并没有得到解决,比如说:在线微信支付成功后,支付单支付失败或金额发生变化了该如何保障?订单异常出错卡在已支付未结算时如何对账?
四、资金落地
资金落地这个概念极其重要,意思就是所有的资金都必须落在对应的账户余额里,而不能卡在单据里。比如余额支付100元,支付之前这笔100元在买家的账户余额里,支付之后就消失了,同时支付单会变成已支付,在结算成功之后这笔100元才会出现在卖家账户余额里。这种情况下整个系统的账很难核对,特别是在系统出故障后带引起大量的资金悬而未决,需要人工一一核对。引入资金落地后如下图:
做了两点调整:
1、所有的在线付款线经过充值再付款,这样做的好处是只要微信等支付渠道回调成功,就可以把在线付款的钱落地到买家账户里,不会因为支付失败而丢失资金;
2、引入平台担保账户,所有买家已支付,卖家未收到的钱全部入担保账户,这样会计对账时只需要看所有的账户余额就能掌控资金的去向。
五、中央银行
引入资金落地概念后让资金变得有迹可循,担保账户的介入让交易资金从原来的凭空消失和凭空出现变成了资金转移。但并不完美,还是存在充值会让资金凭空出现,提现会让资金凭空消失。于是我引入中央银行的概念,彻底消除所有的增与减,让所有资金符合一个规律:“资金不会凭空出现也不会凭空消失,只会从一个账户转移到另一个账户”。如下图:
中央银行发行账户初始化时发行1亿资金,有用户充值时就从发行账户转账到被充值账户,消灭了充值的无中生有。引入结算单,将支付和结算分离,但结算单会依赖支付单。
相对于充值、支付、结算等正向流程,逆向流程如下图:
退款行为做了退款和提现统一抽象,通过逆向网关单与微信等支付渠道打通,整个流程分成三步:
1、退已经结算给卖家的钱,从卖家账户里把钱扣除退还到平台担保账户里;
2、退款给买家,从平台担保账户里把钱退给买家;
3、原路返回,触发退款(提现)行为,从买家账户里把钱先转入在途账户,等微信回调退款成功后再从在途账户里回收到中央银行的发行账户。
综上所述,做到了任何时刻,系统里所有账户(包括发行账户)总余额是1个亿。与现实世界进行对比,所有充值行为就类比为一种外汇与中央银行兑换本币资金,所有提现就是用本币兑换外汇行为,市面上所有印出来的资金总量是1个亿,当然如果不够用了,介于大量充值行为(外汇担保)继续增加货币发行量。
六、虚账约束
在上问的架构设计中,通过引入系统级别的发行账户、担保账户和在途账户,将所有资金被牢牢锁住。这些账户加上所有用户的余额账户均为实账,方便对账和追溯资金来源,但是并不能做代收代付的安全约束,比如支付100元却结算200元,或者是支付100元退款300元等等,甚至每笔支付单还会存在多个返点单。要想解决这个约束问题,需要引入支付单的虚账概念。所有交易资金都会进担保账户,但是担保账户共用的,无法体现在每笔支付单上的分配。可以通过每一笔支付单上设置虚账来记录当前拥有的担保资金余额,每次操作原子性更新,通过支付单来约束相关结算单、返点单、退款单。可以得出所有支付单上记录的担保资金余额总和等于系统担保资金余额,结构关系如下图:
支付单为每笔交易行为的总管理,正向流程与逆向流程都通过支付单对应的虚账来做控制,可以防范超结算超退,同时也能统计出异常存留的担保资金。
同样的道理,在途资金映射到每笔提现单上提现的金额也算是提现单的虚账,用户提现后可以看到自己可用余额是多少,冻结余额(在途)是多少,这时展示的冻结余额并没有实际的账户映射,只是方便查看的一种虚账。
七、总结
上文的分析做一个归纳总结:
1、账务中心不依赖其他任何系统,所有的账户余额和单据都归属账务中心所有;
2、不要在一个单据上做多种行为,支付和结算通过单据隔离;
3、资金必须落地;
4、资金不能凭空出现与消失,只能从一个账户到另一个账户(去掉代码里的加钱和减钱行为,只留原子性的转账行为);
5、系统级的账户有:发行账户(中央银行)、担保账户、在途账户;
6、账户中心只忠实记录所有的资金行为,千万不要将营销抵用金、记账本等非资金行为引入;
7、实账容万金,虚账定乾坤。