feat 云闪付对账处理

This commit is contained in:
xxm1995
2024-03-24 23:18:05 +08:00
committed by 喵呀
parent 6057802c45
commit 68334cd0dc
7 changed files with 277 additions and 24 deletions

View File

@@ -4,13 +4,9 @@
- [x] 第一排: (数字格式)显示今日收入、支出金额,支付总订单数量、退款总订单数, 时间分支分为: 今日金额/昨日金额/七天内金额
- [x] 第二排: (饼图)显示各通道各支付方式数量和占比, 时间分为: 今日金额/昨日金额/七天内金额
- [x] 第三排: (折线图)显示各通道支付分为支付金额和退款,时间分为: 今日金额/昨日金额/七天内金额
- [ ] 报表功能
- [ ] 各通道收入和支付情况
- [ ] 增加转账功能
- [ ] 支付宝
- [ ] 微信
- [ ] 云闪付
- [ ] 增加账户余额查询功能
- [ ] 云闪付支持对账功能
- [x] 结算台DEMO增加云闪付示例
- [x] 增加支付限额
@@ -19,6 +15,7 @@
2.0.x 版本内容
- [ ] 微信新增V3版本接口
- [ ] 付款码支付自动路由到V2接口
- [ ] 资金流水优化
- [ ] 统一关闭接口增加使用撤销关闭订单
- [ ] 支持分账
- [ ] 增加各类日志记录,例如钱包的各项操作

View File

@@ -53,9 +53,19 @@ public interface UnionPayCode {
/** 总金额 */
String TOTAL_FEE = "settleAmt";
/** 对账单下载类型编码 */
String RECONCILE_BILL_TYPE = "00";
/* 对账单交易代码 */
/** 消费 */
String RECONCILE_TYPE_PAY = "S22";
/** 退款 */
String RECONCILE_TYPE_REFUND = "S30 ";
/** 对账各字段位数游标 */
int[] RECONCILE_BILL_SPLIT = {3,11,11,6,10,19,12,4,2,21,2,32,2,6,10,13,13,4,15,2,2,6,2,4,32,1,21,15,1,15,32,13,13,8,32,13,13,12,2,1,32,13,2,1,12,67};
}

View File

@@ -0,0 +1,44 @@
package cn.bootx.platform.daxpay.service.code;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 云闪付对账单字段
* @author xxm
* @since 2024/3/24
*/
@Getter
@AllArgsConstructor
public enum UnionReconcileFieldEnum {
/** 交易代码 */
TRADE_TYPE(0, "tradeType"),
/** 交易传输时间 MMDDhhmmss */
txnTime(4, "txnTime"),
/** 交易金额 */
TXN_AMT(6, "txnAmt"),
/** 查询流水号 */
QUERY_ID(9, "queryId"),
/** 商户订单号 */
ORDER_ID(11, "orderId");
/** 序号 */
private final int no;
/** 字段名 */
private final String filed;
/**
* 根据序号查询
*/
public static UnionReconcileFieldEnum findByNo(int no){
return Arrays.stream(values())
.filter(o->o.no == no)
.findFirst()
.orElse(null);
}
}

View File

@@ -0,0 +1,70 @@
package cn.bootx.platform.daxpay.service.core.channel.union.entity;
import cn.bootx.table.modify.annotation.DbColumn;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 云闪付业务明细对账单
* @author xxm
* @since 2024/3/24
*/
@Data
@Accessors(chain = true)
public class UnionReconcileBillDetail {
/** 关联对账订单ID */
@DbColumn(comment = "关联对账订单ID")
private Long recordOrderId;
/** 交易代码 */
private String tradeType;
/** 代理机构标识码 */
/** 发送机构标识码 */
/** 系统跟踪号 */
/** 交易传输时间 */
private String txnTime;
/** 帐号 */
/** 交易金额 */
private String txnAmt;
/** 商户类别 */
/** 终端类型 */
/** 查询流水号 */
private String queryId;
/** 支付方式(旧) */
/** 商户订单号 */
private String orderId;
/** 支付卡类型 */
/** 原始交易的系统跟踪号 */
/** 原始交易日期时间 */
/** 商户手续费 */
/** 结算金额 */
/** 支付方式 */
/** 集团商户代码 */
/** 交易类型 */
/** 交易子类 */
/** 业务类型 */
/** 帐号类型 */
/** 账单类型 */
/** 账单号码 */
/** 交互方式 */
/** 商户代码 */
/** 分账入账方式 */
/** 二级商户代码 */
/** 二级商户简称 */
/** 二级商户分账入账金额 */
/** 清算净额 */
/** 终端号 */
/** 商户自定义域 */
/** 优惠金额 */
/** 发票金额 */
/** 分期付款附加手续费 */
/** 分期付款期数 */
/** 交易介质 */
/** 原始交易订单号 */
/** 清算金额 */
/** 服务点输入方式码 */
/** 移动支付产品标志 */
/** 交易代码 */
/** 检索参考号 */
/** 保留使用 */
}

