ref 删除对账功能

This commit is contained in:
bootx
2025-05-05 15:59:36 +08:00
parent fe3cde8cbb
commit 6e084ede35
54 changed files with 0 additions and 3584 deletions

View File

@@ -1,64 +0,0 @@
package org.dromara.daxpay.channel.alipay.bo;
import cn.hutool.core.annotation.Alias;
import lombok.Data;
/**
* 支付宝业务明细对账单解析类
* @author xxm
* @since 2024/1/18
*/
@Data
public class AlipayReconcileBillDetail {
@Alias("支付宝交易号")
private String tradeNo;
@Alias("商户订单号")
private String outTradeNo;
@Alias("业务类型")
private String tradeType;
@Alias("商品名称")
private String subject;
@Alias("创建时间")
private String createTime;
/** yyyy-MM-dd HH:mm:ss */
@Alias("完成时间")
private String endTime;
@Alias("门店编号")
private String storeId;
@Alias("门店名称")
private String storeName;
@Alias("操作员")
private String operator;
@Alias("终端号")
private String terminalId;
@Alias("对方账户")
private String otherAccount;
@Alias("订单金额(元)")
private String orderAmount;
@Alias("商家实收(元)")
private String realAmount;
@Alias("支付宝红包(元)")
private String alipayAmount;
@Alias("集分宝(元)")
private String jfbAmount;
@Alias("支付宝优惠(元)")
private String alipayDiscountAmount;
@Alias("商家优惠(元)")
private String discountAmount;
@Alias("券核销金额(元)")
private String couponDiscountAmount;
@Alias("券名称")
private String couponName;
@Alias("商家红包消费金额(元)")
private String couponAmount;
@Alias("卡消费金额(元)")
private String cardAmount;
@Alias("退款批次号/请求号")
private String batchNo;
@Alias("服务费(元)")
private String serviceAmount;
@Alias("分润(元)")
private String splitAmount;
@Alias("备注")
private String remark;
}

View File

@@ -1,43 +0,0 @@
package org.dromara.daxpay.channel.alipay.bo;
import cn.bootx.platform.common.mybatisplus.base.MpIdEntity;
import cn.hutool.core.annotation.Alias;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 支付宝业务汇总对账单解析类
* @author xxm
* @since 2024/1/17
*/
@EqualsAndHashCode(callSuper = true)
@Data
@TableName("pay_alipay_reconcile_bill_total")
public class AlipayReconcileBillTotal extends MpIdEntity {
@Alias("门店编号")
private String storeId;
@Alias("门店名称")
private String storeName;
@Alias("交易订单总笔数")
private String totalNum;
@Alias("退款订单总笔数")
private String totalRefundNum;
@Alias("订单金额(元)")
private String totalOrderAmount;
@Alias("商家实收(元)")
private String totalAmount;
@Alias("支付宝优惠(元)")
private String totalDiscountAmount;
@Alias("商家优惠(元)")
private String totalCouponAmount;
// 卡消费金额(元) 服务费(元) 分润(元) 实收净额(元)
@Alias("卡消费金额(元)")
private String totalConsumeAmount;
@Alias("服务费(元)")
private String totalServiceAmount;
@Alias("分润(元)")
private String totalShareAmount;
@Alias("实收净额(元)")
private String totalNetAmount;
}

View File

