文章目录

  • 前言
  • 一、对账系统构建
  • 二、执行流程
  • 三、获取支付渠道数据
    • 1.接口形式
      • 1.1 后台配置
      • 1.2 脚本编写
        • 1.2.1 模板
        • 1.2.2 解析脚本
    • 2.FTP形式
      • 2.1 后台配置
      • 2.2 脚本编写
        • 2.2.1 模板
        • 2.2.2 解析脚本
  • 四、获取支付平台数据
  • 五、数据比对
    • 1. 比对模型
    • 2. 比对器
  • 总结

前言

从《支付系统设计一:支付系统产品化》系列中支付网关系统设计我们知道,在对接支付渠道的时候只需要产品经理进行后台渠道相关信息的配置,以及开发人员编写的模板、脚本就能够完成支付渠道的对接了,同样的,通过此模式也可以完成支付系统和支付渠道的对账。


一、对账系统构建

对账系统构建在网关系统的基础上,将网关应用单独部署几个节点,作为对账系统。


需要解决的问题有:

  1. 作为对账的网关节点不能向注册中心注册服务,对账节点只是用来完成对账功能。
    项目有两个主启动类包,demo-reconmain依赖demo-main,其中通过demo-reconmain主启动类启动需要配置:
  // 对账节点不注册服务System.setProperty("eureka.client.register-with-eureka", "false");
  1. 作为处理交易的网关节点不能消费对账任务,网关节点只是用来完成交易功能。
  // 对账节点消费对账任务以及刷新任务System.setProperty("paygw.rabbit.flag", buildRabbitFlag(RabbitFlagConsts.ALL, RabbitFlagConsts.RECON));

关于这块,见以前写的篇博客《多机房控制消息消费方实现》

即通过此设计,支付网关系统又可以作为对账系统,可以很巧妙的解决支付渠道侧对账数据获取的问题。

二、执行流程

三、获取支付渠道数据

不同的支付渠道获取对账文件的方式也各不相同,有的是推送FTP,有的是通过接口下载等,文件类型也有所不同,有的是TEXT,有的是CSV等,文件内容格式也是各不相同,所以我们同样的套路,不同处使用脚本实现,将渠道侧数据解析入临时表。

1.接口形式

1.1 后台配置

如兴业银行的对账文件通过接口下载,那么我们需要配置下载对账文件的通讯信息:

1.2 脚本编写

1.2.1 模板

cib_depute_recon_main.vm

##单付对账脚本
#set($umask  = "1000")
#set($version =  "1.0.2")##版本号
#set($mchtId=$data.merExtends.merId)##渠道商户号
#set($signType="RSA")##签名类型
#set($serialNo=$DateUtil.getCurrentDateTimeStr()) ##渠道请求流水号使用时间
#set($transTime=$DateUtil.getCurrentDateTimeStr())##交易时间
#set($checkType="1")##D+1对账文件
#set($checkDate= $DateUtil.format($data.reconStartDate,"yyyyMMdd"))##对账日期yyyyMMdd
#set($businessMap =
{"version":"$!version","mchtId":"$!mchtId","signType":"$!signType","serialNo":"$!serialNo","transTime":"$!transTime","checkType":"$!checkType","checkDate":"$!checkDate"
})
#set($certCodePrivate=$data.merExtends.certCodePrivate)##商户自己的私钥
#set($businessStr=$MapUtils.generateParamStr($businessMap))
#set($mac=$certService.sign($certCodePrivate,$businessStr))##获取签名
#set($signMap =
{"mac":"$!mac"
})
$umask$JSON.toJSONString($MapUtils.putAll($businessMap,$signMap))

cib_depute_recon_header.vm

#set($map =
{"Content-Type":"application/json;charset=UTF-8"
})
$map

1.2.2 解析脚本