View File

@@ -1,13 +1,31 @@
package cn.bootx.platform.daxpay.service.core.channel.union.service;
import cn.bootx.platform.common.core.util.LocalDateTimeUtil;
import cn.bootx.platform.daxpay.code.ReconcileTradeEnum;
import cn.bootx.platform.daxpay.service.code.UnionPayCode;
import cn.bootx.platform.daxpay.service.code.UnionReconcileFieldEnum;
import cn.bootx.platform.daxpay.service.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.service.core.channel.union.entity.UnionReconcileBillDetail;
import cn.bootx.platform.daxpay.service.core.order.reconcile.entity.ReconcileDetail;
import cn.bootx.platform.daxpay.service.sdk.union.api.UnionPayKit;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.compress.Deflate;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.Map;
import java.io.*;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import static cn.bootx.platform.daxpay.service.code.UnionPayCode.RECONCILE_BILL_SPLIT;
import static cn.bootx.platform.daxpay.service.code.UnionPayCode.RECONCILE_BILL_TYPE;
/**
@@ -26,12 +44,127 @@ public class UnionPayReconcileService {
public void downAndSave(Date date, Long recordOrderId, UnionPayKit unionPayKit){
// 下载对账单
Map<String, Object> stringObjectMap = unionPayKit.downloadBill(date, RECONCILE_BILL_TYPE);
String fileContent = stringObjectMap.get("fileContent").toString();
try {
// 先解base64再DEFLATE解压为zip流
byte[] decode = Base64.decode(fileContent);
ByteArrayOutputStream out = new ByteArrayOutputStream();
Deflate deflate = Deflate.of(new ByteArrayInputStream(decode), out, false);
deflate.inflater();
deflate.close();
// 读取zip文件, 解析出对账单内容
byte[] zipBytes = out.toByteArray();
ZipArchiveInputStream zipArchiveInputStream = new ZipArchiveInputStream(new ByteArrayInputStream(zipBytes),"GBK");
ZipArchiveEntry entry;
List<UnionReconcileBillDetail> billDetails = new ArrayList<>();
while ((entry= zipArchiveInputStream.getNextZipEntry()) != null){
System.out.println(StrUtil.startWith(entry.getName(), "INN"));
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(zipArchiveInputStream,"GBK"));
if (StrUtil.startWith(entry.getName(), "INN")){
// 明细解析
List<String> strings = IoUtil.readLines(bufferedReader, new ArrayList<>());
billDetails = this.parseDetail(strings);
} else {
// 汇总目前不进行处理
}
}
// 保存原始对账记录
this.save(billDetails, recordOrderId);
// 将原始交易明细对账记录转换通用结构并保存到上下文中
this.convertAndSave(billDetails);
// Reader bufferedReader = new BufferedReader(new InputStreamReader(Files.newInputStream(Paths.get("D:/data/INN24031100ZM_777290058206553.txt"))));
// List<String> strings = IoUtil.readLines(bufferedReader, new ArrayList<>());
// List<UnionReconcileBillDetail> unionReconcileBillDetails = this.parseDetail(strings);
// System.out.println(unionReconcileBillDetails);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 转换和保存
* 解析对账单明细
*/
public void convertAndSave(){
private List<UnionReconcileBillDetail> parseDetail(List<String> list){
return list.stream()
.map(this::convertDetail)
.collect(Collectors.toList());
}
/**
* 解析明细条目
*/
private UnionReconcileBillDetail convertDetail(String line){
//解析的结果MAPkey为对账文件列序号value为解析的值
Map<String,String> zmDataMap = new HashMap<>();
//左侧游标
int leftIndex = 0;
//右侧游标
int rightIndex = 0;
for(int i=0;i<RECONCILE_BILL_SPLIT.length;i++){
rightIndex = leftIndex + RECONCILE_BILL_SPLIT[i];
String filed = StrUtil.sub(line, leftIndex, rightIndex);
leftIndex = rightIndex+1;
UnionReconcileFieldEnum fieldEnum = UnionReconcileFieldEnum.findByNo(i);
if (Objects.nonNull(fieldEnum)){
zmDataMap.put(fieldEnum.getFiled(), filed.trim());
}
}
return BeanUtil.toBean(zmDataMap, UnionReconcileBillDetail.class);
}
/**
* 转换为通用对账记录对象
*/
private void convertAndSave(List<UnionReconcileBillDetail> billDetails){
List<ReconcileDetail> collect = billDetails.stream()
.map(this::convert)
.collect(Collectors.toList());
// 写入到上下文中
PaymentContextLocal.get().getReconcileInfo().setReconcileDetails(collect);
}
/**
* 转换为通用对账记录对象
*/
private ReconcileDetail convert(UnionReconcileBillDetail billDetail){
// 金额
String orderAmount = billDetail.getTxnAmt();
int amount = Integer.parseInt(orderAmount);
// 默认为支付对账记录
ReconcileDetail reconcileDetail = new ReconcileDetail()
.setRecordOrderId(billDetail.getRecordOrderId())
.setOrderId(billDetail.getOrderId())
.setType(ReconcileTradeEnum.PAY.getCode())
.setAmount(amount)
.setGatewayOrderNo(billDetail.getQueryId());
// 时间
String txnTime = billDetail.getTxnTime();
if (StrUtil.isNotBlank(txnTime)) {
LocalDateTime time = LocalDateTimeUtil.parse(txnTime, DatePattern.NORM_DATETIME_PATTERN);
reconcileDetail.setOrderTime(time);
}
// 退款覆盖更新对应的字段
if (Objects.equals(billDetail.getTradeType(), UnionPayCode.RECONCILE_TYPE_REFUND)){
reconcileDetail.setType(ReconcileTradeEnum.REFUND.getCode());
}
return reconcileDetail;
}
/**
* 保存原始对账记录
*/
private void save(List<UnionReconcileBillDetail> billDetails, Long recordOrderId){
billDetails.forEach(o->o.setRecordOrderId(recordOrderId));
}
}