@@ -1,223 +0,0 @@
package org.dromara.daxpay.channel.alipay.service.payment.reconcile;
import org.dromara.daxpay.channel.alipay.bo.AlipayReconcileBillDetail;
import org.dromara.daxpay.channel.alipay.bo.AlipayReconcileBillTotal;
import org.dromara.daxpay.channel.alipay.entity.config.AliPayConfig;
import org.dromara.daxpay.channel.alipay.service.payment.config.AlipayConfigService;
import org.dromara.daxpay.core.enums.TradeStatusEnum;
import org.dromara.daxpay.core.enums.TradeTypeEnum;
import org.dromara.daxpay.core.exception.OperationFailException;
import org.dromara.daxpay.core.exception.ReconciliationFailException;
import org.dromara.daxpay.service.bo.reconcile.ChannelReconcileTradeBo;
import org.dromara.daxpay.service.bo.reconcile.ReconcileResolveResultBo;
import org.dromara.daxpay.service.entity.reconcile.ReconcileStatement;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.text.csv.CsvReader;
import cn.hutool.core.text.csv.CsvUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConstants;
import com.alipay.api.domain.AlipayDataDataserviceBillDownloadurlQueryModel;
import com.alipay.api.request.AlipayDataDataserviceBillDownloadurlQueryRequest;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.dromara.x.file.storage.core.FileInfo;
import org.dromara.x.file.storage.core.FileStorageService;
import org.dromara.x.file.storage.core.upload.UploadPretreatment;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 支付宝对账服务
* @author xxm
* @since 2024/1/17
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AlipayReconcileService {
private final AlipayConfigService aliPayConfigService;
private final FileStorageService fileStorageService;
/**
* 下载对账单, 并进行解析
*
* @param date 对账日期 yyyy-MM-dd 格式
* @param statement 对账单对象
*/
@SneakyThrows
@Transactional(rollbackFor = Exception.class)
public ReconcileResolveResultBo downAndResolve(String date, ReconcileStatement statement, AliPayConfig aliPayConfig){
try {
var model = new AlipayDataDataserviceBillDownloadurlQueryModel();
model.setBillDate(date);
// 下载交易类型
model.setBillType("trade");
var request = new AlipayDataDataserviceBillDownloadurlQueryRequest();
// 特约商户调用
if (aliPayConfig.isIsv()){
request.putOtherTextParam(AlipayConstants.APP_AUTH_TOKEN, aliPayConfig.getAppAuthToken());
}
request.setBizModel(model);
var response = aliPayConfigService.execute(request,aliPayConfig);
// 判断返回结果
if (!response.isSuccess()) {
log.error("获取支付宝对账单失败: {}", response.getSubMsg());
throw new ReconciliationFailException(response.getSubMsg());
}
// 获取对账单下载地址并下载
String url = response.getBillDownloadUrl();
byte[] zipBytes = HttpUtil.downloadBytes(url);
// 使用 Apache commons-compress 包装流, 读取返回的对账CSV文件
ZipArchiveInputStream zipArchiveInputStream = new ZipArchiveInputStream(new ByteArrayInputStream(zipBytes), "GBK");
ZipArchiveEntry entry;
List<AlipayReconcileBillDetail> billDetails = new ArrayList<>();
byte[] bytes = null;
while ((entry = zipArchiveInputStream.getNextZipEntry()) != null) {
String name = entry.getName();
// 只处理明细
if (!StrUtil.endWith(name, "_业务明细(汇总).csv")) {
bytes = IoUtil.readBytes(zipArchiveInputStream);
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes), "GBK"));
List<String> strings = IoUtil.readLines(bufferedReader, new ArrayList<>());
billDetails = this.parseDetail(strings);
}
}
// 保存原始对账文件
String originalFile = this.saveOriginalFile(statement, bytes);
// 将原始交易明细对账记录转换通用结构
var reconcileTradeBos = this.convertReconcileTrade(billDetails);
return new ReconcileResolveResultBo()
.setChannelTrades(reconcileTradeBos)
.setOriginalFileUrl(originalFile);
} catch (AlipayApiException e) {
log.error("下载对账单失败",e);
throw new OperationFailException("下载对账单失败: "+e.getMessage());
}
}
/**
* 上传对账单解析并保存
*/
@SneakyThrows
public ReconcileResolveResultBo upload(ReconcileStatement statement, byte[] bytes) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes),"GBK"));
List<String> strings = IoUtil.readLines(bufferedReader, new ArrayList<>());
List<AlipayReconcileBillDetail> billDetails = this.parseDetail(strings);
// 保存原始对账文件
String originalFile = this.saveOriginalFile(statement, bytes);
// 将原始交易明细对账记录转换通用结构
var reconcileTradeBos = this.convertReconcileTrade(billDetails);
return new ReconcileResolveResultBo()
.setChannelTrades(reconcileTradeBos)
.setOriginalFileUrl(originalFile);
}
/**
* 转换为通用对账记录对象
*/
private List<ChannelReconcileTradeBo> convertReconcileTrade(List<AlipayReconcileBillDetail> billDetails){
return billDetails.stream()
.map(this::convert)
.toList();
}
/**
* 转换为通用对账记录对象
*/
private ChannelReconcileTradeBo convert(AlipayReconcileBillDetail billDetail){
// 金额
var amount = new BigDecimal(billDetail.getOrderAmount());
// 默认为支付对账记录
ChannelReconcileTradeBo reconcileTradeBo = new ChannelReconcileTradeBo()
.setPlatformTradeNo(billDetail.getOutTradeNo())
.setTradeType(TradeTypeEnum.PAY.getCode())
.setAmount(amount)
.setTradeStatus(TradeStatusEnum.SUCCESS.getCode())
.setChannelTradeNo(billDetail.getTradeNo());
// 时间
String endTime = billDetail.getEndTime();
if (StrUtil.isNotBlank(endTime)) {
LocalDateTime time = LocalDateTimeUtil.parse(endTime, DatePattern.NORM_DATETIME_PATTERN);
reconcileTradeBo.setTradeTime(time);
}
// 退款覆盖更新对应的字段
if (Objects.equals(billDetail.getTradeType(), "退款")){
reconcileTradeBo.setPlatformTradeNo(billDetail.getBatchNo())
.setTradeType(TradeTypeEnum.REFUND.getCode());
}
return reconcileTradeBo;
}
/**
* 解析明细
*/
private List<AlipayReconcileBillDetail> parseDetail(List<String> list){
// 截取需要进行解析的文本内容
String billDetail = list.stream()
.collect(Collectors.joining(System.lineSeparator()));
billDetail = StrUtil.subBetween(billDetail,
"#-----------------------------------------业务明细列表----------------------------------------"+System.lineSeparator(),
"#-----------------------------------------业务明细列表结束------------------------------------");
billDetail = billDetail.replaceAll("\t", "");
CsvReader reader = CsvUtil.getReader();
return reader.read(billDetail, AlipayReconcileBillDetail.class);
}
/**
* 解析汇总
*/
private List<AlipayReconcileBillTotal> parseTotal(List<String> list){
// 去除前 4 行和后 2 行 然后合并是个一个字符串
String billTotal = list.stream()
.collect(Collectors.joining(System.lineSeparator()));
billTotal = StrUtil.subBetween(billTotal,
"#-----------------------------------------业务汇总列表----------------------------------------"+System.lineSeparator(),
"#----------------------------------------业务汇总列表结束-------------------------------------");
billTotal = billTotal.replaceAll("\t", "");
CsvReader reader = CsvUtil.getReader();
return reader.read(billTotal, AlipayReconcileBillTotal.class);
}
/**
* 保存下载的原始对账文件
*/
private String saveOriginalFile(ReconcileStatement statement, byte[] bytes) {
// 将原始文件进行保存
String date = LocalDateTimeUtil.format(statement.getDate(), DatePattern.PURE_DATE_PATTERN);
// 将原始文件进行保存 通道-日期
String fileName = StrUtil.format("交易对账单-支付宝-{}.csv",date);
UploadPretreatment uploadPretreatment = fileStorageService.of(bytes);
if (StrUtil.isNotBlank(fileName)) {
uploadPretreatment.setOriginalFilename(fileName);
}
FileInfo upload = uploadPretreatment.upload();
return upload.getUrl();
}
}