/*** @author Kkk* @Describe: 兴业银行代付对账解析*/
class CIBDeputeReconParser extends AbstractReconDataFetchParser{def logger = LoggerFactory.getLogger(CIBDeputeReconParser.class)def resp_code_success = ["E0000"]public static final String ALGORITHM = "SHA1PRNG"/** 证书服务*/@AutowiredCertService certService/*** 查询结果处理*/@OverrideReconDataFetchResult parse4ReconData(PayGwContext context, Object message) {Map<String, Object> data = context.getMessageDescription().getDatas()ReconDataFetchResult fetchResult = new ReconDataFetchResult()try {//验证签名(必须)def flag = verifySign(context, message)if (!flag) {//签名通过返回解析后的数据,不包含签名类型和签名数据throw new Exception("[兴业银行-单笔代付对账请求] 返回参数,验签失败!")}Object result = JSON.parse(message)JSONObject jobj = (JSONObject) resultdef respCode= jobj.get("respCode");def respMsg= jobj.get("respMsg");if(!resp_code_success.contains(respCode)){fetchResult.setSuccess(false)fetchResult.setRemark("兴业银行对账失败["+respCode+"]["+respMsg+"]");return fetchResult}String fileContent= jobj.get("fileContent")String aesKey= jobj.get("aesKey")//使用私钥解密aesKeydef certCodePrivate = context.getMessageDescription().getData("merExtends").get("certCodePrivate")def aesK=certService.decryptBase64(certCodePrivate,aesKey)//用解密得到的aesKey解密fileContentbyte[] afterFileContent =this.AESDecode(fileContent,aesK)//将得到的fileContent解码byte[] bb1 = BASE64.decodeCib(new String(afterFileContent,"utf-8").toCharArray())//解压缩byte[] dedata = FileUtils.decompress(bb1)//流读取文件内容并入表保存InputStream is = new ByteArrayInputStream(dedata)BufferedReader bufferedReadertry{def tempStrLoggerUtil.info(logger, "兴业银行-单付对账-文件-开始解析")bufferedReader = new BufferedReader(new InputStreamReader(is,"UTF-8"))//解析第一行def str=bufferedReader.readLine()LoggerUtil.info(logger, "解析第一行str:{}",str)while ((tempStr = bufferedReader.readLine()) != null) {if (StringUtils.isNotBlank(tempStr)) {LoggerUtil.info(logger, "兴业对账文件解析内容:{}",tempStr)String[] transStr = tempStr.split("\\|")ReconTrans reconTrans = convert2ReconTrans(transStr)store(context, reconTrans)}}} catch (Exception e) {logger.error("兴业银行-单笔代付-对账异常", e)fetchResult.setSuccess(false)fetchResult.setRemark(e.getMessage())} finally {if (bufferedReader != null) {try {bufferedReader.close()} catch (IOException e1) {}}}fetchResult.setSuccess(true)data.put(PayGwConstant.PAYGW_TRANS_STATUS, TransStatusEnum.SUCCESS.getCode())fetchResult.setRemark("兴业银行-单笔代付对账成功。")return fetchResult}catch (PayGwException e){LoggerUtil.error(logger, "[兴业银行-单笔代付对账请求失败]",  e);fetchResult.setSuccess(false)fetchResult.setRemark(e.getErrorMsg())return fetchResult}catch (Exception e){LoggerUtil.error(logger,"[兴业银行-单笔代付对账请求失败]:{}--异常信息",e)fetchResult.setSuccess(false)fetchResult.setRemark("兴业银行-单笔代付对账失败,paygw解析数据异常")return fetchResult}}/*** 验签*/boolean verifySign(PayGwContext context, String resData) {def certCodePublic = context.getMessageDescription().getData("merExtends").get("certCodePublic")Map<String,String> resMap=MapUtils.covertToJSON(resData);String mac=resMap.get("mac");//获取签约值resMap.remove("mac");String oriSign=MapUtils.generateParamStr(resMap);boolean  vflag=  certService.checkSign(certCodePublic,mac,oriSign)logger.info("兴业银行-单笔代付对账请求,请求返回签名值({}),验签结果({})",mac,vflag)return vflag}/*** 构建对象*/private ReconTrans convert2ReconTrans(def transStr) {ReconTrans trans = new ReconTrans()trans.setInstReqNo(transStr[0])trans.setAcctNo(transStr[1])trans.setTransCode(TransactionEnum.DEPUTE.code)trans.setTransAmount(new BigDecimal(transStr[4]))String dataStr=transStr[5]if(StringUtils.isNotEmpty(dataStr)){Date d=DateUtil.parseDateTime(dataStr,"yyyyMMddHHmmss")trans.setTransDateTime(d)trans.setTransDate(d)}trans.setTransStatus(TransStatusEnum.SUCCESS.code())return trans}/*** 使用AES解密fileContent*/static byte[] AESDecode(String str,String num) throws Exception{KeyGenerator kg = KeyGenerator.getInstance("AES");SecureRandom sr = SecureRandom.getInstance(ALGORITHM);sr.setSeed(num.getBytes());kg.init(128, sr);SecretKey sk = kg.generateKey();byte[] raw = sk.getEncoded();SecretKey key = new SecretKeySpec(raw,"AES");Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");cipher.init(Cipher.DECRYPT_MODE, key);byte[] str_AES = cipher.doFinal(HexConvertorUtil.hex2Bytes(str));return str_AES;}
}

2.FTP形式

2.1 后台配置

如平安银行的对账文件通过FTP下载,那么我们需要配置下载对账文件的通讯信息:

2.2 脚本编写

2.2.1 模板

pingan_depute_recon_main.vm

#set($umask  = "1000")
#set($map =
{"fileName":$!data.fileNameCHK
})
$umask$!MapUtils.toJsonStr($map)

2.2.2 解析脚本

/*** @author Kkk* @Describe: 平安银行单付对账文件解析*/
class PANBANKDeputeReconFilesParser extends AbstractReconDataFetchParser {Logger logger = LoggerFactory.getLogger(PANBANKDeputeReconFilesParser.class)def success = true, remark = '平安银行单付对账入库成功。', panbank_success = "0000"@OverrideReconDataFetchResult parse4ReconData(PayGwContext context, Object message) {ReconDataFetchResult fetchResult = new ReconDataFetchResult()Map<String, Object> data = context.getMessageDescription().getDatas()//先设置结果的扩展字段JSONObject extend = JSON.parseObject(StringUtils.valueOf(data.get("extend1")))logger.info("平安银行单付对账解析文件,第四步解析前extend值为:{}",extend)if (StringUtils.isNotBlank(message) && message.length > 0) {//解析文件,并入库parseFile(context, message)extend.put("stepOrder", 6)fetchResult.setExtend1(extend.toString())fetchResult.setSuccess(success)fetchResult.setRemark(remark)logger.info("平安银行单付对账解析文件data:{}",data)data.put(PayGwConstant.PAYGW_TRANS_STATUS, TransStatusEnum.SUCCESS.getCode())logger.info("平安银行单付对账解析文件,并入库fetchResult.setRemark-4:{}--extend:{}",fetchResult.getRemark(),extend)return fetchResult}fetchResult.setSuccess(false)fetchResult.setRemark("平安银行单付对账失败,文件下载失败。")logger.info("平安银行单付对账fetchResult.setRemark-4:{}--extend:{}",fetchResult.getRemark(),extend)return fetchResult}/*** 解析对账文件*/void parseFile(PayGwContext context, byte[] absoluteFilePath) {def line = null//获取对账文件的文件流InputStream is = new ByteArrayInputStream(absoluteFilePath)try {//按行读取对账文件LineReader xline = new LineReader(new InputStreamReader(is, "GBK"))//获取渠道文件集合List<ReconTrans> list = new ArrayList<>()boolean firstLine = truewhile ((line = xline.readLine()) != null) {//按行转换成对账流水记录ReconTrans reconTrans = convert2ReconTrans(line, firstLine)//从此以后再无第一行firstLine = falseif (reconTrans == null) {continue}list.add(reconTrans)}//入库for (ReconTrans trans : list) {store(context, trans)}} catch (PayGwException e) {//更新success = falseremark = '平安银行单付对账失败,原因:' + e.getErrorMsg()LoggerUtil.error(logger, "文件读取异常:", e)} catch (Exception e) {//更新success = falseremark = '平安银行单付对账失败'LoggerUtil.error(logger, "文件读取异常:", e)}finally {IOUtils.closeQuietly(is)}}private ReconTrans convert2ReconTrans(def lineStr, boolean firstLine) {ReconTrans trans = new ReconTrans()def transStatusdef item = lineStr.split("\\|\\:\\:\\|", -1)def transDate = item[0]def instReqNo = item[3]def acctNo = item[5]def transAmt = new BigDecimal(item[6])//0000 成功 其余为失败def errorCode = item[10]//交易状态(平安银行对账文件全为成功数据)if (StringUtils.equals(errorCode, panbank_success)) {transStatus = TransStatusEnum.SUCCESS.code()} else {//其他状态不入库return null}trans.setTransStatus(transStatus)trans.setInstReqNo(instReqNo)trans.setTransCode(TransactionEnum.DEPUTE.code)//用户卡号trans.setAcctNo(acctNo)//交易金额trans.setTransAmount(transAmt)//交易日期trans.setTransDate(DateUtil.parseDate(transDate, "yyyyMMdd"))//交易时间,因对账文件中不存在交易时间,故将交易日期入库trans.setTransDateTime(DateUtil.parseDateTime(transDate, "yyyyMMdd"))return trans}
}

四、获取支付平台数据

直接使用SQL查询出对应支付渠道的对应的交易类型的交易数据。

五、数据比对

拉取两侧数据,构建数据比对模型放到内存中进行数据比对

1. 比对模型

/*** @author Kkk* @Describe: 对账-比对模型*/
public class CompareModel {/*** 唯一索引*/private String uniqueIndex;/*** 值*/private String value;/*** 业务流水ID*/private Long transId;
}

2. 比对器

/*** @author Kkk* @Describe: 对账-比较器定义*/
public interface IComparator {IComparator putOrigins(List<CompareModel> origins);IComparator putTargets(List<CompareModel> targets);CompareResult compare();
}

总结

后文详细展开具体实现。

支付系统设计五:对账系统设计01-总览相关推荐