View File

@@ -1,9 +1,9 @@
package cn.bootx.platform.daxpay.service.core.payment.reconcile.strategy;
import cn.bootx.platform.common.core.exception.BizException;
import cn.bootx.platform.common.core.util.LocalDateTimeUtil;
import cn.bootx.platform.common.sequence.func.Sequence;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.service.core.channel.union.convert.UnionPayConvert;
import cn.bootx.platform.daxpay.service.core.channel.union.dao.UnionPayRecordManager;
import cn.bootx.platform.daxpay.service.core.channel.union.entity.UnionPayConfig;
@@ -68,6 +68,10 @@ public class UnionPayReconcileStrategy extends AbsReconcileStrategy {
@Override
public void doBeforeHandler() {
UnionPayConfig config = configService.getConfig();
// 测试环境使用测试号
if (config.isSandbox()) {
config.setMachId("700000000000001");
}
this.unionPayKit = configService.initPayService(config);
}
@@ -76,12 +80,9 @@ public class UnionPayReconcileStrategy extends AbsReconcileStrategy {
*/
@Override
public void downAndSave() {
if (true){
throw new PayFailureException("功能暂时未实现");
}
Date date = DateUtil.date(this.getRecordOrder().getDate());
reconcileService.downAndSave(date, this.getRecordOrder().getId(), this.unionPayKit);
throw new BizException("123");
}
/**

View File

@@ -720,15 +720,13 @@ public class UnionPayKit extends UnionPayService {
this.setSign(params);
String responseStr = getHttpRequestTemplate().postForObject(this.getFileTransUrl(), params, String.class);
JSONObject response = UriVariables.getParametersToMap(responseStr);
if (this.verify(response)) {
if (SDKConstants.OK_RESP_CODE.equals(response.get(SDKConstants.param_respCode))) {
// if (this.verify(response)) {
// if (SDKConstants.OK_RESP_CODE.equals(response.get(SDKConstants.param_respCode))) {
return response;
}
throw new PayErrorException(new PayException(response.get(SDKConstants.param_respCode).toString(), response.get(SDKConstants.param_respMsg).toString(), response.toString()));
}
throw new PayErrorException(new PayException("failure", "验证签名失败", response.toString()));
// }
// throw new PayErrorException(new PayException(response.get(SDKConstants.param_respCode).toString(), response.get(SDKConstants.param_respMsg).toString(), response.toString()));
// }
// throw new PayErrorException(new PayException("failure", "验证签名失败", response.toString()));
}