View File

@@ -1,65 +0,0 @@
package org.dromara.daxpay.channel.alipay.strategy.merchant;
import org.dromara.daxpay.channel.alipay.entity.config.AliPayConfig;
import org.dromara.daxpay.channel.alipay.service.payment.config.AlipayConfigService;
import org.dromara.daxpay.channel.alipay.service.payment.reconcile.AlipayReconcileService;
import org.dromara.daxpay.core.enums.ChannelEnum;
import org.dromara.daxpay.service.bo.reconcile.ReconcileResolveResultBo;
import org.dromara.daxpay.service.enums.ReconcileFileTypeEnum;
import org.dromara.daxpay.service.strategy.AbsReconcileStrategy;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/**
* 支付宝对账策略
* @author xxm
* @since 2024/1/17
*/
@Slf4j
@Service
@Scope(SCOPE_PROTOTYPE)
@RequiredArgsConstructor
public class AlipayReconcileStrategy extends AbsReconcileStrategy {
private final AlipayReconcileService reconcileService;
private final AlipayConfigService aliPayConfigService;
/**
* 策略标识, 可以自行进行扩展
*
* @see ChannelEnum
*/
@Override
public String getChannel() {
return ChannelEnum.ALIPAY.getCode();
}
/**
* 上传对账单解析并保存
*
*/
@SneakyThrows
@Override
public ReconcileResolveResultBo uploadAndResolve(MultipartFile file, ReconcileFileTypeEnum fileType) {
return reconcileService.upload(this.getStatement(), file.getBytes());
}
/**
* 下载对账单到本地进行保存
*/
@Override
public ReconcileResolveResultBo downAndResolve() {
AliPayConfig aliPayConfig = aliPayConfigService.getAliPayConfig(false);
String date = LocalDateTimeUtil.format(this.getStatement().getDate(), DatePattern.NORM_DATE_PATTERN);
return reconcileService.downAndResolve(date, this.getStatement(),aliPayConfig);
}
}

View File

