feat 支付退款同步

This commit is contained in:
xxm1995
2024-01-29 16:10:10 +08:00
parent 9c7d49c400
commit 2049905ab9
42 changed files with 546 additions and 118 deletions

View File

@@ -53,9 +53,6 @@ public interface AliPayCode {
/** 退款业务号 */
String OUT_BIZ_NO = "out_biz_no";
/** 退款流水号 */
/** 退款金额 */
String REFUND_FEE = "refund_fee";
@@ -78,6 +75,10 @@ public interface AliPayCode {
/** 交易结束,不可退款 */
String NOTIFY_TRADE_FINISHED = "TRADE_FINISHED";
// 退款状态
/** 退款成功 */
String REFUND_SUCCESS = "REFUND_SUCCESS";
// 错误提示
/** 交易不存在 */

View File

@@ -143,7 +143,7 @@ public class AliPayCallbackService extends AbsCallbackStrategy {
public PayCallbackTypeEnum getCallbackType() {
CallbackLocal callback = PaymentContextLocal.get().getCallbackInfo();
Map<String, String> callbackParam = callback.getCallbackParam();
String refundFee = callbackParam.get("refund_fee");
String refundFee = callbackParam.get(REFUND_FEE);
// 如果有退款金额,说明是退款回调
if (StrUtil.isNotBlank(refundFee)){
return PayCallbackTypeEnum.REFUND;

View File

@@ -1,11 +1,14 @@
package cn.bootx.platform.daxpay.service.core.channel.alipay.service;
import cn.bootx.platform.common.core.util.LocalDateTimeUtil;
import cn.bootx.platform.daxpay.code.PayRefundSyncStatusEnum;
import cn.bootx.platform.daxpay.code.PaySyncStatusEnum;
import cn.bootx.platform.daxpay.service.code.AliPayCode;
import cn.bootx.platform.daxpay.service.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayOrder;
import cn.bootx.platform.daxpay.service.core.order.refund.entity.PayRefundOrder;
import cn.bootx.platform.daxpay.service.core.payment.sync.result.PayGatewaySyncResult;
import cn.bootx.platform.daxpay.service.core.payment.sync.result.RefundGatewaySyncResult;
import cn.hutool.json.JSONUtil;
import com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradeFastpayRefundQueryModel;
@@ -45,7 +48,6 @@ public class AliPaySyncService {
AlipayTradeQueryModel queryModel = new AlipayTradeQueryModel();
queryModel.setOutTradeNo(String.valueOf(payOrder.getId()));
// queryModel.setQueryOptions(Collections.singletonList("trade_settle_info"));
// 查询参数
AlipayTradeQueryResponse response = AliPayApi.tradeQueryToResponse(queryModel);
String tradeStatus = response.getTradeStatus();
syncResult.setSyncInfo(JSONUtil.toJsonStr(response));
@@ -77,7 +79,7 @@ public class AliPaySyncService {
}
}
catch (AlipayApiException e) {
log.error("查询订单失败:", e);
log.error("支付订单同步失败:", e);
syncResult.setErrorMsg(e.getErrMsg());
}
return syncResult;
@@ -86,10 +88,23 @@ public class AliPaySyncService {
/**
* 退款同步查询
*/
private void syncRefundStatus(PayOrder payOrder) throws AlipayApiException {
AlipayTradeFastpayRefundQueryModel queryModel = new AlipayTradeFastpayRefundQueryModel();
queryModel.setOutTradeNo(String.valueOf(payOrder.getId()));
AlipayTradeFastpayRefundQueryResponse response = AliPayApi.tradeRefundQueryToResponse(queryModel);
response.getRefundStatus();
public RefundGatewaySyncResult syncRefundStatus(PayRefundOrder refundOrder) {
RefundGatewaySyncResult syncResult = new RefundGatewaySyncResult().setSyncStatus(PayRefundSyncStatusEnum.FAIL);
try {
AlipayTradeFastpayRefundQueryModel queryModel = new AlipayTradeFastpayRefundQueryModel();
queryModel.setOutTradeNo(String.valueOf(refundOrder.getId()));
AlipayTradeFastpayRefundQueryResponse response = AliPayApi.tradeRefundQueryToResponse(queryModel);
syncResult.setSyncInfo(JSONUtil.toJsonStr(response));
String tradeStatus = response.getRefundStatus();
// 成功
if (Objects.equals(tradeStatus, AliPayCode.NOTIFY_TRADE_SUCCESS)){
return syncResult.setSyncStatus(PayRefundSyncStatusEnum.SUCCESS);
}
} catch (AlipayApiException e) {
log.error("退款订单同步失败:", e);
syncResult.setErrorMsg(e.getErrMsg());
}
return syncResult;
}
}

View File

@@ -2,15 +2,18 @@ package cn.bootx.platform.daxpay.service.core.channel.wechat.service;
import cn.bootx.platform.common.core.util.LocalDateTimeUtil;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.code.PayRefundSyncStatusEnum;
import cn.bootx.platform.daxpay.code.PaySyncStatusEnum;
import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.service.code.WeChatPayCode;
import cn.bootx.platform.daxpay.service.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.service.core.channel.wechat.entity.WeChatPayConfig;
import cn.bootx.platform.daxpay.service.core.order.pay.dao.PayChannelOrderManager;
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayOrder;
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayChannelOrder;
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayOrder;
import cn.bootx.platform.daxpay.service.core.order.refund.entity.PayRefundOrder;
import cn.bootx.platform.daxpay.service.core.payment.sync.result.PayGatewaySyncResult;
import cn.bootx.platform.daxpay.service.core.payment.sync.result.RefundGatewaySyncResult;
import cn.hutool.core.date.DatePattern;
import cn.hutool.json.JSONUtil;
import com.ijpay.core.enums.SignType;
@@ -82,7 +85,8 @@ public class WeChatPaySyncService {
// 已退款/退款中 触发一下退款记录的查询
if (Objects.equals(tradeStatus, WeChatPayCode.PAY_REFUND)) {
this.syncRefundStatus(order, weChatPayConfig);
// this.syncRefundStatus(order, weChatPayConfig);
// TODO 特殊处理, 提示用户走退款同步
}
// 已关闭
if (Objects.equals(tradeStatus, WeChatPayCode.PAY_CLOSED)
@@ -101,28 +105,25 @@ public class WeChatPaySyncService {
/**
* 退款查询
*/
private PayGatewaySyncResult syncRefundStatus(PayOrder order, WeChatPayConfig weChatPayConfig){
PayChannelOrder orderChannel = payChannelOrderManager.findByPaymentIdAndChannel(order.getId(), PayChannelEnum.WECHAT.getCode())
public RefundGatewaySyncResult syncRefundStatus(PayRefundOrder refundOrder, WeChatPayConfig weChatPayConfig){
PayChannelOrder orderChannel = payChannelOrderManager.findByPaymentIdAndChannel(refundOrder.getId(), PayChannelEnum.WECHAT.getCode())
.orElseThrow(() -> new PayFailureException("支付订单通道信息不存在"));
Map<String, String> params = UnifiedOrderModel.builder()
.appid(weChatPayConfig.getWxAppId())
.mch_id(weChatPayConfig.getWxMchId())
.nonce_str(WxPayKit.generateStr())
.out_trade_no(String.valueOf(order.getId()))
.out_trade_no(String.valueOf(refundOrder.getId()))
.build()
.createSign(weChatPayConfig.getApiKeyV2(), SignType.HMACSHA256);
String xmlResult = WxPayApi.orderRefundQuery(false, params);
Map<String, String> result = WxPayKit.xmlToMap(xmlResult);
// 获取
// TODO 处理退款同步的情况
// 判断是否全部退款
Integer refundFee = Integer.valueOf(result.get(WeChatPayCode.REFUND_FEE));
if (Objects.equals(refundFee, orderChannel.getAmount())){
return new PayGatewaySyncResult().setSyncStatus(PaySyncStatusEnum.REFUND);
return new RefundGatewaySyncResult().setSyncStatus(PayRefundSyncStatusEnum.REFUNDING);
}
return new PayGatewaySyncResult().setSyncInfo(JSONUtil.toJsonStr(result));
return new RefundGatewaySyncResult().setSyncInfo(JSONUtil.toJsonStr(result));
}
}

View File

@@ -38,14 +38,14 @@ public class PayOrder extends MpBaseEntity implements EntityBaseFunction<PayOrde
@DbColumn(comment = "标题")
private String title;
/** 是否是异步支付 */
@DbColumn(comment = "是否是异步支付")
private boolean asyncPay;
/** 是否是组合支付 */
@DbColumn(comment = "是否是组合支付")
private boolean combinationPay;
/** 是否是异步支付 */
@DbColumn(comment = "是否是异步支付")
private boolean asyncPay;
/**
* 异步支付通道
* @see PayChannelEnum#ASYNC_TYPE_CODE

View File

@@ -48,15 +48,15 @@ public class PayChannelOrderService {
}
/**
* 切换支付订单关联的异步支付通道
* 切换支付订单关联的异步支付通道, 同时会设置是否支付完成状态
*/
@Transactional(rollbackFor = Exception.class)
public void switchAsyncPayChannel(PayOrder payOrder, PayChannelParam payChannelParam){
AsyncPayLocal asyncPayInfo = PaymentContextLocal.get().getAsyncPayInfo();
// 是否支付完成
PayStatusEnum payStatus = asyncPayInfo.isPayComplete() ? PayStatusEnum.SUCCESS : PayStatusEnum.PROGRESS;
Optional<PayChannelOrder> payOrderChannelOpt =
channelOrderManager.findByPaymentIdAndChannel(payOrder.getId(), payChannelParam.getChannel());
// 判断新发起的
Optional<PayChannelOrder> payOrderChannelOpt = channelOrderManager.findByPaymentIdAndChannel(payOrder.getId(), payChannelParam.getChannel());
if (!payOrderChannelOpt.isPresent()){
PayChannelOrder payChannelOrder = new PayChannelOrder();
// 替换原有的的支付通道信息
@@ -70,7 +70,7 @@ public class PayChannelOrderService {
.setPayTime(LocalDateTime.now())
.setChannelExtra(payChannelParam.getChannelExtra())
.setStatus(payStatus.getCode());
channelOrderManager.deleteByPaymentIdAndAsync(payChannelOrder.getId());
channelOrderManager.deleteByPaymentIdAndAsync(payOrder.getId());
channelOrderManager.save(payChannelOrder);
} else {
// 更新支付通道信息
@@ -94,6 +94,7 @@ public class PayChannelOrderService {
if (Objects.equals(refundChannelOrder.getStatus(), PayRefundStatusEnum.SUCCESS.getCode())){
PayStatusEnum status = refundableBalance == 0 ? PayStatusEnum.REFUNDED : PayStatusEnum.PARTIAL_REFUND;
payChannelOrder.setStatus(status.getCode());
refundChannelOrder.setRefundTime(LocalDateTime.now());
} else {
payChannelOrder.setStatus(PayStatusEnum.REFUNDING.getCode());
}

View File

@@ -1,7 +1,7 @@
package cn.bootx.platform.daxpay.service.core.order.refund.entity;
import cn.bootx.platform.common.core.function.EntityBaseFunction;
import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity;
import cn.bootx.platform.common.mybatisplus.base.MpCreateEntity;
import cn.bootx.platform.daxpay.code.PayRefundStatusEnum;
import cn.bootx.platform.daxpay.service.core.order.refund.convert.RefundOrderChannelConvert;
import cn.bootx.platform.daxpay.service.dto.order.refund.RefundChannelOrderDto;
@@ -12,6 +12,8 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.time.LocalDateTime;
/**
* 支付退款订单关联通道信息
* @author xxm
@@ -22,7 +24,7 @@ import lombok.experimental.Accessors;
@DbTable(comment = "支付退款通道订单")
@Accessors(chain = true)
@TableName("pay_refund_channel_order")
public class PayRefundChannelOrder extends MpBaseEntity implements EntityBaseFunction<RefundChannelOrderDto> {
public class PayRefundChannelOrder extends MpCreateEntity implements EntityBaseFunction<RefundChannelOrderDto> {
@DbColumn(comment = "关联退款id")
private Long refundId;
@@ -49,6 +51,8 @@ public class PayRefundChannelOrder extends MpBaseEntity implements EntityBaseFun
@DbColumn(comment = "退款状态")
private String status;
@DbColumn(comment = "退款时间")
private LocalDateTime refundTime;
/**
* 转换

View File

@@ -2,6 +2,7 @@ package cn.bootx.platform.daxpay.service.core.order.refund.entity;
import cn.bootx.platform.common.core.function.EntityBaseFunction;
import cn.bootx.platform.common.mybatisplus.base.MpBaseEntity;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.code.PayRefundStatusEnum;
import cn.bootx.platform.daxpay.service.core.order.refund.convert.PayRefundOrderConvert;
import cn.bootx.platform.daxpay.service.dto.order.refund.PayRefundOrderDto;
@@ -41,6 +42,17 @@ public class PayRefundOrder extends MpBaseEntity implements EntityBaseFunction<P
@DbColumn(comment = "退款号")
private String refundNo;
/** 退款时是否是含有异步通道 */
@DbColumn(comment = "是否含有异步通道")
private boolean asyncPay;
/**
* 异步通道
* @see PayChannelEnum#ASYNC_TYPE_CODE
*/
@DbColumn(comment = "异步通道")
private String asyncChannel;
/** 如果有异步通道, 保存关联的网关订单号 */
@DbColumn(comment = "网关订单号")
private String gatewayOrderNo;

View File

@@ -36,7 +36,7 @@ import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class PayRefundQueryService {
public class PayRefundOrderQueryService {
private final PayRefundOrderManager refundOrderManager;
private final PayRefundChannelOrderManager refundOrderChannelManager;

View File

@@ -0,0 +1,33 @@
package cn.bootx.platform.daxpay.service.core.order.refund.service;
import cn.bootx.platform.common.core.exception.DataNotExistException;
import cn.bootx.platform.daxpay.service.core.order.refund.dao.PayRefundOrderManager;
import cn.bootx.platform.daxpay.service.core.order.refund.entity.PayRefundOrder;
import cn.bootx.platform.daxpay.service.core.payment.sync.service.PayRefundSyncService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 退款订单服务类
* @author xxm
* @since 2024/1/29
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class PayRefundOrderService {
private final PayRefundOrderManager refundOrderManager;
private final PayRefundSyncService refundSyncService;;
/**
* 退款同步
*/
public void syncById(Long id){
PayRefundOrder refundOrder = refundOrderManager.findById(id)
.orElseThrow(() -> new DataNotExistException("退款订单不存在"));
refundSyncService.syncPayOrder(refundOrder);
}
}

View File

@@ -192,12 +192,11 @@ public class PayAssistService {
// 退款类型状态
tradesStatus = Arrays.asList(REFUNDED.getCode(), PARTIAL_REFUND.getCode(), REFUNDING.getCode());
if (tradesStatus.contains(payOrder.getStatus())) {
throw new PayFailureException("退款中");
throw new PayFailureException("该订单处于退款状态");
}
// 其他状态直接抛出兜底异常
throw new PayFailureException("订单不是待支付状态,请重新确认订单状态");
}
return null;
}
}

View File

@@ -2,6 +2,8 @@ package cn.bootx.platform.daxpay.service.core.payment.refund.service;
import cn.bootx.platform.common.core.exception.ValidationFailedException;
import cn.bootx.platform.common.core.util.CollUtil;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.code.PayRefundStatusEnum;
import cn.bootx.platform.daxpay.code.PayStatusEnum;
import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.param.pay.RefundChannelParam;
@@ -27,6 +29,7 @@ import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 支付退款支撑服务
@@ -84,9 +87,9 @@ public class PayRefundAssistService {
}
/**
* 根据退款参数获取支付订单, 并进行检查
* 检查并处理退款参数
*/
public void checkByRefundParam(RefundParam param, PayOrder payOrder){
public void checkAndDisposeParam(RefundParam param, PayOrder payOrder){
// 全额退款和部分退款校验
if (!param.isRefundAll()) {
if (CollUtil.isEmpty(param.getRefundChannels())) {
@@ -112,6 +115,13 @@ public class PayRefundAssistService {
throw new PayFailureException("当前状态["+statusEnum.getName()+"]不允许状态非法, 无法退款");
}
// 过滤掉金额为0的退款参数
List<RefundChannelParam> channelParams = param.getRefundChannels()
.stream()
.filter(r -> r.getAmount() > 0)
.collect(Collectors.toList());
param.setRefundChannels(channelParams);
// 退款号唯一校验
if (StrUtil.isNotBlank(param.getRefundNo())
&& payRefundOrderManager.existsByRefundNo(param.getRefundNo())){
@@ -140,11 +150,28 @@ public class PayRefundAssistService {
.setRefundTime(LocalDateTime.now())
.setTitle(payOrder.getTitle())
.setGatewayOrderNo(asyncRefundInfo.getGatewayOrderNo())
.setErrorCode(asyncRefundInfo.getErrorCode())
.setErrorMsg(asyncRefundInfo.getErrorMsg())
.setStatus(asyncRefundInfo.getStatus().getCode())
.setClientIp(refundParam.getClientIp())
.setReqId(PaymentContextLocal.get().getRequestInfo().getReqId());
// 错误状态特殊处理
if (asyncRefundInfo.getStatus() == PayRefundStatusEnum.FAIL){
refundOrder.setErrorCode(asyncRefundInfo.getErrorCode());
refundOrder.setErrorMsg(asyncRefundInfo.getErrorMsg());
// 退款失败不保存剩余可退余额, 否则数据看起开会产生困惑
refundOrder.setRefundableBalance(null);
}
// 退款参数中是否存在异步通道
RefundChannelParam asyncChannel = refundParam.getRefundChannels()
.stream()
.filter(r -> PayChannelEnum.ASYNC_TYPE_CODE.contains(r.getChannel()))
.findFirst()
.orElse(null);
if (Objects.nonNull(asyncChannel)){
refundOrder.setAsyncChannel(asyncChannel.getChannel());
refundOrder.setAsyncPay(true);
}
// 主键使用预先生成的ID, 如果有异步通道, 关联的退款号就是这个ID
long refundId = asyncRefundInfo.getRefundId();
refundOrder.setId(refundId);

View File

@@ -86,7 +86,7 @@ public class PayRefundService {
// 获取支付订单
PayOrder payOrder = payRefundAssistService.getPayOrder(param);
// 第一次检查退款参数, 校验一些特殊情况
payRefundAssistService.checkByRefundParam(param, payOrder);
payRefundAssistService.checkAndDisposeParam(param, payOrder);
// 组装退款参数, 处理全部退款和简单退款情况
List<PayChannelOrder> payChannelOrders = payChannelOrderManager.findAllByPaymentId(payOrder.getId());
@@ -126,6 +126,7 @@ public class PayRefundService {
/**
* 分支付通道进行退款
* TODO 增加错误处理, 目前出现错误后存储的数据不全
*/
public RefundResult refundByChannel(RefundParam refundParam, PayOrder payOrder, List<PayChannelOrder> payChannelOrders){
// 0.基础数据准备, 并比对通道支付单是否与可退款记录数量一致

View File

@@ -12,6 +12,8 @@ import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/**

View File

@@ -12,6 +12,8 @@ import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/**
@@ -68,6 +70,7 @@ public class WeChatPayRefundStrategy extends AbsRefundStrategy {
.getRefundInfo()
.getStatus();
this.getRefundChannelOrder().setStatus(refundStatusEnum.getCode());
// 更新支付通道订单中的属性
payChannelOrderService.updateAsyncPayRefund(this.getPayChannelOrder(), this.getRefundChannelOrder());
}

View File

@@ -2,6 +2,7 @@ package cn.bootx.platform.daxpay.service.core.payment.repair.service;
import cn.bootx.platform.common.core.function.CollectorsFunction;
import cn.bootx.platform.daxpay.code.PayStatusEnum;
import cn.bootx.platform.daxpay.service.code.PayRepairPayTypeEnum;
import cn.bootx.platform.daxpay.service.code.PayRepairWayEnum;
import cn.bootx.platform.daxpay.service.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.service.core.order.pay.dao.PayChannelOrderManager;
@@ -160,6 +161,7 @@ public class PayRepairService {
.setOrderNo(order.getBusinessNo())
.setBeforeStatus(repairResult.getBeforeStatus().getCode())
.setAfterStatus(afterStatus)
.setRepairType(PayRepairPayTypeEnum.PAY.getCode())
.setRepairSource(source)
.setRepairWay(recordType.getCode());
payRepairRecord.setId(repairResult.getRepairId());

View File

@@ -23,6 +23,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;
@@ -106,7 +107,8 @@ public class RefundRepairService {
}
// 设置退款为完成状态
refundOrder.setStatus(PayRefundStatusEnum.SUCCESS.getCode());
refundChannelOrder.setStatus(PayRefundStatusEnum.SUCCESS.getCode());
refundChannelOrder.setStatus(PayRefundStatusEnum.SUCCESS.getCode())
.setRefundTime(LocalDateTime.now());
payOrder.setStatus(afterPayRefundStatus.getCode());
// 更新订单和退款相关订单
payChannelOrderManager.updateById(payChannelOrder);
@@ -175,6 +177,8 @@ public class RefundRepairService {
/**
* 支付订单的修复记录
* 支付完成 -> 退款
* 退款 -> 全部退款
*/
private PayRepairRecord payRepairRecord(PayOrder order, RefundRepairWayEnum repairType, RefundRepairResult repairResult){
// 修复后的状态
@@ -184,18 +188,20 @@ public class RefundRepairService {
.getRepairInfo()
.getSource().getCode();
return new PayRepairRecord()
.setRepairId(repairResult.getRepairId())
.setOrderId(order.getId())
.setRepairType(PayRepairPayTypeEnum.PAY.getCode())
.setRepairSource(source)
.setRepairWay(repairType.getCode())
.setAsyncChannel(order.getAsyncChannel())
.setOrderNo(order.getBusinessNo())
.setBeforeStatus(repairResult.getAfterPayStatus().getCode())
.setAfterStatus(afterStatus)
.setRepairSource(source)
.setRepairWay(repairType.getCode());
.setAfterStatus(afterStatus);
}
/**
* 退款订单的修复记录
* 退款中 -> 退款成功
*/
private PayRepairRecord refundRepairRecord(PayRefundOrder refundOrder, RefundRepairWayEnum repairType, RefundRepairResult repairResult){
// 修复后的状态
@@ -206,6 +212,7 @@ public class RefundRepairService {
.getSource().getCode();
return new PayRepairRecord()
.setOrderId(refundOrder.getId())
.setRepairId(repairResult.getRepairId())
.setOrderNo(refundOrder.getRefundNo())
.setRepairType(PayRepairPayTypeEnum.REFUND.getCode())
.setBeforeStatus(repairResult.getBeforeRefundStatus().getCode())

View File

@@ -3,8 +3,8 @@ package cn.bootx.platform.daxpay.service.core.payment.sync.factory;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.service.func.AbsPaySyncStrategy;
import cn.bootx.platform.daxpay.service.core.payment.sync.strategy.AliPaySyncStrategy;
import cn.bootx.platform.daxpay.service.core.payment.sync.strategy.WeChatPaySyncStrategy;
import cn.bootx.platform.daxpay.service.core.payment.sync.strategy.pay.AliPaySyncStrategy;
import cn.bootx.platform.daxpay.service.core.payment.sync.strategy.pay.WeChatPaySyncStrategy;
import cn.bootx.platform.daxpay.exception.pay.PayUnsupportedMethodException;
import cn.hutool.extra.spring.SpringUtil;

View File

@@ -0,0 +1,39 @@
package cn.bootx.platform.daxpay.service.core.payment.sync.factory;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.exception.pay.PayUnsupportedMethodException;
import cn.bootx.platform.daxpay.service.core.payment.sync.strategy.Refund.AliRefundSyncStrategy;
import cn.bootx.platform.daxpay.service.core.payment.sync.strategy.Refund.WeChatRefundSyncStrategy;
import cn.bootx.platform.daxpay.service.func.AbsRefundSyncStrategy;
import cn.hutool.extra.spring.SpringUtil;
import lombok.experimental.UtilityClass;
/**
* 支付退款同步策略工厂
* @author xxm
* @since 2024/1/29
*/
@UtilityClass
public class RefundSyncStrategyFactory {
/**
* 获取支付同步策略, 只有异步支付方式才需要这个功能
* @param channelCode 支付通道编码
* @return 支付同步策略类
*/
public static AbsRefundSyncStrategy create(String channelCode) {
AbsRefundSyncStrategy strategy;
PayChannelEnum channelEnum = PayChannelEnum.findByCode(channelCode);
switch (channelEnum) {
case ALI:
strategy = SpringUtil.getBean(AliRefundSyncStrategy.class);
break;
case WECHAT:
strategy = SpringUtil.getBean(WeChatRefundSyncStrategy.class);
break;
default:
throw new PayUnsupportedMethodException();
}
// noinspection ConstantConditions
return strategy;
}
}

View File

@@ -22,9 +22,12 @@ public class PayGatewaySyncResult {
*/
private PaySyncStatusEnum syncStatus = FAIL;
/** 同步支付时网关返回的对象, 序列化为json字符串 */
/** 同步时网关返回的对象, 序列化为json字符串 */
private String syncInfo;
/** 错误提示码 */
private String errorCode;
/** 错误提示 */
private String errorMsg;

View File

@@ -1,8 +1,11 @@
package cn.bootx.platform.daxpay.service.core.payment.sync.result;
import cn.bootx.platform.daxpay.code.PayRefundSyncStatusEnum;
import lombok.Data;
import lombok.experimental.Accessors;
import static cn.bootx.platform.daxpay.code.PayRefundSyncStatusEnum.FAIL;
/**
* 支付退款同步结果
* @author xxm
@@ -11,4 +14,19 @@ import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class RefundGatewaySyncResult {
/**
* 支付网关订单状态
* @see PayRefundSyncStatusEnum
*/
private PayRefundSyncStatusEnum syncStatus = FAIL;
/** 同步时网关返回的对象, 序列化为json字符串 */
private String syncInfo;
/** 错误提示码 */
private String errorCode;
/** 错误提示 */
private String errorMsg;
}

View File

@@ -0,0 +1,108 @@
package cn.bootx.platform.daxpay.service.core.payment.sync.service;
import cn.bootx.platform.common.core.exception.RepetitiveOperationException;
import cn.bootx.platform.daxpay.code.PayRefundSyncStatusEnum;
import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.param.pay.RefundSyncParam;
import cn.bootx.platform.daxpay.service.core.order.refund.dao.PayRefundOrderManager;
import cn.bootx.platform.daxpay.service.core.order.refund.entity.PayRefundOrder;
import cn.bootx.platform.daxpay.service.core.payment.sync.factory.RefundSyncStrategyFactory;
import cn.bootx.platform.daxpay.service.core.payment.sync.result.RefundGatewaySyncResult;
import cn.bootx.platform.daxpay.service.func.AbsRefundSyncStrategy;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
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.util.Objects;
/**
* 支付退款同步服务类
* @author xxm
* @since 2024/1/29
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class PayRefundSyncService {
private final PayRefundOrderManager refundOrderManager;
private final LockTemplate lockTemplate;
/**
* 退款同步, 开启一个新的事务, 不受外部抛出异常的影响
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sync(RefundSyncParam param){
// 先获取退款单
PayRefundOrder requestOrder;
if (Objects.nonNull(param.getRefundId())){
requestOrder = refundOrderManager.findById(param.getRefundId())
.orElseThrow(() -> new PayFailureException("未查询到退款订单"));
} else {
requestOrder = refundOrderManager.findByRefundNo(param.getRefundNo())
.orElseThrow(() -> new PayFailureException("未查询到退款订单"));
}
// 如果不是异步支付, 直接返回返回
if (!requestOrder.isAsyncPay()){
// TODO 需要限制同步的请求不进行同步
return;
}
this.syncPayOrder(requestOrder);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void syncPayOrder(PayRefundOrder refundOrder) {
// 加锁
LockInfo lock = lockTemplate.lock("sync:refund:" + refundOrder.getId());
if (Objects.isNull(lock)) {
throw new RepetitiveOperationException("退款同步处理中,请勿重复操作");
}
try {
// 获取支付同步策略类
AbsRefundSyncStrategy syncPayStrategy = RefundSyncStrategyFactory.create(refundOrder.getAsyncChannel());
syncPayStrategy.initRefundParam(refundOrder);
// 执行操作, 获取支付网关同步的结果
RefundGatewaySyncResult syncResult = syncPayStrategy.doSyncStatus();
// 判断是否同步成功
if (Objects.equals(syncResult.getSyncStatus(), PayRefundSyncStatusEnum.FAIL)) {
// 同步失败, 返回失败响应, 同时记录失败的日志
// return new PaySyncResult().setErrorMsg(syncResult.getErrorMsg());
log.error("同步失败");
return;
}
// 判断网关状态是否和支付单一致, 同时特定情况下更新网关同步状态
// boolean statusSync = this.checkAndAdjustSyncStatus(syncResult, payOrder);
// PayRepairResult repairResult = new PayRepairResult();
// try {
// // 状态不一致,执行支付单修复逻辑
// if (!statusSync) {
// repairResult = this.resultHandler(syncResult, payOrder);
// }
// } catch (PayFailureException e) {
// // 同步失败, 返回失败响应, 同时记录失败的日志
// syncResult.setSyncStatus(PaySyncStatusEnum.FAIL);
// this.saveRecord(payOrder, syncResult, false, null, e.getMessage());
// return new PaySyncResult().setErrorMsg(e.getMessage());
// }
//
// // 同步成功记录日志
// this.saveRecord(payOrder, syncResult, !statusSync, repairResult.getRepairId(), null);
// return new PaySyncResult()
// .setGatewayStatus(syncResult.getSyncStatus()
// .getCode())
// .setSuccess(true)
// .setRepair(!statusSync)
// .setRepairId(repairResult.getRepairId());
} finally {
lockTemplate.releaseLock(lock);
}
}
}

View File

@@ -79,11 +79,12 @@ public class PaySyncService {
* 同步支付状态, 开启一个新的事务, 不受外部抛出异常的影响
* 1. 如果状态一致, 不进行处理
* 2. 如果状态不一致, 调用修复逻辑进行修复
* todo 需要进行异常处理, 现在会有 Transaction rolled back because it has been marked as rollback-only 问题
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public PaySyncResult syncPayOrder(PayOrder payOrder) {
// 加锁
LockInfo lock = lockTemplate.lock("payment:refund:" + payOrder.getId());
LockInfo lock = lockTemplate.lock("sync:payment" + payOrder.getId());
if (Objects.isNull(lock)){
throw new RepetitiveOperationException("支付同步处理中,请勿重复操作");
}

View File

@@ -0,0 +1,49 @@
package cn.bootx.platform.daxpay.service.core.payment.sync.strategy.Refund;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.code.PaySyncStatusEnum;
import cn.bootx.platform.daxpay.service.core.channel.alipay.entity.AliPayConfig;
import cn.bootx.platform.daxpay.service.core.channel.alipay.service.AliPayConfigService;
import cn.bootx.platform.daxpay.service.core.channel.alipay.service.AliPaySyncService;
import cn.bootx.platform.daxpay.service.core.payment.sync.result.RefundGatewaySyncResult;
import cn.bootx.platform.daxpay.service.func.AbsRefundSyncStrategy;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/**
* 支付宝退款同步策略
* @author xxm
* @since 2024/1/29
*/
@Scope(SCOPE_PROTOTYPE)
@Component
@RequiredArgsConstructor
public class AliRefundSyncStrategy extends AbsRefundSyncStrategy {
private final AliPayConfigService alipayConfigService;
private final AliPaySyncService aliPaySyncService;;
/**
* 策略标识
*/
@Override
public PayChannelEnum getChannel() {
return PayChannelEnum.ALI;
}
/**
* 异步支付单与支付网关进行状态比对后的结果
*
* @see PaySyncStatusEnum
*/
@Override
public RefundGatewaySyncResult doSyncStatus() {
AliPayConfig config = alipayConfigService.getConfig();
alipayConfigService.initConfig(config);
return aliPaySyncService.syncRefundStatus(this.getRefundOrder());
}
}

View File

@@ -0,0 +1,38 @@
package cn.bootx.platform.daxpay.service.core.payment.sync.strategy.Refund;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.service.core.payment.sync.result.RefundGatewaySyncResult;
import cn.bootx.platform.daxpay.service.func.AbsRefundSyncStrategy;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROTOTYPE;
/**
* 微信退款同步策略
* @author xxm
* @since 2024/1/29
*/
@Scope(SCOPE_PROTOTYPE)
@Component
@RequiredArgsConstructor
public class WeChatRefundSyncStrategy extends AbsRefundSyncStrategy {
/**
* 策略标识
*/
@Override
public PayChannelEnum getChannel() {
return PayChannelEnum.WECHAT;
}
/**
* 异步支付单与支付网关进行状态比对后的结果
*/
@Override
public RefundGatewaySyncResult doSyncStatus() {
return null;
}
}

View File

@@ -1,4 +1,4 @@
package cn.bootx.platform.daxpay.service.core.payment.sync.strategy;
package cn.bootx.platform.daxpay.service.core.payment.sync.strategy.pay;
import cn.bootx.platform.daxpay.code.PayChannelEnum;

View File

@@ -1,4 +1,4 @@
package cn.bootx.platform.daxpay.service.core.payment.sync.strategy;
package cn.bootx.platform.daxpay.service.core.payment.sync.strategy.pay;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.service.core.channel.wechat.entity.WeChatPayConfig;

View File

@@ -1,6 +1,5 @@
package cn.bootx.platform.daxpay.service.core.timeout.task;
import cn.bootx.platform.common.core.exception.RepetitiveOperationException;
import cn.bootx.platform.daxpay.param.pay.PaySyncParam;
import cn.bootx.platform.daxpay.service.core.payment.sync.service.PaySyncService;
import cn.bootx.platform.daxpay.service.core.timeout.dao.PayExpiredTimeRepository;
@@ -8,6 +7,7 @@ import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@@ -29,14 +29,19 @@ public class PayExpiredTimeTask {
private final LockTemplate lockTemplate;
// @Scheduled(cron = "*/5 * * * * ?")
/**
* 先使用定时任务实现, 五秒轮训一下
*
*/
@Scheduled(cron = "*/5 * * * * ?")
public void task(){
log.debug("执行超时取消任务....");
Set<String> expiredKeys = repository.getExpiredKeys(LocalDateTime.now());
for (String expiredKey : expiredKeys) {
LockInfo lock = lockTemplate.lock("payment:expired:" + expiredKey,10000,0);
if (Objects.isNull(lock)){
throw new RepetitiveOperationException("支付同步处理中,请勿重复操作");
log.warn("支付同步处理中,执行下一个");
continue;
}
try {
// 执行同步操作, 网关同步时会对支付的进行状态的处理

View File

@@ -14,7 +14,7 @@ import java.util.Objects;
import java.util.Set;
/**
*
* 待支付订单的状态同步, 先不进行启用
* @author xxm
* @since 2024/1/5
*/

View File

@@ -1,6 +1,7 @@
package cn.bootx.platform.daxpay.service.dto.order.refund;
import cn.bootx.platform.common.core.rest.dto.BaseDto;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.code.PayRefundStatusEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -31,6 +32,16 @@ public class PayRefundOrderDto extends BaseDto {
@Schema(description = "退款号")
private String refundNo;
@Schema(description = "是否含有异步通道")
private boolean asyncPay;
/**
* 异步通道
* @see PayChannelEnum#ASYNC_TYPE_CODE
*/
@Schema(description = "异步通道")
private String asyncChannel;
@Schema(description = "支付网关订单号")
private String gatewayOrderNo;

View File

@@ -1,6 +1,7 @@
package cn.bootx.platform.daxpay.service.dto.order.refund;
import cn.bootx.platform.common.core.rest.dto.BaseDto;
import cn.bootx.platform.daxpay.code.PayRefundStatusEnum;
import cn.bootx.table.modify.annotation.DbColumn;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -36,4 +37,11 @@ public class RefundChannelOrderDto extends BaseDto {
@Schema(description = "退款金额")
private Integer amount;
/**
* 退款状态
* @see PayRefundStatusEnum
*/
@Schema(description = "退款状态")
private String status;
}

View File

@@ -14,6 +14,7 @@ import lombok.extern.slf4j.Slf4j;
import javax.annotation.Resource;
import java.util.Map;
import java.util.Optional;
/**
* 回调处理抽象类, 处理支付回调和退款回调
@@ -32,35 +33,42 @@ public abstract class AbsCallbackStrategy implements PayStrategy {
/**
* 回调处理入口
* TODO 需要处理异常情况进行保存
*/
public String callback(Map<String, String> params) {
// 将参数写入到上下文中
CallbackLocal callbackInfo = PaymentContextLocal.get().getCallbackInfo();
callbackInfo.getCallbackParam().putAll(params);
// 验证消息
if (!this.verifyNotify()) {
callbackInfo.setCallbackStatus(PayCallbackStatusEnum.FAIL).setMsg("验证信息格式不通过");
// 消息有问题, 保存记录并返回
this.saveCallbackRecord();
return null;
}
// 提前设置订单修复的来源
PaymentContextLocal.get().getRepairInfo().setSource(PayRepairSourceEnum.CALLBACK);
try {
// 将参数写入到上下文中
callbackInfo.getCallbackParam().putAll(params);
// 验证消息
if (!this.verifyNotify()) {
callbackInfo.setCallbackStatus(PayCallbackStatusEnum.FAIL).setMsg("验证信息格式不通过");
// 消息有问题, 保存记录并返回
this.saveCallbackRecord();
return null;
}
// 提前设置订单修复的来源
PaymentContextLocal.get().getRepairInfo().setSource(PayRepairSourceEnum.CALLBACK);
// 判断回调类型
PayCallbackTypeEnum callbackType = this.getCallbackType();
if (callbackType == PayCallbackTypeEnum.PAY){
// 解析支付数据并放处理
this.resolvePayData();
payCallbackService.payCallback();
} else {
// 解析退款数据并放处理
this.resolveRefundData();
refundCallbackService.refundCallback();
// 判断回调类型
PayCallbackTypeEnum callbackType = this.getCallbackType();
if (callbackType == PayCallbackTypeEnum.PAY){
// 解析支付数据并放处理
this.resolvePayData();
payCallbackService.payCallback();
} else {
// 解析退款数据并放处理
this.resolveRefundData();
refundCallbackService.refundCallback();
}
this.saveCallbackRecord();
return this.getReturnMsg();
} catch (Exception e) {
log.error("回调处理失败", e);
callbackInfo.setCallbackStatus(PayCallbackStatusEnum.FAIL).setMsg("回调处理失败: "+e.getMessage());
this.saveCallbackRecord();
throw e;
}
// 记录回调记录
this.saveCallbackRecord();
return this.getReturnMsg();
}
/**
@@ -94,12 +102,18 @@ public abstract class AbsCallbackStrategy implements PayStrategy {
*/
public void saveCallbackRecord() {
CallbackLocal callbackInfo = PaymentContextLocal.get().getCallbackInfo();
// 回调类型
String callbackType = Optional.ofNullable(this.getCallbackType())
.map(PayCallbackTypeEnum::getCode)
.orElse(null);
PayCallbackRecord payNotifyRecord = new PayCallbackRecord()
.setPayChannel(this.getChannel().getCode())
.setNotifyInfo(JSONUtil.toJsonStr(callbackInfo.getCallbackParam()))
.setOrderId(callbackInfo.getOrderId())
.setGatewayOrderNo(callbackInfo.getGatewayOrderNo())
.setCallbackType(this.getCallbackType().getCode())
.setCallbackType(callbackType)
.setRepairOrderId(callbackInfo.getPayRepairId())
.setStatus(callbackInfo.getCallbackStatus().getCode())
.setMsg(callbackInfo.getMsg());

View File

@@ -4,6 +4,7 @@ import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayChannelOrder;
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayOrder;
import cn.bootx.platform.daxpay.service.core.order.refund.entity.PayRefundChannelOrder;
import cn.bootx.platform.daxpay.service.core.order.refund.entity.PayRefundOrder;
import cn.bootx.platform.daxpay.service.core.payment.sync.result.RefundGatewaySyncResult;
import lombok.Getter;
/**
@@ -31,15 +32,10 @@ public abstract class AbsRefundRepairStrategy implements PayStrategy{
this.payChannelOrder = payChannelOrder;
}
/**
* 修复前处理
*/
public void doBeforeHandler(){
}
/**
* 支付成功修复
* 异步支付单与支付网关进行状态比对后的结果
*/
public abstract RefundGatewaySyncResult doSyncStatus();
}

View File

@@ -10,6 +10,8 @@ import cn.bootx.platform.daxpay.service.core.order.refund.entity.PayRefundChanne
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
/**
* 抽象支付退款策略基类
*
@@ -59,7 +61,8 @@ public abstract class AbsRefundStrategy implements PayStrategy{
*/
public void doSuccessHandler() {
// 更新退款订单数据状态
this.refundChannelOrder.setStatus(PayRefundStatusEnum.SUCCESS.getCode());
this.refundChannelOrder.setStatus(PayRefundStatusEnum.SUCCESS.getCode())
.setRefundTime(LocalDateTime.now());
// 支付通道订单客可退余额
int refundableBalance = this.getPayChannelOrder().getRefundableBalance() - this.refundChannelOrder.getAmount();

View File

@@ -2,7 +2,7 @@ package cn.bootx.platform.daxpay.service.func;
import cn.bootx.platform.daxpay.code.PaySyncStatusEnum;
import cn.bootx.platform.daxpay.service.core.order.refund.entity.PayRefundOrder;
import cn.bootx.platform.daxpay.service.core.payment.sync.result.PayGatewaySyncResult;
import cn.bootx.platform.daxpay.service.core.payment.sync.result.RefundGatewaySyncResult;
import lombok.Getter;
/**
@@ -18,7 +18,7 @@ public abstract class AbsRefundSyncStrategy implements PayStrategy{
/**
* 初始化参数
*/
public void initParam(PayRefundOrder refundOrder){
public void initRefundParam(PayRefundOrder refundOrder){
this.refundOrder = refundOrder;
}
@@ -26,5 +26,5 @@ public abstract class AbsRefundSyncStrategy implements PayStrategy{
* 异步支付单与支付网关进行状态比对后的结果
* @see PaySyncStatusEnum
*/
public abstract PayGatewaySyncResult doSyncStatus();
public abstract RefundGatewaySyncResult doSyncStatus();
}