大型支付系统学习:金额精度与账务分录

用浮点数计算金额,可能在累计和比较时产生难以解释的差异。

edyang · 2013-10-05

用浮点数计算金额,可能在累计和比较时产生难以解释的差异。账务模型需要先定义货币单位。

本文使用Java 7 与支持事务、唯一约束的关系数据库,代码为设计伪代码。支付系统同时面对用户请求、外部通道和本地账务,三者不共享一个事务。网络异常下必须保留“不确定”状态,并用查询、流水和对账逐步消除差异。

1. 从现象推到机制

可以用最小货币单位的整数,也可以用明确精度与舍入规则的十进制。构造 BigDecimal 时用字符串避免引入二进制浮点近似。账务分录应记录资金变化原因,余额是结果视图,不能只保存一个可被覆盖的数。

大型支付系统学习:金额精度与账务分录的处理、反馈与观察关系

图中的四个节点分别表示业务金额、精确运算、账务分录、余额视图。顺着实线检查输入如何变成输出,再沿虚线核对反馈条件。图示表达的是本文讨论的关系,不是实测曲线或现场截图。

2. 最小实现与输入条件

下面仅演示十进制精确相加。涉及费率时还需定义何时舍入、按什么粒度舍入,以及分摊余数如何归属。

1
2
3
4
BigDecimal a = new BigDecimal("0.10");
BigDecimal b = new BigDecimal("0.20");
BigDecimal sum = a.add(b);
boolean equal = sum.compareTo(new BigDecimal("0.30")) == 0;

示例使用虚构订单和金额,不能连接真实支付通道。实现时需要替换数据库访问与验签适配,并明确事务提交点;外部接口的签名字段和响应约定以通道协议为准。

3. 用对照区分原因

下表列出需要主动改变的条件。预期行为来自机制分析,复现时还需记录运行环境和原始输出,不能将预期当作已经测得的结果。

改变的条件 需要验证的行为
相同数值但小数位不同 compareTo 可表达数值比较
多行金额先舍入再汇总 可能与汇总后舍入不同
重复录入同一资金事件 必须被事件唯一键阻止

对照 1:相同数值但小数位不同

compareTo 可表达数值比较。观察时记录数值相等与 equals 的刻度差异。把这个条件与基准情形分开,有助于判断变化来自哪一层;若得到相反结果,应先核对前提,再修改实现。

对照 2:多行金额先舍入再汇总

可能与汇总后舍入不同。观察时记录每行原值和舍入位置。把这个条件与基准情形分开,有助于判断变化来自哪一层;若得到相反结果,应先核对前提,再修改实现。

对照 3:重复录入同一资金事件

必须被事件唯一键阻止。观察时记录分录来源业务流水。把这个条件与基准情形分开,有助于判断变化来自哪一层;若得到相反结果,应先核对前提,再修改实现。

4. 适用条件与取舍

金额精度不等于账务正确。仍需校验借贷平衡、资金事件唯一性和可追溯性,演示代码不构成完整会计系统。

一次正常运行只覆盖了这组输入。继续扩展前,应把上面的异常对照保留下来,尤其关注“重复录入同一资金事件”这一分支。后续方案若改变了图中任意一处依赖,原来的判断就要重新验证,不能仅凭界面相似或返回值相同沿用结论。

同月延伸

参考资料

  1. Java 7 BigDecimal
  2. RFC 2104:HMAC