交易对账系统的实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/j16421881/article/details/85052284

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可以通过控制台动态更新对账轮询时间,特定时间的轮询次数,手动触发轮询,便于在对账失败或者数据修复后再次发起对账。

猜你喜欢

转载自blog.csdn.net/j16421881/article/details/85052284