  1. 支付设计白皮书:支付系统的对账系统设计

    对账介绍 看这篇文章的相信大家对支付都有了解,对于对账来说应该不陌生,肯定也明白对账的目的.简单例子,就是你和另外一个人做生意,约定的结款是月结,他每天都从你这里进货,你会记账说我应该收多少钱,他也会 ...

  2. java财务对账系统设计_聊聊对账系统的设计方案

    前言 对账系统作为支付系统中的基石系统,处于整个支付环节中的最后一层,主要用来保证我方支付数据与第三方支付渠道或银行的数据一致性. 在没有对账系统之前,财务在第二日手工核对前一日的应收与实收.倘若不一 ...

  3. java对账系统设计_聊聊对账系统的设计方案

    前言 对账系统作为支付系统中的基石系统,处于整个支付环节中的最后一层,主要用来保证我方支付数据与第三方支付渠道或银行的数据一致性. 在没有对账系统之前,财务在第二日手工核对前一日的应收与实收.倘若不一 ...

  4. 对账系统设计详解(下)

    本文由作者 陈天宇宙 发布于社区,业务图较多,建议PC端阅读 上篇指路:对账系统设计详解(上) 07 资金对账项目配置设计完成线上支付交易以后,虽然通道方告知支付成功,但是钱是不是真的能给,还需要打一 ...