@@ -1,64 +0,0 @@
package org.dromara.daxpay.channel.alipay.strategy.sub;
import org.dromara.daxpay.channel.alipay.entity.config.AliPayConfig;
import org.dromara.daxpay.channel.alipay.service.payment.config.AlipayConfigService;
import org.dromara.daxpay.channel.alipay.service.payment.reconcile.AlipayReconcileService;
import org.dromara.daxpay.core.enums.ChannelEnum;
import org.dromara.daxpay.service.bo.reconcile.ReconcileResolveResultBo;
import org.dromara.daxpay.service.enums.ReconcileFileTypeEnum;
import org.dromara.daxpay.service.strategy.AbsReconcileStrategy;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/**
* 支付宝对账策略
* @author xxm
* @since 2024/1/17
*/
@Slf4j
@Service
@Scope(SCOPE_PROTOTYPE)
@RequiredArgsConstructor
public class AlipaySubReconcileStrategy extends AbsReconcileStrategy {
private final AlipayReconcileService reconcileService;
private final AlipayConfigService aliPayConfigService;
/**
* 策略标识, 可以自行进行扩展
*
* @see ChannelEnum
*/
@Override
public String getChannel() {
return ChannelEnum.ALIPAY_ISV.getCode();
}
/**
* 上传对账单解析并保存
*
*/
@SneakyThrows
@Override
public ReconcileResolveResultBo uploadAndResolve(MultipartFile file, ReconcileFileTypeEnum fileType) {
return reconcileService.upload(this.getStatement(), file.getBytes());
}
/**
* 下载对账单到本地进行保存
*/
@Override
public ReconcileResolveResultBo downAndResolve() {
AliPayConfig aliPayConfig = aliPayConfigService.getAliPayConfig(true);
String date = LocalDateTimeUtil.format(this.getStatement().getDate(), DatePattern.NORM_DATE_PATTERN);
return reconcileService.downAndResolve(date, this.getStatement(),aliPayConfig);
}
}

View File

@@ -1,151 +0,0 @@
package org.dromara.daxpay.channel.wechat.entity.reconcile;
import cn.hutool.core.annotation.Alias;
import cn.hutool.core.util.StrUtil;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
/**
* 微信交易对账解析文件
* @author xxm
* @since 2024/8/11
*/
@Slf4j
@Data
@Accessors(chain = true)
public class WechatReconcileBillDetail {
// 交易时间
@Alias("交易时间")
private String transactionTime;
// 公众账号ID
@Alias("公众账号ID")
private String appid;
// 商户号
@Alias("商户号")
private String mchid;
// 特约商户号
@Alias("特约商户号")
private String subMchid;
// 设备号
@Alias("设备号")
private String deviceInfo;
// 微信订单号
@Alias("微信订单号")
private String transactionId;
// 商户订单号
@Alias("商户订单号")
private String outTradeNo;
// 用户标识
@Alias("用户标识")
private String openid;
// 交易类型 JSAPI/NATIVE
@Alias("交易类型")
private String tradeType;
// 交易状态
@Alias("交易状态")
private String tradeState;
// 付款银行
@Alias("付款银行")
private String bankType;
// 货币种类
@Alias("货币种类")
private String feeType;
// 应结订单金额
@Alias("应结订单金额")
private String settlementTotalFee;
// 代金券金额
@Alias("代金券金额")
private String couponFee;
// 微信退款单号
@Alias("微信退款单号")
private String refundId;
// 商户退款单号
@Alias("商户退款单号")
private String outRefundNo;
/**
* 退款金额
* 该笔退款单参与计费的应结算金额(申请退款金额-免充值券退款金额),
* 如果该行数据为订单则展示为0.00非负数、单位元保留到小数点后2位
*/
@Alias("退款金额")
private String refundFee;
// 充值券退款金额
@Alias("充值券退款金额")
private String couponRefundFee;
// 退款类型
@Alias("退款类型")
private String refundType;
// 退款状态
@Alias("退款状态")
private String refundStatus;
// 商品名称
@Alias("商品名称")
private String body;
// 商户数据包
@Alias("商户数据包")
private String attach;
// 手续费
@Alias("手续费")
private String fee;
// 费率
@Alias("费率")
private String feeRate;
// 订单金额
@Alias("订单金额")
private String totalFee;
// 申请退款金额
@Alias("申请退款金额")
private String applyRefundFee;
// 费率备注
@Alias("费率备注")
private String feeRemark;
/**
* 去除前缀的 ` 符号
*/
public void removeStartSymbol() {
Field[] fields = this.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.getType() == String.class) {
field.setAccessible(true);
try {
String value = (String) field.get(this);
if (StrUtil.startWith(value, "`")) {
field.set(this, StrUtil.replaceFirst(value, "`", ""));
}
} catch (IllegalAccessException e) {
log.warn("去除前缀错误错误", e);
}
}
}
}
}

