支付系统设计五:对账系统设计01-总览

news2024/11/26 22:20:04

文章目录

  • 前言
  • 一、对账系统构建
  • 二、执行流程
  • 三、获取支付渠道数据
    • 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. 作为处理交易的网关节点不能消费对账任务,网关节点只是用来完成交易功能。
    关于这块,见以前写的篇博客《多机房控制消息消费方实现》

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

二、执行流程

在这里插入图片描述

三、获取支付渠道数据

不同的支付渠道获取对账文件的方式也各不相同,有的是推送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"
    /*
     * 证书服务
     */
    @Autowired
    CertService certService

    /**
     * 查询结果处理
     */
    @Override
    ReconDataFetchResult 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) result
            def 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")
            //使用私钥解密aesKey
            def certCodePrivate = context.getMessageDescription().getData("merExtends").get("certCodePrivate")
            def aesK=certService.decryptBase64(certCodePrivate,aesKey)
            //用解密得到的aesKey解密fileContent
            byte[] 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 bufferedReader
            try{
                def tempStr
                LoggerUtil.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"

    @Override
    ReconDataFetchResult 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 = true
            while ((line = xline.readLine()) != null) {
                //按行转换成对账流水记录
                ReconTrans reconTrans = convert2ReconTrans(line, firstLine)
                //从此以后再无第一行
                firstLine = false
                if (reconTrans == null) {
                    continue
                }
                list.add(reconTrans)
            }
            //入库
            for (ReconTrans trans : list) {
                store(context, trans)
            }
        } catch (PayGwException e) {
            //更新
            success = false
            remark = '平安银行单付对账失败,原因:' + e.getErrorMsg()
            LoggerUtil.error(logger, "文件读取异常:", e)
        } catch (Exception e) {
            //更新
            success = false
            remark = '平安银行单付对账失败'
            LoggerUtil.error(logger, "文件读取异常:", e)
        }
        finally {
            IOUtils.closeQuietly(is)
        }
    }

    private ReconTrans convert2ReconTrans(def lineStr, boolean firstLine) {
        ReconTrans trans = new ReconTrans()
        def transStatus
        def 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();
}

总结

后文详细展开具体实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/538549.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AE基础教程

一&#xff1a;粒子插件。 AEPR插件-Trapcode Suite V18.1.0 中文版 二&#xff1a;跟随手指特效。 1&#xff1a;空对象位置关键帧跟随手指。 2&#xff1a;发射粒子位置&#xff0c;按住Alt键&#xff0c;连接到空对象位置处。。 三&#xff1a;CtrI导入素材快捷键。 四&a…

Elasticsearch基础学习-常用查询和基本的JavaAPI操作ES

关于ES数据库的和核心倒排索引的介绍 一、Elasticsearch概述简介关于全文检索引擎关系型数据库的全文检索功能缺点全文检索的应用场景Elasticsearch 应用案例 二、Elasticsearch学习准备安装下载关于es检索的核心-倒排索引正向索引&#xff08;forward index&#xff09;倒排索…

辅助驾驶功能开发-功能规范篇(16)-2-领航辅助系统NAP-自动变道-1

书接上回 2.3.4.自动变道 当车辆处于导航引导模式NOA功能时(即车辆横向控制功能激活),且车速大于40km/h,驾驶员按下转向灯拨杆或系统判断当前有变道需要时,自动变道系统通过对车道线、自车道前方目标距离、邻近车道前后方目标距离等环境条件进行判断,在转向灯亮起3s后控…

看到这个数据库设计,我终于明白了我和其他软测人的差距

测试人员为什么要懂数据库设计&#xff1f; 更精准的掌握业务&#xff0c;针对接口测试、Web 测试&#xff0c;都是依照项目/产品需求进行用例设计&#xff0c;如果掌握数据库设计知识&#xff0c;能直接面对开发的数据表&#xff0c;更好、更精准的理解业务逻辑&#xff1b;有…

【滑动窗口】滑窗模板,在小小的算法题里滑呀滑呀滑

首先大家先喊出我们的口号&#xff1a;跟着模板搞&#xff0c;滑窗没烦恼&#xff01; 一.什么是滑动窗口&#xff1f; 滑动窗口算法是双指针算法的一种特定化的算法模型&#xff0c;常用于在特定的条件下求最大或者最小的字符串&#xff0c;特定的数组&#xff0c;以及字符序列…

JAVA 可用的高性能docker镜像及如何使用?

目前docker hub上下载量很大的java、openjdk镜像都已经被弃用,不再维护,目前可用的java docker镜像有哪一些呢?哪一些镜像是主流的? 本文带有领略java可用的镜像资源、如何使用它们,如何构建springboot镜像? 1. 可用的java镜像 1.1. amazoncorretto 1.1.1. 什么是Corr…

环路详解:交换机环路产生的过程和原因图解

前言&#xff1a; 在了解环路之前得先了解交换机的工作原理&#xff0c;当然交换机的基本工作原理其实非常简单&#xff0c;只有“单播转发与泛洪转发”、“交换机MAC地址表”这两个&#xff01;其他的如vlan&#xff0c;生成树等也是在此基础上增加的&#xff0c;弥补交换机基…

node笔记_koa框架的路由