  5. 团队作业第五次—项目系统设计与数据库设计

    作业描述 所属课程 软件工程1916|W(福州大学) 作业要求 团队作业第五次-项目系统设计与数据库设计 团队名称 待就业六人组 作业目标 宏观的对系统的整体结构设计,并在此基础上,进行数据库设计 系 ...

  6. 对账系统设计详解(上)

    本文由作者 陈天宇宙 发布于社区,业务图较多,建议PC端阅读01 对账介绍想必大家对"对账"这个词都不陌生,单从字面意思就能略知一二:其实就是字面意思:"对"就 ...

  7. 万字长文详解对账系统设计,推荐收藏

    想必大家对"对账"这个词都不陌生,单从字面意思就能略知一二:其实就是字面意思:"对"就是核对,"账"就是账目:"对账"就 ...

  8. 闭式系统蒸汽管径推荐速度_空调水系统设计、空调风系统设计要点

    空调水系统设计.空调风系统设计要点 一.空调水系统流速的确定 一般,当管径在DN100到DN250之间时,流速推荐值为1.5m/s左右,当管径小于DN100时,推荐流速应小于1.0m/s,管径大于DN ...

  9. 支付宝支付 第五集:二维码生成工具

    支付宝支付 第五集:二维码生成工具 一.代码 目录结构 BufferedImageLuminanceSource.java package com.dzy.alipay.qrcode;import c ...

最新文章

  1. Windows 编程[9] - WM_CLOSE 消息
  2. SpringBoot 中添加jsp支持遇到的问题
  3. C#中Invoke 和 BeginInvoke的涵义和区别
  4. JSP 统计网站访问人数
  5. android系统存储路径在哪里,Android 手机存储目录
  6. 69.Daily Temperatures(日常气温)
  7. 使用代码形式配置Log4J日志框架
  8. 第 14 章 结构和其他数据形式(names)
  9. c事件和委托的区别_web前端教程分享JavaScript学习笔记之Event事件二
  10. 腾达无线加密与Win7
  11. 开源网络爬虫程序(spider)一览
  12. VLAN详解系列:(6)VLAN间路由详解
  13. 名悦集团:深圳人买什么车好?
  14. 区块链与大数据,打造智能经济(读书笔记)——井底望天
  15. 一般来说仿制一个网站大概需要多少钱呢
  16. mixin机器人java开发_mixin_labs-java-bot
  17. 什么是jQuery,jQuery的特点。
  18. 局域网访问提示无法访问检查拼写_Win7访问共享文件夹提示请检查名称的拼写的解决方法...
  19. 最具有催眠功能的网站
  20. 数字化赋能,助推微电网高质量发展

热门文章

  1. 知识产权行业拓客的10个经典方法
  2. Git安装图文教程(Windows、Linux全平台)
  3. 一篇文章搞懂【Emmet】语法规则(前端必备技能)
  4. 再见2021,程序员如何走向鄙视链的顶端?
  5. 知乎上关于电子商务话题的精彩问答
  6. 车辆被盗后发生交通事故由谁来赔偿
  7. 有人想让汽车无人化,但有人想让汽车飞起来
  8. 诺贝尔奖今起揭晓 4位华裔科学家成热门
  9. Python数据采集案例(1):微博热搜榜采集
  10. K8s——kubernetes集群中ceph集群使用【下】