View File

@@ -1,169 +0,0 @@
package org.dromara.daxpay.channel.wechat.service.payment.reconcile;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.text.csv.CsvReader;
import cn.hutool.core.text.csv.CsvUtil;
import cn.hutool.core.util.StrUtil;
import com.github.binarywang.wxpay.bean.request.WxPayApplyTradeBillV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayApplyBillV3Result;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.dromara.daxpay.channel.wechat.entity.config.WechatPayConfig;
import org.dromara.daxpay.channel.wechat.entity.reconcile.WechatReconcileBillDetail;
import org.dromara.daxpay.channel.wechat.service.payment.config.WechatPayConfigService;
import org.dromara.daxpay.core.enums.TradeStatusEnum;
import org.dromara.daxpay.core.enums.TradeTypeEnum;
import org.dromara.daxpay.core.exception.OperationFailException;
import org.dromara.daxpay.service.bo.reconcile.ChannelReconcileTradeBo;
import org.dromara.daxpay.service.bo.reconcile.ReconcileResolveResultBo;
import org.dromara.daxpay.service.entity.reconcile.ReconcileStatement;
import org.dromara.x.file.storage.core.FileInfo;
import org.dromara.x.file.storage.core.FileStorageService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
/**
* 微信支付对账
* @author xxm
* @since 2024/1/17
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatReconcileService {
private final FileStorageService fileStorageService;
private final WechatPayConfigService wechatPayConfigService;
/**
* 下载对账单并保存
*
* @param statement 对账订单
* @param date 对账日期 yyyyMMdd 格式
*/
@SneakyThrows
@Transactional(rollbackFor = Exception.class)
public ReconcileResolveResultBo downAndResolve(ReconcileStatement statement, String date) {
WechatPayConfig config = wechatPayConfigService.getAndCheckConfig(false);
WxPayService wxPayService = wechatPayConfigService.wxJavaSdk(config);
var request = new WxPayApplyTradeBillV3Request();
request.setBillDate(date);
request.setBillType("ALL");
try {
WxPayApplyBillV3Result wxPayApplyBillV3Result = wxPayService.applyTradeBill(request);
String downloadUrl = wxPayApplyBillV3Result.getDownloadUrl();
byte[] bytes;
try (InputStream inputStream = wxPayService.downloadBill(downloadUrl)) {
bytes = IoUtil.readBytes(inputStream);
}
BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes)));
String result = IoUtil.read(reader);
// 过滤特殊字符
result = result.replaceAll("`", "").replaceAll("\uFEFF", "");
CsvReader csvRows = CsvUtil.getReader();
// 获取交易记录并保存 同时过滤出当前应用的交易记录
String billDetail = StrUtil.subBefore(result, "总交易单数", false);
var billDetails = csvRows.read(billDetail, WechatReconcileBillDetail.class).stream()
// 只读取当前商户的订单
.filter(o->Objects.equals(o.getAppid(), config.getWxAppId()))
// 只读取对账日的记录
.filter(o->{
String transactionTime = o.getTransactionTime();
LocalDateTime time = LocalDateTimeUtil.parse(transactionTime, DatePattern.NORM_DATETIME_PATTERN);
return Objects.equals(statement.getDate(), LocalDate.from(time));
})
.toList();
// 保存原始对账文件
String originalFile = this.saveOriginalFile(statement, bytes);
// 解析账单文件
var tradeBos = this.convertReconcileTrade(billDetails);
return new ReconcileResolveResultBo()
.setOriginalFileUrl(originalFile)
.setChannelTrades(tradeBos);
} catch (WxPayException | IOException e) {
log.error("下载对账单失败", e);
throw new OperationFailException("下载对账单失败");
}
}
/**
* 转换为通用对账记录对象
*/
private List<ChannelReconcileTradeBo> convertReconcileTrade(List<WechatReconcileBillDetail> billDetails){
return billDetails.stream()
.map(this::convert)
.toList();
}
/**
* 转换为通用对账记录对象
*/
private ChannelReconcileTradeBo convert(WechatReconcileBillDetail billDetail){
// 交易时间时间 指该笔交易的支付成功时间或发起退款成功时间(注:不是退款成功时间)
LocalDateTime time = LocalDateTimeUtil.parse(billDetail.getTransactionTime(), DatePattern.NORM_DATETIME_PATTERN);
// 默认为支付对账记录
ChannelReconcileTradeBo tradeBo = new ChannelReconcileTradeBo()
.setPlatformTradeNo(billDetail.getOutTradeNo())
.setTradeType(TradeTypeEnum.PAY.getCode())
.setTradeTime(time)
.setChannelTradeNo(billDetail.getTransactionId());
if (Objects.equals(billDetail.getTradeState(), WxPayConstants.WxpayTradeStatus.SUCCESS)) {
tradeBo.setTradeType(TradeTypeEnum.PAY.getCode())
.setAmount(new BigDecimal(billDetail.getTotalFee()))
.setTradeStatus(TradeStatusEnum.SUCCESS.getCode());
}
// 退款覆盖更新对应的字段
if (Objects.equals(billDetail.getTradeState(), WxPayConstants.WxpayTradeStatus.REFUND)) {
tradeBo.setPlatformTradeNo(billDetail.getOutRefundNo())
.setChannelTradeNo(billDetail.getRefundId())
.setAmount(new BigDecimal(billDetail.getRefundFee()))
.setTradeType(TradeTypeEnum.REFUND.getCode());
tradeBo.setTradeType(TradeTypeEnum.REFUND.getCode());
// 状态
switch (billDetail.getRefundStatus()) {
case WxPayConstants.RefundStatus.SUCCESS -> tradeBo.setTradeStatus(TradeStatusEnum.SUCCESS.getCode());
case WxPayConstants.RefundStatus.PROCESSING -> tradeBo.setTradeStatus(TradeStatusEnum.CLOSED.getCode());
case WxPayConstants.ResultCode.FAIL -> tradeBo.setTradeStatus(TradeStatusEnum.FAIL.getCode());
case WxPayConstants.RefundStatus.CHANGE -> tradeBo.setTradeStatus(TradeStatusEnum.EXCEPTION.getCode());
}
tradeBo.setTradeStatus(TradeStatusEnum.SUCCESS.getCode());
}
// 撤销状态
if (Objects.equals(billDetail.getTradeState(), WxPayConstants.WxpayTradeStatus.REVOKED)) {
tradeBo.setTradeType(TradeTypeEnum.PAY.getCode());
tradeBo.setTradeStatus(TradeStatusEnum.REVOKED.getCode());
}
return tradeBo;
}
/**
* 保存下载的原始对账文件
*/
private String saveOriginalFile(ReconcileStatement reconcileOrder, byte[] bytes) {
String date = LocalDateTimeUtil.format(reconcileOrder.getDate(), DatePattern.PURE_DATE_PATTERN);
// 将原始文件进行保存 通道-日期
String fileName = StrUtil.format("交易对账单-微信-{}.csv",date);
var uploadPretreatment = fileStorageService.of(bytes);
if (StrUtil.isNotBlank(fileName)) {
uploadPretreatment.setOriginalFilename(fileName);
}
FileInfo upload = uploadPretreatment.upload();
return upload.getUrl();
}
}

