Table of Contents
对账规则
公司业务接入银行存管后,由存管银行负责投资者交易清算与资金交收。涉及跟第三方交易必须通过对账来保证资金的一致性。对账一般有两种,单向对账跟双向对账。由于公司所有资金流动都以银行为准,采用单向对账的方式。
单向对账:一般拿第三方支付机构或银行流水,与自己系统进行对账,防止出现掉单问题;
双向对账:两个应用间的流水进行双向核对,如订单与财务系统,既要保证财务系统支付成功的记录,订单系统也是成功的;也要确保订单系统记录成功的记录,财务系统也成功。
银行每天凌晨把昨天的交易资金流水以csv的形式打包成zip,调用方通过https下载到本地服务器。对账csv按业务不同分为不同的文件,以付款业务csv对账文件模板为例,简化如下
订单号 | 付款人ID | 收款人ID | 金额 | 订单状态 |
20181217001 | 123 | 321 | 100 | SUCCESS |
其中订单号、付款人、收款人是保存在银行跟我方交易系统中的全局唯一号,是进行对账的凭证。实际的对账文件不止这么多字段,但对账的关注点在于谁付了钱,谁收了钱,金额大小,以及订单状态,其他字段根据实际业务进行取舍。
对账适配器
我方系统中的交易记录跟银行对账文件的字段名、字段多少,乃至单位、格式都不一样。对账前需要把双方的数据格式化成同样的数据进行比对。交易量目前不大,为了简单直接在内存里面比对。
由于银行对账文件为标准的csv文件,轻易不会变动,采用Jackson旗下出品的csv解析框架jackson-dataformat-csv把csv转换为JavaBean。jackson-dataformat-csv支持懒加载,先建立csv到JavaBean对象的映射,真正访问JavaBean的时候才去加载csv,避免大量对象占满内存。
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-csv</artifactId>
<version>2.8.10</version>
</dependency>
解析后把csv的JavaBean对象转换为对账适配器,接着根据全局唯一订单号跟业务类型到我方系统中查询对应的交易,也转换为对账适配器。对账适配器简化如下:
public class TransactionCompareAdapter {
//省略getX、setX
@PropertyName("请求流水号")
private String requestNo;
@PropertyName("金额")
private String amount;
@PropertyName("发起方平台用户编号")
private String sourcePlatformUserNo;
@PropertyName("接收方平台用户编号")
private String targetPlatformUserNo;
@PropertyName("交易状态")
private String transactionStatus;
}
对账比对
由于双方数据已经完成格式化,这时候就轮到专业比对框架JaVers出场了。注意适配器中的PropertyName就来自org.javers.core.metamodel.annotation.PropertyName,通过注解给类的Field设置自定义中文名。如果有些字段不想参与比对,可以通过@DiffIgnore注解忽略。适配器的Field全部为字符串类型,一是便于框架进行比对,二是为了处理简单。JaVers的Maven依赖如下
<dependency>
<groupId>org.javers</groupId>
<artifactId>javers-core</artifactId>
<version>3.3.4</version>
</dependency>
JaVers底层基于若干字符串比对算法,其中常用的一种为 Levenshtein distance算法。这里不对算法进行详细介绍,算法的运用保证了比对的准确与高效,避免了大量的if else。下面通过示例代码模拟对账。
import TransactionCompareAdapter;
import java.util.List;
import org.javers.core.Javers;
import org.javers.core.JaversBuilder;
import org.javers.core.diff.Change;
import org.javers.core.diff.Diff;
import org.javers.core.diff.ListCompareAlgorithm;
import org.javers.core.diff.changetype.ValueChange;
import org.junit.Test;
public class JavaBeanCompareTest {
/**
* 创建基于Levenshtein distance算法的比对工具类
* */
private static final Javers javers =
JaversBuilder.javers()
.withListCompareAlgorithm(ListCompareAlgorithm.LEVENSHTEIN_DISTANCE)
.build();
@Test
public void transactionCompareAdapterTest() {
// 银行流水记录
TransactionCompareAdapter bank = new TransactionCompareAdapter();
bank.setAmount("199");
bank.setSourcePlatformUserNo("123");
// 我方平台流水记录
TransactionCompareAdapter myAccount = new TransactionCompareAdapter();
myAccount.setAmount("99");
myAccount.setSourcePlatformUserNo("123");
// 进行比对
Diff diff = javers.compare(bank, myAccount);
// 列出两笔流水的所有不同
List<Change> changes = diff.getChanges();
for (Change valueChange : changes) {
// 打印不同Field跟Field的中文名
ValueChange change = (ValueChange) valueChange;
System.out.println(String.format("%s不匹配,期望值%s,实际值%s", change.getPropertyName(), change.getLeft(),
change.getRight()));
}
}
}
由于示范代码中双方amount(金额)不一致,比对后打印如下
金额不匹配,期望值199,实际值99
对账不匹配的处理
每天凌晨4点从银行下载对账文件开启对账。如果没出现任何不一致,说明对账成功,跟银行确认对账成功。所有的对账类型逻辑上保证幂等,对账成功后哪怕重复对账结果应该仍然是正确的。
由于网络原因,银行订单已经处理成功,我方订单可能还卡在处理中,状态不一致不可避免。状态不一致的时候对账系统通过发送MQ给对应的交易系统,把订单状态自动更改为跟银行一致。如果金额、收款方、付款方不一致,说明出现严重Bug,通过邮件通知或者其他手段,人工介入处理。
对账轮询采用了唯品会开源的分布式Job框架Saturn。Saturn可以通过控制台动态更新对账轮询时间,特定时间的轮询次数,手动触发轮询,便于在对账失败或者数据修复后再次发起对账。