feat 支付对账开发, 调整支付宝流水表数据结构, 支付修复策略问题调试

This commit is contained in:
xxm1995
2024-02-28 16:58:42 +08:00
committed by xxm
parent ba7eb68780
commit 75ef54085d
33 changed files with 461 additions and 109 deletions

View File

@@ -5,7 +5,6 @@ import cn.bootx.platform.common.core.rest.Res;
import cn.bootx.platform.common.core.rest.ResResult;
import cn.bootx.platform.common.core.rest.param.PageParam;
import cn.bootx.platform.common.core.util.ValidationUtil;
import cn.bootx.platform.daxpay.service.core.order.reconcile.service.PayReconcileOrderService;
import cn.bootx.platform.daxpay.service.core.order.reconcile.service.PayReconcileQueryService;
import cn.bootx.platform.daxpay.service.core.payment.reconcile.service.PayReconcileService;
import cn.bootx.platform.daxpay.service.dto.order.reconcile.PayReconcileDetailDto;
@@ -29,14 +28,13 @@ import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
public class PayReconcileOrderController {
private final PayReconcileService reconcileService;
private final PayReconcileOrderService reconcileOrderService;
private final PayReconcileQueryService reconcileQueryService;
@Operation(summary = "手动创建支付对账订单")
@PostMapping("/create")
public ResResult<Void> create(@RequestBody ReconcileOrderCreate param){
ValidationUtil.validateParam(param);
reconcileOrderService.create(param.getDate(), param.getChannel());
reconcileService.create(param.getDate(), param.getChannel());
return Res.ok();
}
@@ -47,6 +45,13 @@ public class PayReconcileOrderController {
return Res.ok();
}
@Operation(summary = "手动触发对账单比对")
@PostMapping("/compare")
public ResResult<Void> compare(Long id){
reconcileService.compare(id);
return Res.ok();
}
@Operation(summary = "订单分页")
@GetMapping("/page")
public ResResult<PageResult<PayReconcileOrderDto>> page(PageParam pageParam, ReconcileOrderQuery query){

View File

@@ -4,7 +4,6 @@ import cn.bootx.platform.daxpay.service.core.order.reconcile.entity.PayReconcile
import lombok.Data;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
import java.util.List;
/**
@@ -19,8 +18,4 @@ public class ReconcileLocal {
/** 通用支付对账记录 */
private List<PayReconcileDetail> reconcileDetails;
/** 支付完成时间 从支付网关中获取 */
private LocalDateTime payTime;
}

View File

@@ -0,0 +1,34 @@
package cn.bootx.platform.daxpay.service.configuration.sequence;
import cn.bootx.platform.common.sequence.func.Sequence;
import cn.bootx.platform.common.sequence.range.SeqRangeManager;
import cn.bootx.platform.common.sequence.util.SequenceUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 序列生成器配置类
* @author xxm
* @since 2024/2/28
*/
@Configuration
@RequiredArgsConstructor
public class DaxPaySequenceConfiguration {
/**
* 支付宝对账单序列生成器
*/
@Bean
public Sequence alipayReconcileSequence(SeqRangeManager seqRangeManager) {
return SequenceUtil.create(1,1,1,"AlipayReconcileSequence");
}
/**
* 微信对账单序列生成器
*/
@Bean
public Sequence wechatReconcileSequence(SeqRangeManager seqRangeManager) {
return SequenceUtil.create(1,1,1,"WechatReconcileSequence");
}
}

View File

@@ -12,6 +12,9 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
/**
*
* @author xxm
@@ -31,4 +34,13 @@ public class AliPayRecordManager extends BaseManager<AliPayRecordMapper, AliPayR
QueryWrapper<AliPayRecord> generator = QueryGenerator.generator(param);
return this.page(mpPage, generator);
}
/**
* 按时间范围查询
*/
public List<AliPayRecord> findByDate(LocalDateTime startDate, LocalDateTime endDate){
return this.lambdaQuery()
.between(AliPayRecord::getGatewayTime, startDate, endDate)
.list();
}
}

View File