View File

@@ -1,169 +0,0 @@
package org.dromara.daxpay.channel.wechat.service.payment.reconcile;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.text.csv.CsvReader;
import cn.hutool.core.text.csv.CsvUtil;
import cn.hutool.core.util.StrUtil;
import com.github.binarywang.wxpay.bean.request.WxPayApplyTradeBillV3Request;
import com.github.binarywang.wxpay.bean.result.WxPayApplyBillV3Result;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.dromara.daxpay.channel.wechat.entity.config.WechatPayConfig;
import org.dromara.daxpay.channel.wechat.entity.reconcile.WechatReconcileBillDetail;
import org.dromara.daxpay.channel.wechat.service.payment.config.WechatPayConfigService;
import org.dromara.daxpay.core.enums.TradeStatusEnum;
import org.dromara.daxpay.core.enums.TradeTypeEnum;
import org.dromara.daxpay.core.exception.OperationFailException;
import org.dromara.daxpay.service.bo.reconcile.ChannelReconcileTradeBo;
import org.dromara.daxpay.service.bo.reconcile.ReconcileResolveResultBo;
import org.dromara.daxpay.service.entity.reconcile.ReconcileStatement;
import org.dromara.x.file.storage.core.FileInfo;
import org.dromara.x.file.storage.core.FileStorageService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
/**
* 微信支付对账
* @author xxm
* @since 2024/1/17
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class WechatSubReconcileService {
private final FileStorageService fileStorageService;
private final WechatPayConfigService wechatPayConfigService;
/**
* 下载对账单并保存
*
* @param statement 对账订单
* @param date 对账日期 yyyyMMdd 格式
*/
@SneakyThrows
@Transactional(rollbackFor = Exception.class)
public ReconcileResolveResultBo downAndResolve(ReconcileStatement statement, String date) {
WechatPayConfig config = wechatPayConfigService.getAndCheckConfig(true);
WxPayService wxPayService = wechatPayConfigService.wxJavaSdk(config);
var request = new WxPayApplyTradeBillV3Request();
request.setBillDate(date);
request.setBillType("ALL");
try {
WxPayApplyBillV3Result wxPayApplyBillV3Result = wxPayService.applyTradeBill(request);
String downloadUrl = wxPayApplyBillV3Result.getDownloadUrl();
byte[] bytes;
try (InputStream inputStream = wxPayService.downloadBill(downloadUrl)) {
bytes = IoUtil.readBytes(inputStream);
}
BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes)));
String result = IoUtil.read(reader);
// 过滤特殊字符
result = result.replaceAll("`", "").replaceAll("\uFEFF", "");
CsvReader csvRows = CsvUtil.getReader();
// 获取交易记录并保存 同时过滤出当前应用的交易记录
String billDetail = StrUtil.subBefore(result, "总交易单数", false);
var billDetails = csvRows.read(billDetail, WechatReconcileBillDetail.class).stream()
// 只读取当前商户的订单
.filter(o->Objects.equals(o.getAppid(), config.getSubAppId()))
// 只读取对账日的记录
.filter(o->{
String transactionTime = o.getTransactionTime();
LocalDateTime time = LocalDateTimeUtil.parse(transactionTime, DatePattern.NORM_DATETIME_PATTERN);
return Objects.equals(statement.getDate(), LocalDate.from(time));
})
.toList();
// 保存原始对账文件
String originalFile = this.saveOriginalFile(statement, bytes);
// 解析账单文件
var tradeBos = this.convertReconcileTrade(billDetails);
return new ReconcileResolveResultBo()
.setOriginalFileUrl(originalFile)
.setChannelTrades(tradeBos);
} catch (WxPayException | IOException e) {
log.error("下载对账单失败", e);
throw new OperationFailException("下载对账单失败");
}
}
/**
* 转换为通用对账记录对象
*/
private List<ChannelReconcileTradeBo> convertReconcileTrade(List<WechatReconcileBillDetail> billDetails){
return billDetails.stream()
.map(this::convert)
.toList();
}
/**
* 转换为通用对账记录对象
*/
private ChannelReconcileTradeBo convert(WechatReconcileBillDetail billDetail){
// 交易时间时间 指该笔交易的支付成功时间或发起退款成功时间(注:不是退款成功时间)
LocalDateTime time = LocalDateTimeUtil.parse(billDetail.getTransactionTime(), DatePattern.NORM_DATETIME_PATTERN);
// 默认为支付对账记录
ChannelReconcileTradeBo tradeBo = new ChannelReconcileTradeBo()
.setPlatformTradeNo(billDetail.getOutTradeNo())
.setTradeType(TradeTypeEnum.PAY.getCode())
.setTradeTime(time)
.setChannelTradeNo(billDetail.getTransactionId());
if (Objects.equals(billDetail.getTradeState(), WxPayConstants.WxpayTradeStatus.SUCCESS)) {
tradeBo.setTradeType(TradeTypeEnum.PAY.getCode())
.setAmount(new BigDecimal(billDetail.getTotalFee()))
.setTradeStatus(TradeStatusEnum.SUCCESS.getCode());
}
// 退款覆盖更新对应的字段
if (Objects.equals(billDetail.getTradeState(), WxPayConstants.WxpayTradeStatus.REFUND)) {
tradeBo.setPlatformTradeNo(billDetail.getOutRefundNo())
.setChannelTradeNo(billDetail.getRefundId())
.setAmount(new BigDecimal(billDetail.getRefundFee()))
.setTradeType(TradeTypeEnum.REFUND.getCode());
tradeBo.setTradeType(TradeTypeEnum.REFUND.getCode());
// 状态
switch (billDetail.getRefundStatus()) {
case WxPayConstants.RefundStatus.SUCCESS -> tradeBo.setTradeStatus(TradeStatusEnum.SUCCESS.getCode());
case WxPayConstants.RefundStatus.PROCESSING -> tradeBo.setTradeStatus(TradeStatusEnum.CLOSED.getCode());
case WxPayConstants.ResultCode.FAIL -> tradeBo.setTradeStatus(TradeStatusEnum.FAIL.getCode());
case WxPayConstants.RefundStatus.CHANGE -> tradeBo.setTradeStatus(TradeStatusEnum.EXCEPTION.getCode());
}
tradeBo.setTradeStatus(TradeStatusEnum.SUCCESS.getCode());
}
// 撤销状态
if (Objects.equals(billDetail.getTradeState(), WxPayConstants.WxpayTradeStatus.REVOKED)) {
tradeBo.setTradeType(TradeTypeEnum.PAY.getCode());
tradeBo.setTradeStatus(TradeStatusEnum.REVOKED.getCode());
}
return tradeBo;
}
/**
* 保存下载的原始对账文件
*/
private String saveOriginalFile(ReconcileStatement reconcileOrder, byte[] bytes) {
String date = LocalDateTimeUtil.format(reconcileOrder.getDate(), DatePattern.PURE_DATE_PATTERN);
// 将原始文件进行保存 通道-日期
String fileName = StrUtil.format("交易对账单-微信-{}.csv",date);
var uploadPretreatment = fileStorageService.of(bytes);
if (StrUtil.isNotBlank(fileName)) {
uploadPretreatment.setOriginalFilename(fileName);
}
FileInfo upload = uploadPretreatment.upload();
return upload.getUrl();
}
}

