文章目录
- 前言
- 一、对账系统构建
- 二、执行流程
- 三、获取支付渠道数据
- 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. 比对器
- 总结
前言
从《支付系统设计一:支付系统产品化》系列中支付网关系统设计我们知道,在对接支付渠道的时候只需要产品经理进行后台渠道相关信息的配置,以及开发人员编写的模板、脚本就能够完成支付渠道的对接了,同样的,通过此模式也可以完成支付系统和支付渠道的对账。
一、对账系统构建
对账系统构建在网关系统的基础上,将网关应用单独部署几个节点,作为对账系统。
需要解决的问题有:
- 作为对账的网关节点不能向注册中心注册服务,对账节点只是用来完成对账功能。
项目有两个主启动类包,demo-reconmain依赖demo-main,其中通过demo-reconmain主启动类启动需要配置:
// 对账节点不注册服务
System.setProperty("eureka.client.register-with-eureka", "false");
- 作为处理交易的网关节点不能消费对账任务,网关节点只是用来完成交易功能。
关于这块,见以前写的篇博客《多机房控制消息消费方实现》
即通过此设计,支付网关系统又可以作为对账系统,可以很巧妙的解决支付渠道侧对账数据获取的问题。
二、执行流程
三、获取支付渠道数据
不同的支付渠道获取对账文件的方式也各不相同,有的是推送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();
}
总结
后文详细展开具体实现。