文章目录 ⭐前言⭐koa 原生路由写法⭐引入 koa-router&#x1f496; 安装koa-router&#x1f496; 动态读取路径文件作为路由 ⭐结束 ⭐前言 大家好&#xff0c;我是yma16&#xff0c;本文介绍koa框架的路由。 往期文章 node_windows环境变量配置 node_npm发布包 linux_配置no…

[网络安全]DVWA之XSS(Reflected)攻击姿势及解题详析合集

[网络安全]DVWA之XSS&#xff08;Reflected&#xff09;攻击姿势及解题详析合集 XSS(Reflected)-low level源代码姿势 XSS(Reflected)-medium level源代码姿势1.双写绕过2.大小写绕过 XSS(Reflected)-high level源代码str_replace函数 姿势 XSS(Reflected)-Impossible level源代…

ssh正反隧道(代理msf对icmp穿透监听)

ssh正向隧道&#xff1a; 就是将本地端口映射到远程上&#xff0c;相当访问本地端口就是访问远程的端口 正向 访问本地对应的是远程的端口 ssh -fNCL 本地ip:本地port:远程ip:远程port 用户远程ip/域名 实例&#xff1a; ssh -fNCL 192.168.222.128:90:192…

HTML的表单

前后端交互过程&#xff1a; 表单在 Web 网页中用来给访问者填写信息采集客户端信息&#xff0c;使网页具有交互的功能&#xff0c;用户填写完提交后&#xff0c;表单的内容就从客户端的浏览器传送到服务器上&#xff0c;经过服务器上程序处理后&#xff0c;再将用户所需信息传…

人机大战?——带你玩转三子棋(C语言)

TOC 1、前言 在学习完数组之后&#xff0c;我们就可以自己来实现一个简单游戏—三子棋了&#xff01; 为了确保程序的独立性&#xff1a;我们创建了一个源函数game.c 和test.c&#xff0c;一个头文件game.h test.c——测试游戏 game.c——游戏函数的实现 game.h——游戏函数…

Redis缓存数据库(三)

目录 一、概述 1、Redis架构 2、AKF 3、CAP原则 一、概述 1、Redis架构 Redis 有哪些架构模式&#xff1f;讲讲各自的特点 单机版 特点&#xff1a;简单 问题&#xff1a; 1、内存容量有限 2、处理能力有限 3、无法高可用。 主从复制 Redis 的复制&#xff08;replic…

python绘制散点图|散点大小和颜色深浅由数值决定

python绘图系列文章目录 往期python绘图合集: python绘制简单的折线图 python读取excel中数据并绘制多子图多组图在一张画布上 python绘制带误差棒的柱状图 python绘制多子图并单独显示 python读取excel数据并绘制多y轴图像 python绘制柱状图并美化|不同颜色填充柱子 python随机…

【嵌入式系统应用开发】FPGA——HLS入门实践之led灯闪烁

目录 1 HLS1.1 HLS简介1.2 HLS与VHDL/Verilog1.3 HLS优点与局限 2 环境配置3 HLS实例——Led点亮3.1 工程创建3.2 添加文件3.3 C仿真与C综合3.4 创建Vivado工程3.5 导入HLS生成的IP核3.6 添加实验代码3.7 编译生成获取结果 总结 1 HLS 1.1 HLS简介 HLS(High Level Synthesis)…

十大排序算法(上)直接插入排序、希尔排序、直接选择排序、堆排序

&#x1f308;目录 1. 排序的概念2. 常见的排序算法3. 排序算法的实现3.1 插入排序3.1.1 直接插入排序3.1.2 希尔排序&#xff08;缩小增量排序&#xff09; 3.2 选择排序3.2.1 基本思想3.2.2 直接选择排序3.2.3 堆排序 1. 排序的概念 排序&#xff0c;就是使一串记录&#xf…

阿里通义千问_VS_讯飞星火

今天终于获得阿里通义千问大模型体验授权&#xff0c;第一时间来测试一下效果&#xff0c;使用申请手机号登录&#xff08;地址&#xff1a;https://tongyi.aliyun.com&#xff09;后&#xff0c;需要同意通义千问大模型体验规则&#xff0c;如下图所示&#xff1a; 同意之后就…

【C++初阶】类与对象(中)之运算符重载 + 赋值运算符重载

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前学习C和算法 ✈️专栏&#xff1a;C航路 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你有帮助的话 欢迎 评论&#x1f4ac; 点赞&#x1…

CPU性能优化:Cache

CPU性能提升&#xff1a;Cache机制 随着半导体工艺和芯片设计技术的发展&#xff0c;CPU的工作频率也越来越高&#xff0c;和CPU进行频繁的数据交换的内存的运行速度却没有相应的提升&#xff0c;于是两者之间产生了带宽问题。进而影响计算机系统的整体性能。CPU执行一条指令需…

C++/PTA 至多删三个字符

至多删三个字符 题目要求解题思路代码总结 题目要求 给定一个全部由小写英文字母组成的字符串&#xff0c;允许你至多删掉其中 3 个字符&#xff0c;结果可能有多少种不同的字符串&#xff1f; 输入格式&#xff1a; 输入在一行中给出全部由小写英文字母组成的、长度在区间 […