View File

@@ -1,65 +0,0 @@
package org.dromara.daxpay.channel.wechat.strategy.merchant;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.dromara.daxpay.channel.wechat.service.payment.config.WechatPayConfigService;
import org.dromara.daxpay.channel.wechat.service.payment.reconcile.WechatReconcileService;
import org.dromara.daxpay.core.enums.ChannelEnum;
import org.dromara.daxpay.service.bo.reconcile.ReconcileResolveResultBo;
import org.dromara.daxpay.service.enums.ReconcileFileTypeEnum;
import org.dromara.daxpay.service.strategy.AbsReconcileStrategy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/**
* 微信支付对账策略
* @author xxm
* @since 2024/1/17
*/
@Slf4j
@Scope(SCOPE_PROTOTYPE)
@Service
@RequiredArgsConstructor
public class WechatReconcileStrategy extends AbsReconcileStrategy {
private final WechatReconcileService reconcileService;
private final WechatPayConfigService wechatPayConfigService;
/**
* 策略标识
*
* @see ChannelEnum
*/
@Override
public String getChannel() {
return ChannelEnum.WECHAT.getCode();
}
/**
* 上传对账单解析并保存
*/
@SneakyThrows
@Override
public ReconcileResolveResultBo uploadAndResolve(MultipartFile file, ReconcileFileTypeEnum fileType) {
// WechatPayConfig wechatPayConfig = wechatPayConfigService.getAndCheckConfig();
// return reconcileService.uploadBill(file.getBytes(),wechatPayConfig);
return null;
}
/**
* 下载对账单
*/
@Override
public ReconcileResolveResultBo downAndResolve() {
String date = LocalDateTimeUtil.format(this.getStatement().getDate(), DatePattern.NORM_DATE_PATTERN);
return reconcileService.downAndResolve(this.getStatement(), date);
}
}