@@ -12,6 +12,8 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 支付宝流水记录
* @author xxm
@@ -47,6 +49,10 @@ public class AliPayRecord extends MpCreateEntity implements EntityBaseFunction<A
@DbColumn(comment = "网关订单号")
private String gatewayOrderNo;
/** 网关完成时间 */
@DbColumn(comment = "网关完成时间")
private LocalDateTime gatewayTime;
@Override
public AliPayRecordDto toDto() {
return AlipayConvert.CONVERT.convert(this);

View File

@@ -138,14 +138,14 @@ public class AliPayReconcileService {
// 默认为支付对账记录
PayReconcileDetail payReconcileDetail = new PayReconcileDetail()
.setRecordOrderId(billDetail.getRecordOrderId())
.setPaymentId(billDetail.getOutTradeNo())
.setOrderId(billDetail.getOutTradeNo())
.setType(PayReconcileTradeEnum.PAY.getCode())
.setAmount(amount)
.setTitle(billDetail.getSubject())
.setGatewayOrderNo(billDetail.getTradeNo());
// 退款覆盖更新对应的字段
if (Objects.equals(billDetail.getTradeType(), "退款")){
payReconcileDetail.setRefundId(billDetail.getBatchNo())
payReconcileDetail.setOrderId(billDetail.getBatchNo())
.setType(PayReconcileTradeEnum.REFUND.getCode());
}

View File

@@ -37,8 +37,9 @@ public class AliPayRecordService {
.setType(AliPayRecordTypeEnum.PAY.getCode())
.setTitle(order.getTitle())
.setOrderId(order.getId())
.setAmount(channelOrder.getAmount())
.setGatewayOrderNo(order.getGatewayOrderNo())
.setAmount(channelOrder.getAmount());
.setGatewayTime(channelOrder.getPayTime());
aliPayRecordManager.save(aliPayRecord);
}
@@ -50,8 +51,9 @@ public class AliPayRecordService {
.setType(AliPayRecordTypeEnum.REFUND.getCode())
.setTitle(order.getTitle())
.setOrderId(order.getId())
.setAmount(channelOrder.getAmount())
.setGatewayOrderNo(order.getGatewayOrderNo())
.setAmount(channelOrder.getAmount());
.setGatewayTime(channelOrder.getRefundTime());
aliPayRecordManager.save(aliPayRecord);
}

View File

@@ -12,6 +12,9 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
/**
*
* @author xxm
@@ -30,4 +33,13 @@ public class WeChatPayRecordManager extends BaseManager<WeChatPayRecordMapper, W
QueryWrapper<WeChatPayRecord> generator = QueryGenerator.generator(param);
return this.page(mpPage, generator);
}
/**
* 按时间范围查询
*/
public List<WeChatPayRecord> findByDate(LocalDateTime start, LocalDateTime end) {
return this.lambdaQuery()
.between(WeChatPayRecord::getGatewayTime, start, end)
.list();
}
}

View File

@@ -12,6 +12,8 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 微信支付记录
* @author xxm
@@ -47,6 +49,10 @@ public class WeChatPayRecord extends MpCreateEntity implements EntityBaseFunctio
@DbColumn(comment = "网关订单号")
private String gatewayOrderNo;
/** 网关完成时间 */
@DbColumn(comment = "网关完成时间")
private LocalDateTime gatewayTime;
@Override
public WeChatPayRecordDto toDto() {
return WeChatConvert.CONVERT.convert(this);

View File

@@ -36,8 +36,9 @@ public class WeChatPayRecordService {
.setType(WechatPayRecordTypeEnum.PAY.getCode())
.setTitle(order.getTitle())
.setOrderId(order.getId())
.setAmount(channelOrder.getAmount())
.setGatewayOrderNo(order.getGatewayOrderNo())
.setAmount(channelOrder.getAmount());
.setGatewayTime(channelOrder.getPayTime());
weChatPayRecordManager.save(weChatPayRecord);
}
@@ -49,8 +50,9 @@ public class WeChatPayRecordService {
.setType(WechatPayRecordTypeEnum.REFUND.getCode())
.setTitle(order.getTitle())
.setOrderId(order.getId())
.setAmount(channelOrder.getAmount())
.setGatewayOrderNo(order.getGatewayOrderNo())
.setAmount(channelOrder.getAmount());
.setGatewayTime(channelOrder.getRefundTime());
weChatPayRecordManager.save(weChatPayRecord);
}

View File

@@ -118,7 +118,7 @@ public class WechatPayReconcileService{
// 默认为支付对账记录
PayReconcileDetail payReconcileDetail = new PayReconcileDetail()
.setRecordOrderId(billDetail.getRecordOrderId())
.setPaymentId(billDetail.getMchOrderNo())
.setOrderId(billDetail.getMchOrderNo())
.setTitle(billDetail.getSubject())
.setGatewayOrderNo(billDetail.getWeiXinOrderNo());
// 支付
@@ -138,7 +138,7 @@ public class WechatPayReconcileService{
int amount = Math.abs(((int) v));
payReconcileDetail.setType(PayReconcileTradeEnum.REFUND.getCode())
.setAmount(amount)
.setRefundId(billDetail.getMchRefundNo());
.setOrderId(billDetail.getMchRefundNo());
}
// TODO 已撤销, 暂时未处理
if (Objects.equals(billDetail.getStatus(), "REVOKED")){

View File

@@ -58,6 +58,7 @@ public class PayChannelOrder extends MpCreateEntity implements EntityBaseFunctio
private String status;
@DbColumn(comment = "支付时间")
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private LocalDateTime payTime;

View File

@@ -10,6 +10,8 @@ import cn.bootx.table.modify.annotation.DbColumn;
import cn.bootx.table.modify.annotation.DbTable;
import cn.bootx.table.modify.mysql.annotation.DbMySqlIndex;
import cn.bootx.table.modify.mysql.constants.MySqlIndexType;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -76,10 +78,12 @@ public class PayOrder extends MpBaseEntity implements EntityBaseFunction<PayOrde
/** 支付时间 */
@DbColumn(comment = "支付时间")
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private LocalDateTime payTime;
/** 关闭时间 */
@DbColumn(comment = "支付时间")
@DbColumn(comment = "关闭时间")
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private LocalDateTime closeTime;
/** 过期时间 */

View File

@@ -12,6 +12,8 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
*
* @author xxm
@@ -22,6 +24,14 @@ import org.springframework.stereotype.Repository;
@RequiredArgsConstructor
public class PayReconcileDetailManager extends BaseManager<PayReconcileDetailMapper, PayReconcileDetail> {
/**
* 根据订单id查询
*/
public List<PayReconcileDetail> findAllByOrderId(Long orderId){
return this.findAllByField(PayReconcileDetail::getRecordOrderId, orderId);
}
/**
* 分页
*/

View File

@@ -45,6 +45,10 @@ public class PayReconcileDetail extends MpCreateEntity implements EntityBaseFunc
/** 本地订单ID */
@DbColumn(comment = "本地订单ID")
private String orderId;
/** 支付订单ID */
@DbColumn(comment = "本地订单ID")
private String paymentId;
/** 本地退款ID */

View File

@@ -55,9 +55,6 @@ public class PayReconcileOrder extends MpCreateEntity implements EntityBaseFunct
@DbColumn(comment = "错误信息")
private String errorMsg;
/**
* @return
*/
@Override
public PayReconcileOrderDto toDto() {
return PayReconcileConvert.CONVERT.convert(this);

View File

@@ -1,19 +1,14 @@
package cn.bootx.platform.daxpay.service.core.order.reconcile.service;
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.order.reconcile.dao.PayReconcileOrderManager;
import cn.bootx.platform.daxpay.service.core.order.reconcile.entity.PayReconcileOrder;
import cn.hutool.core.date.DatePattern;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.Optional;
/**
@@ -43,37 +38,4 @@ public class PayReconcileOrderService {
return reconcileOrderManager.findById(id);
}
/**
* 创建对账订单
*/
public PayReconcileOrder create(LocalDate date,String channel) {
// 获取通道枚举
PayChannelEnum channelEnum = PayChannelEnum.findByCode(channel);
// 判断是否为为异步通道
if (!PayChannelEnum.ASYNC_TYPE.contains(channelEnum)){
throw new PayFailureException("不支持对账的通道, 请检查");
}
// 生成批次号
String format = LocalDateTimeUtil.format(date, DatePattern.PURE_DATE_PATTERN);
String key = channelEnum.getReconcilePrefix()+format;
String seqNo = key + this.genSeqNo(key);
PayReconcileOrder order = new PayReconcileOrder()
.setBatchNo(seqNo)
.setChannel(channel)
.setDate(date);
reconcileOrderManager.save(order);
return order;
}
/**
* 生成批次号
* 规则:通道简称 + yyyyMMdd + 两位流水号
* 例子wx2024012001、ali2024012002
*/
private String genSeqNo(String key){
long next = sequence.next(key);
return String.format("%02d",next);
}
}

View File

@@ -1,8 +1,11 @@
package cn.bootx.platform.daxpay.service.core.payment.reconcile.service;
import cn.bootx.platform.common.core.exception.DataNotExistException;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.service.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.service.core.order.reconcile.dao.PayReconcileDetailManager;
import cn.bootx.platform.daxpay.service.core.order.reconcile.dao.PayReconcileOrderManager;
import cn.bootx.platform.daxpay.service.core.order.reconcile.entity.PayReconcileDetail;
import cn.bootx.platform.daxpay.service.core.order.reconcile.entity.PayReconcileOrder;
import cn.bootx.platform.daxpay.service.core.order.reconcile.service.PayReconcileOrderService;
@@ -11,7 +14,10 @@ import cn.bootx.platform.daxpay.service.func.AbsReconcileStrategy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.util.List;
/**
@@ -23,9 +29,36 @@ import java.util.List;
@Service
@RequiredArgsConstructor
public class PayReconcileService {
private final PayReconcileOrderManager reconcileOrderManager;
private final PayReconcileOrderService reconcileOrderService;
private final PayReconcileDetailManager reconcileDetailManager;
/**
* 创建对账订单
*/
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public PayReconcileOrder create(LocalDate date, String channel) {
// 获取通道枚举
PayChannelEnum channelEnum = PayChannelEnum.findByCode(channel);
// 判断是否为为异步通道
if (!PayChannelEnum.ASYNC_TYPE.contains(channelEnum)){
throw new PayFailureException("不支持对账的通道, 请检查");
}
// 构建对账策略
AbsReconcileStrategy reconcileStrategy = PayReconcileStrategyFactory.create(channel);
// 生成批次号
String seqNo = reconcileStrategy.generateSequence(date);
PayReconcileOrder order = new PayReconcileOrder()
.setBatchNo(seqNo)
.setChannel(channel)
.setDate(date);
reconcileOrderManager.save(order);
return order;
}
/**
* 下载对账单并进行保存
*/
@@ -38,19 +71,19 @@ public class PayReconcileService {
/**
* 下载对账单并进行保存
*/
public void downAndSave(PayReconcileOrder recordOrder) {
public void downAndSave(PayReconcileOrder reconcileOrder) {
// 构建对账策略
AbsReconcileStrategy absReconcileStrategy = PayReconcileStrategyFactory.create(recordOrder.getChannel());
absReconcileStrategy.initParam(recordOrder);
absReconcileStrategy.doBeforeHandler();
AbsReconcileStrategy reconcileStrategy = PayReconcileStrategyFactory.create(reconcileOrder.getChannel());
reconcileStrategy.setRecordOrder(reconcileOrder);
reconcileStrategy.doBeforeHandler();
try {
absReconcileStrategy.downAndSave();
recordOrder.setDown(true);
reconcileOrderService.update(recordOrder);
reconcileStrategy.downAndSave();
reconcileOrder.setDown(true);
reconcileOrderService.update(reconcileOrder);
} catch (Exception e) {
log.error("下载对账单异常", e);
recordOrder.setErrorMsg("原因: " + e.getMessage());
reconcileOrderService.update(recordOrder);
reconcileOrder.setErrorMsg("原因: " + e.getMessage());
reconcileOrderService.update(reconcileOrder);
throw new RuntimeException(e);
}
// 保存转换后的通用结构对账单
@@ -60,4 +93,47 @@ public class PayReconcileService {
reconcileDetailManager.saveAll(reconcileDetails);
}
/**
* 对账单比对
*/
public void compare(Long reconcileOrderId){
PayReconcileOrder payReconcileOrder = reconcileOrderService.findById(reconcileOrderId)
.orElseThrow(() -> new DataNotExistException("未找到对账订单"));
this.compare(payReconcileOrder);
}
/**
* 对账单比对
*/
public void compare(PayReconcileOrder reconcileOrder){
// 判断是否已经下载了对账单明细
if (!reconcileOrder.isDown()){
throw new PayFailureException("请先下载对账单");
}
// 查询对账单
List<PayReconcileDetail> reconcileDetails = reconcileDetailManager.findAllByOrderId(reconcileOrder.getId());
// 获取通道枚举
if (!PayChannelEnum.ASYNC_TYPE_CODE.contains(reconcileOrder.getChannel())){
log.error("不支持对账的通道, 请检查, 对账订单ID: {}", reconcileOrder.getId());
throw new PayFailureException("不支持对账的通道, 请检查");
}
// 判断是否为为异步通道
// 构建对账策略
AbsReconcileStrategy reconcileStrategy = PayReconcileStrategyFactory.create(reconcileOrder.getChannel());
// 初始化参数
reconcileStrategy.setRecordOrder(reconcileOrder);
reconcileStrategy.setReconcileDetails(reconcileDetails);
try {
// 执行比对任务
reconcileStrategy.compare();
// reconcileOrder.setCompare(true);
// reconcileOrderService.update(reconcileOrder);
} catch (Exception e) {
log.error("比对对账单异常", e);
reconcileOrder.setErrorMsg("原因: " + e.getMessage());
reconcileOrderService.update(reconcileOrder);
throw new RuntimeException(e);
}
}
}

View File

@@ -1,17 +1,35 @@
package cn.bootx.platform.daxpay.service.core.payment.reconcile.strategy;
import cn.bootx.platform.common.core.function.CollectorsFunction;
import cn.bootx.platform.common.core.util.CollUtil;
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.code.PayReconcileTradeEnum;
import cn.bootx.platform.daxpay.service.code.AliPayRecordTypeEnum;
import cn.bootx.platform.daxpay.service.core.channel.alipay.dao.AliPayRecordManager;
import cn.bootx.platform.daxpay.service.core.channel.alipay.entity.AliPayConfig;
import cn.bootx.platform.daxpay.service.core.channel.alipay.entity.AliPayRecord;
import cn.bootx.platform.daxpay.service.core.channel.alipay.service.AliPayConfigService;
import cn.bootx.platform.daxpay.service.core.channel.alipay.service.AliPayReconcileService;
import cn.bootx.platform.daxpay.service.core.order.reconcile.entity.PayReconcileDetail;
import cn.bootx.platform.daxpay.service.func.AbsReconcileStrategy;
import cn.hutool.core.date.DatePattern;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/**
@@ -27,8 +45,14 @@ public class AlipayReconcileStrategy extends AbsReconcileStrategy {
private final AliPayReconcileService reconcileService;
private final AliPayRecordManager recordManager;
private final AliPayConfigService configService;
@Getter
@Qualifier("alipayReconcileSequence")
private final Sequence sequence;
private AliPayConfig config;
@@ -42,6 +66,19 @@ public class AlipayReconcileStrategy extends AbsReconcileStrategy {
return PayChannelEnum.ALI;
}
/**
* 生成对账序列号
* 规则:通道简称 + yyyyMMdd + 两位流水号
* 例子wx2024012001、ali2024012002
*/
@Override
public String generateSequence(LocalDate date) {
String prefix = getChannel().getReconcilePrefix();
String dateStr = LocalDateTimeUtil.format(date, DatePattern.PURE_DATE_PATTERN);
String key = String.format("%02d", sequence.next());
return prefix + dateStr + key;
}
/**
* 对账前处理, 主要是初始化支付SDK配置
*/
@@ -59,4 +96,69 @@ public class AlipayReconcileStrategy extends AbsReconcileStrategy {
String date = LocalDateTimeUtil.format(this.getRecordOrder().getDate(), DatePattern.NORM_DATE_PATTERN);
reconcileService.downAndSave(date,this.getRecordOrder().getId());
}
/**
* 比对生成对账差异单
* 1. 远程有, 本地无 补单(追加回订单/记录差异表)
* 2. 远程无, 本地有 记录差错表
* 3. 远程有, 本地有, 但状态不一致 记录差错表
*/
@Override
public void compare() {
List<PayReconcileDetail> details = this.getReconcileDetails();
if (CollUtil.isEmpty(details)){
return;
}
Map<String, PayReconcileDetail> detailMap = details.stream()
.collect(Collectors.toMap(PayReconcileDetail::getOrderId, Function.identity(), CollectorsFunction::retainLatest));
// 对哪天进行对账
LocalDate date = this.getRecordOrder().getDate();
// 查询流水
LocalDateTime localDateTime = LocalDateTimeUtil.date2DateTime(date);
LocalDateTime start = LocalDateTimeUtil.beginOfDay(localDateTime);
LocalDateTime end = LocalDateTimeUtil.endOfDay(localDateTime);
List<AliPayRecord> records = recordManager.findByDate(start, end);
Map<Long, AliPayRecord> recordMap = records.stream()
.collect(Collectors.toMap(AliPayRecord::getOrderId, Function.identity(), CollectorsFunction::retainLatest));
// 对账与流水比对
for (PayReconcileDetail detail : details) {
// 判断本地有没有记录
AliPayRecord record = recordMap.get(Long.valueOf(detail.getOrderId()));
if (Objects.isNull(record)){
log.info("本地订单不存在: {}", detail.getOrderId());
continue;
}
System.out.println(detail.getId());
// 交易类型 支付/退款
if (Objects.equals(detail.getType(), PayReconcileTradeEnum.PAY.getCode())){
// 判断类型是否存在差异
if (!Objects.equals(record.getType(), AliPayRecordTypeEnum.PAY.getCode())){
log.info("本地订单类型不正常: {}", detail.getOrderId());
continue;
}
} else {
// 判断类型是否存在差异
if (!Objects.equals(record.getType(), AliPayRecordTypeEnum.REFUND.getCode())){
log.info("本地订单类型不正常: {}", detail.getOrderId());
continue;
}
}
// 判断是否存在差异 金额, 状态
if (!Objects.equals(record.getAmount(), detail.getAmount())){
log.info("本地订单金额不正常: {}", detail.getOrderId());
continue;
}
}
// 流水与对账单比对, 找出本地有, 远程没有的记录
for (AliPayRecord record : records) {
PayReconcileDetail detail = detailMap.get(String.valueOf(record.getOrderId()));
if (Objects.isNull(detail)){
log.info("远程订单不存在: {}", record.getOrderId());
continue;
}
}
}
}

View File

@@ -1,17 +1,34 @@
package cn.bootx.platform.daxpay.service.core.payment.reconcile.strategy;
import cn.bootx.platform.common.core.function.CollectorsFunction;
import cn.bootx.platform.common.core.util.CollUtil;
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.code.PayReconcileTradeEnum;
import cn.bootx.platform.daxpay.service.code.AliPayRecordTypeEnum;
import cn.bootx.platform.daxpay.service.core.channel.wechat.dao.WeChatPayRecordManager;
import cn.bootx.platform.daxpay.service.core.channel.wechat.entity.WeChatPayConfig;
import cn.bootx.platform.daxpay.service.core.channel.wechat.entity.WeChatPayRecord;
import cn.bootx.platform.daxpay.service.core.channel.wechat.service.WeChatPayConfigService;
import cn.bootx.platform.daxpay.service.core.channel.wechat.service.WechatPayReconcileService;
import cn.bootx.platform.daxpay.service.core.order.reconcile.entity.PayReconcileDetail;
import cn.bootx.platform.daxpay.service.func.AbsReconcileStrategy;
import cn.hutool.core.date.DatePattern;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/**
@@ -29,6 +46,11 @@ public class WechatPayReconcileStrategy extends AbsReconcileStrategy {
private final WeChatPayConfigService weChatPayConfigService;
private final WeChatPayRecordManager recordManager;
@Qualifier("wechatReconcileSequence")
private final Sequence sequence;
private WeChatPayConfig config;
/**
@@ -41,6 +63,20 @@ public class WechatPayReconcileStrategy extends AbsReconcileStrategy {
return PayChannelEnum.WECHAT;
}
/**
* 生成对账序列号
* 生成批次号
* 规则:通道简称 + yyyyMMdd + 两位流水号
* 例子wx2024012001、ali2024012002
*/
@Override
public String generateSequence(LocalDate date) {
String prefix = getChannel().getReconcilePrefix();
String dateStr = LocalDateTimeUtil.format(date, DatePattern.PURE_DATE_PATTERN);
String key = String.format("%02d", sequence.next());
return prefix + dateStr + key;
}
/**
* 对账前处理, 主要是初始化支付SDK配置
*/
@@ -57,4 +93,68 @@ public class WechatPayReconcileStrategy extends AbsReconcileStrategy {
String format = LocalDateTimeUtil.format(this.getRecordOrder().getDate(), DatePattern.PURE_DATE_PATTERN);
reconcileService.downAndSave(format,this.getRecordOrder().getId(), this.config);
}
/**
* 比对生成对账差异单
* 1. 远程有, 本地无 补单(追加回订单/记录差异表)
* 2. 远程无, 本地有 记录差错表
* 3. 远程有, 本地有, 但状态不一致 记录差错表
*/
@Override
public void compare() {
List<PayReconcileDetail> details = this.getReconcileDetails();
if (CollUtil.isEmpty(details)){
return;
}
Map<String, PayReconcileDetail> detailMap = details.stream()
.collect(Collectors.toMap(PayReconcileDetail::getOrderId, Function.identity(), CollectorsFunction::retainLatest));
// 对哪天进行对账
LocalDate date = this.getRecordOrder().getDate();
// 查询流水
LocalDateTime localDateTime = LocalDateTimeUtil.date2DateTime(date);
LocalDateTime start = LocalDateTimeUtil.beginOfDay(localDateTime);
LocalDateTime end = LocalDateTimeUtil.endOfDay(localDateTime);
List<WeChatPayRecord> records = recordManager.findByDate(start, end);
Map<Long, WeChatPayRecord> recordMap = records.stream()
.collect(Collectors.toMap(WeChatPayRecord::getOrderId, Function.identity(), CollectorsFunction::retainLatest));
// 对账与流水比对
for (PayReconcileDetail detail : details) {
// 判断本地有没有记录
WeChatPayRecord record = recordMap.get(Long.valueOf(detail.getOrderId()));
if (Objects.isNull(record)){
log.info("本地订单不存在: {}", detail.getOrderId());
continue;
}
// 交易类型 支付/退款
if (Objects.equals(detail.getType(), PayReconcileTradeEnum.PAY.getCode())){
// 判断类型是否存在差异
if (!Objects.equals(record.getType(), AliPayRecordTypeEnum.PAY.getCode())){
log.info("本地订单类型不正常: {}", detail.getOrderId());
continue;
}
} else {
// 判断类型是否存在差异
if (!Objects.equals(record.getType(), AliPayRecordTypeEnum.REFUND.getCode())){
log.info("本地订单类型不正常: {}", detail.getOrderId());
continue;
}
}
// 判断是否存在差异 金额, 状态
if (!Objects.equals(record.getAmount(), detail.getAmount())){
log.info("本地订单金额不正常: {}", detail.getOrderId());
continue;
}
}
// 流水与对账单比对, 找出本地有, 远程没有的记录
for (WeChatPayRecord record : records) {
PayReconcileDetail detail = detailMap.get(String.valueOf(record.getOrderId()));
if (Objects.isNull(detail)){
log.info("远程订单不存在: {}", record.getOrderId());
continue;
}
}
}
}

View File

@@ -98,6 +98,9 @@ public class PayRepairService {
List<PayChannelOrder> channelOrders = repairStrategies.stream()
.map(AbsPayRepairStrategy::getChannelOrder)
.collect(Collectors.toList());
// 更新通道订单
channelOrderManager.updateAllById(channelOrders);
// 发送通知
clientNoticeService.registerPayNotice(order, null, channelOrders);
this.saveRecord(order, repairType, repairResult);
return repairResult;
@@ -105,13 +108,15 @@ public class PayRepairService {
/**
* 变更未待支付
*
* TODO 后期保存为异常订单
*/
private void waitPay(PayOrder order, List<AbsPayRepairStrategy> repairStrategies) {
repairStrategies.forEach(AbsPayRepairStrategy::doCloseLocalHandler);
// 修改订单支付状态为成功
order.setStatus(PayStatusEnum.PROGRESS.getCode());
// 待支付批量处理
repairStrategies.forEach(AbsPayRepairStrategy::doWaitPayHandler);
// 修改订单支付状态为待支付
order.setStatus(PayStatusEnum.PROGRESS.getCode())
.setPayTime(null)
.setCloseTime(null);
payOrderService.updateById(order);
}
@@ -141,7 +146,7 @@ public class PayRepairService {
// 执行策略的关闭方法
absPayStrategies.forEach(AbsPayRepairStrategy::doCloseLocalHandler);
order.setStatus(PayStatusEnum.CLOSE.getCode())
// TODO 尝试是否可以使用网关返回的
// TODO 尝试是否可以使用网关返回的时间
.setCloseTime(LocalDateTime.now());
payOrderService.updateById(order);
}

View File

@@ -7,7 +7,6 @@ import cn.bootx.platform.daxpay.service.core.channel.alipay.entity.AliPayConfig;
import cn.bootx.platform.daxpay.service.core.channel.alipay.service.AliPayCloseService;
import cn.bootx.platform.daxpay.service.core.channel.alipay.service.AliPayConfigService;
import cn.bootx.platform.daxpay.service.core.channel.alipay.service.AliPayRecordService;
import cn.bootx.platform.daxpay.service.core.order.pay.dao.PayChannelOrderManager;
import cn.bootx.platform.daxpay.service.func.AbsPayRepairStrategy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -34,8 +33,6 @@ public class AliPayRepairStrategy extends AbsPayRepairStrategy {
private final AliPayRecordService aliRecordService;
private final PayChannelOrderManager payChannelOrderManager;
/**
* 策略标识
*/
@@ -63,7 +60,6 @@ public class AliPayRepairStrategy extends AbsPayRepairStrategy {
.getFinishTime();
this.getChannelOrder().setStatus(PayStatusEnum.SUCCESS.getCode())
.setPayTime(payTime);
payChannelOrderManager.updateById(this.getChannelOrder());
// 支付完成, 保存记录
aliRecordService.pay(this.getOrder(), this.getChannelOrder());
}
@@ -72,8 +68,8 @@ public class AliPayRepairStrategy extends AbsPayRepairStrategy {
* 等待支付处理
*/
@Override
public void doWaitPayHandler() {
super.doWaitPayHandler();
public void doWaitPayHandler(){
this.getChannelOrder().setPayTime(null).setStatus(PayStatusEnum.PROGRESS.getCode());
}
/**

View File

@@ -7,7 +7,6 @@ import cn.bootx.platform.daxpay.service.core.channel.wechat.entity.WeChatPayConf
import cn.bootx.platform.daxpay.service.core.channel.wechat.service.WeChatPayCloseService;
import cn.bootx.platform.daxpay.service.core.channel.wechat.service.WeChatPayConfigService;
import cn.bootx.platform.daxpay.service.core.channel.wechat.service.WeChatPayRecordService;
import cn.bootx.platform.daxpay.service.core.order.pay.dao.PayChannelOrderManager;
import cn.bootx.platform.daxpay.service.func.AbsPayRepairStrategy;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -32,8 +31,6 @@ public class WeChatPayRepairStrategy extends AbsPayRepairStrategy {
private final WeChatPayConfigService weChatPayConfigService;
private final PayChannelOrderManager payChannelOrderManager;
private final WeChatPayRecordService weChatPayRecordService;
private WeChatPayConfig weChatPayConfig;
@@ -46,6 +43,14 @@ public class WeChatPayRepairStrategy extends AbsPayRepairStrategy {
return PayChannelEnum.WECHAT;
}
/**
* 等待支付处理
*/
@Override
public void doWaitPayHandler(){
this.getChannelOrder().setPayTime(null).setStatus(PayStatusEnum.PROGRESS.getCode());
}
/**
* 修复前处理
*/
@@ -64,7 +69,6 @@ public class WeChatPayRepairStrategy extends AbsPayRepairStrategy {
.getFinishTime();
this.getChannelOrder().setStatus(PayStatusEnum.SUCCESS.getCode())
.setPayTime(payTime);
payChannelOrderManager.updateById(this.getChannelOrder());
// 保存流水记录
weChatPayRecordService.pay(this.getOrder(), this.getChannelOrder());
}

View File

@@ -104,11 +104,10 @@ public class PaySyncService {
if (Objects.equals(syncResult.getSyncStatus(), PaySyncStatusEnum.FAIL)){
// 同步失败, 返回失败响应, 同时记录失败的日志
this.saveRecord(payOrder, syncResult, false, null, syncResult.getErrorMsg());
// return new SyncResult().setErrorMsg(syncResult.getErrorMsg());
throw new PayFailureException(syncResult.getErrorMsg());
}
// 支付订单的网关订单号是否一致, 不一致进行更新
if (Objects.nonNull(syncResult.getGatewayOrderNo()) && !Objects.equals(syncResult.getGatewayOrderNo(), payOrder.getGatewayOrderNo())){
if (!Objects.equals(syncResult.getGatewayOrderNo(), payOrder.getGatewayOrderNo())){
payOrder.setGatewayOrderNo(syncResult.getGatewayOrderNo());
payOrderService.updateById(payOrder);
}

View File

@@ -7,6 +7,8 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 微信支付记录
* @author xxm
@@ -41,4 +43,8 @@ public class AliPayRecordDto extends BaseDto {
@Schema(description = "网关订单号")
private String gatewayOrderNo;
/** 网关完成时间 */
@Schema(description = "网关完成时间")
private LocalDateTime gatewayTime;
}

View File

@@ -7,6 +7,8 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 微信支付记录
* @author xxm
@@ -41,4 +43,8 @@ public class WeChatPayRecordDto extends BaseDto {
@Schema(description = "网关订单号")
private String gatewayOrderNo;
/** 网关完成时间 */
@Schema(description = "网关完成时间")
private LocalDateTime gatewayTime;
}

View File

@@ -47,9 +47,7 @@ public abstract class AbsPayRepairStrategy implements PayStrategy{
/**
* 等待支付处理
*/
public void doWaitPayHandler(){
}
public void doWaitPayHandler(){}
/**
* 关闭本地支付

View File

@@ -1,32 +1,40 @@
package cn.bootx.platform.daxpay.service.func;
import cn.bootx.platform.daxpay.service.core.order.reconcile.entity.PayReconcileDetail;
import cn.bootx.platform.daxpay.service.core.order.reconcile.entity.PayReconcileOrder;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDate;
import java.util.List;
/**
* 支付对账策略
* @author xxm
* @since 2024/1/18
*/
@Setter
@Getter
public abstract class AbsReconcileStrategy implements PayStrategy {
/** 对账订单 */
private PayReconcileOrder recordOrder;
/** 对账订单明细 */
private List<PayReconcileDetail> reconcileDetails;
/**
* 生成对账序列号
*/
public abstract String generateSequence(LocalDate date);
/**
* 对账前处理, 主要是初始化支付SDK配置
*/
public void doBeforeHandler() {
}
/**
* 初始化参数
*/
public void initParam(PayReconcileOrder recordOrder){
this.recordOrder = recordOrder;
}
/**
* 下载对账单到本地进行保存
*/
@@ -36,10 +44,8 @@ public abstract class AbsReconcileStrategy implements PayStrategy {
* 比对生成对账差异单
* 1. 远程有, 本地无 补单(追加回订单/记录差异表)
* 2. 远程无, 本地有 记录差错表
* 3. 远程有, 本地有, 但状态不一致
* 3. 远程有, 本地有, 但状态不一致 记录差错表
*/
public void offsetting(){
}
public abstract void compare();
}

View File

@@ -20,10 +20,13 @@ import lombok.experimental.Accessors;
@Schema(title = "支付退款查询参数")
public class RefundOrderQuery extends QueryOrder {
@Schema(description = "退款")
@Schema(description = "退款Id")
private Long id;
@Schema(description = "支付")
@Schema(description = "退款")
private String refundNo;
@Schema(description = "支付ID")
private Long paymentId;
@Schema(description = "关联的业务号")

View File

@@ -24,7 +24,7 @@ public class PayReconcileTask implements Job {
private final PayReconcileTaskService reconcileTaskService;
/**
* 若参数变量名修改 QuartzJobScheduler 中也需对应修改 需要给一个set方法, 让系统设置值
* 要同步的通道
*/
@Setter
private String channel;
@@ -47,6 +47,6 @@ public class PayReconcileTask implements Job {
} else {
date = date.minusDays(1);
}
reconcileTaskService.x1(date,channel);
reconcileTaskService.reconcileTask(date,channel);
}
}

View File

@@ -1,7 +1,6 @@
package cn.bootx.platform.daxpay.service.task.service;
import cn.bootx.platform.daxpay.service.core.order.reconcile.entity.PayReconcileOrder;
import cn.bootx.platform.daxpay.service.core.order.reconcile.service.PayReconcileOrderService;
import cn.bootx.platform.daxpay.service.core.payment.reconcile.service.PayReconcileService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -19,21 +18,20 @@ import java.time.LocalDate;
@RequiredArgsConstructor
public class PayReconcileTaskService {
private final PayReconcileService reconcileService;
private final PayReconcileOrderService reconcileOrderService;
/**
* 执行任务
*/
public void x1(LocalDate date, String channel){
public void reconcileTask(LocalDate date, String channel){
// 1. 查询需要定时对账的通道, 创建出来对账订单
PayReconcileOrder reconcileOrder = reconcileOrderService.create(date, channel);
PayReconcileOrder reconcileOrder = reconcileService.create(date, channel);
// 2. 执行对账任务, 下载对账单并解析, 分别存储为原始数据和通用对账数据
reconcileService.downAndSave(reconcileOrder);
// 3. 执行账单比对, 生成差异单
reconcileService.compare(reconcileOrder);
}
}

View File

@@ -1,4 +1,5 @@
# 将字段上的注解拷贝到对应构造参数上, 如果要新增别的注解, 需要在下方重新进行定义,
lombok.copyableAnnotations += org.springframework.context.annotation.Lazy
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier
# 默认开启实体类的链式API 会导致一些反射相关类库出问题, 如 easyExcel
#lombok.accessors.chain = true

View File

@@ -29,7 +29,7 @@
<java.version>1.8</java.version>
<!-- 二方库版本 -->
<bootx-platform.version>1.3.6.1</bootx-platform.version>
<bootx-platform.version>1.3.6.2</bootx-platform.version>
<daxpay.version>2.0.1</daxpay.version>
<!-- 三方库 -->
<slf4j.version>1.7.30</slf4j.version>