View File

@@ -1,62 +0,0 @@
package org.dromara.daxpay.channel.wechat.strategy.sub;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.dromara.daxpay.channel.wechat.service.payment.reconcile.WechatSubReconcileService;
import org.dromara.daxpay.core.enums.ChannelEnum;
import org.dromara.daxpay.service.bo.reconcile.ReconcileResolveResultBo;
import org.dromara.daxpay.service.enums.ReconcileFileTypeEnum;
import org.dromara.daxpay.service.strategy.AbsReconcileStrategy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/**
* 微信支付对账策略
* @author xxm
* @since 2024/1/17
*/
@Slf4j
@Scope(SCOPE_PROTOTYPE)
@Service
@RequiredArgsConstructor
public class WechatSubReconcileStrategy extends AbsReconcileStrategy {
private final WechatSubReconcileService reconcileService;
/**
* 策略标识
*
* @see ChannelEnum
*/
@Override
public String getChannel() {
return ChannelEnum.WECHAT_ISV.getCode();
}
/**
* 上传对账单解析并保存
*/
@SneakyThrows
@Override
public ReconcileResolveResultBo uploadAndResolve(MultipartFile file, ReconcileFileTypeEnum fileType) {
// WechatPayConfig wechatPayConfig = wechatPayConfigService.getAndCheckConfig();
// return reconcileService.uploadBill(file.getBytes(),wechatPayConfig);
return null;
}
/**
* 下载对账单
*/
@Override
public ReconcileResolveResultBo downAndResolve() {
String date = LocalDateTimeUtil.format(this.getStatement().getDate(), DatePattern.NORM_DATE_PATTERN);
return reconcileService.downAndResolve(this.getStatement(), date);
}
}