feat 支付退款改为二阶段提交, 支付宝支付查询

This commit is contained in:
xxm1995
2024-01-30 16:50:46 +08:00
parent 2049905ab9
commit d2680b3dfd
40 changed files with 344 additions and 233 deletions

View File

@@ -89,10 +89,14 @@
- 2024-01-28:
- [x] 支付宝对账单下载异常排查-支付宝每日都会生成对账单, 哪怕为空, 也会生成
- [x] 订单修复记录前端显示调整
- 2044-01-29:
- 2044-01-30:
- [x] 退款接口更改为先落库, 后更新
- [ ] 增加退款同步策略, 对退款中的状态的退款订单进行处理
- [ ] 退款操作支持重试
- [ ] 支付通道对出现疑似退款的订单进行**报错提醒**, 通过退款同步进行补偿
- **任务池**
2.0.1 版本内容
- [ ] 支付流程也改为先落库后支付情况, 避免极端情况导致掉单
**任务池**
- [ ] 微信退款状态不一致补偿
- [ ] 支付SDK编写
- [ ] 接入支付网关的演示项目

View File

@@ -6,6 +6,7 @@ import cn.bootx.platform.common.core.rest.ResResult;
import cn.bootx.platform.common.core.rest.param.PageParam;
import cn.bootx.platform.common.spring.util.WebServletUtil;
import cn.bootx.platform.daxpay.param.pay.RefundParam;
import cn.bootx.platform.daxpay.result.pay.PaySyncResult;
import cn.bootx.platform.daxpay.service.core.order.refund.service.PayRefundOrderQueryService;
import cn.bootx.platform.daxpay.service.core.order.refund.service.PayRefundOrderService;
import cn.bootx.platform.daxpay.service.core.payment.refund.service.PayRefundService;
@@ -83,8 +84,7 @@ public class PayRefundOrderController {
@Operation(summary = "退款同步")
@PostMapping("/syncById")
public ResResult<Void> syncById(Long ID){
payRefundOrderService.syncById(ID);
return Res.ok();
public ResResult<PaySyncResult> syncById(Long id){
return Res.ok(payRefundOrderService.syncById(id));
}
}

View File

@@ -20,9 +20,8 @@ public enum PayRefundStatusEnum {
* 接口调用成功不代表成功
*/
PROGRESS("progress","退款中"),
/** 部分成功 */
PART_SUCCESS("part_success","部分成功"),
SUCCESS("success","成功"),
CLOSE("close","关闭"),
FAIL("fail","失败");
/** 编码 */

View File

@@ -33,7 +33,7 @@ public class PaySyncResult extends CommonResult {
@Schema(description = "是否进行了修复")
private boolean repair;
@Schema(description = "支付单修复ID")
@Schema(description = "修复ID")
private Long repairId;
@Schema(description = "失败原因")

View File

@@ -13,4 +13,15 @@ import lombok.experimental.Accessors;
@Accessors(chain = true)
@Schema(title = "退款同步结果")
public class RefundSyncResult {
@Schema(description = "是否进行了修复")
private boolean repair;
@Schema(description = "支付单修复ID")
private Long repairId;
@Schema(description = "失败原因")
private String errorMsg;
}

View File

@@ -1,20 +0,0 @@
package cn.bootx.platform.daxpay.service.code;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 回调类型枚举
* @author xxm
* @since 2024/1/24
*/
@Getter
@AllArgsConstructor
public enum PayCallbackTypeEnum {
PAY("pay", "支付回调"),
REFUND("refund", "退款回调");
public final String code;
public final String name;
}

View File

@@ -4,16 +4,17 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 支付修复时的支付/退款类型
* 支付系统中常见的操作类型, 支付/退款/转账等
* @author xxm
* @since 2024/1/28
*/
@Getter
@AllArgsConstructor
public enum PayRepairPayTypeEnum {
public enum PaymentTypeEnum {
PAY("pay","支付"),
REFUND("refund","退款");
REFUND("refund","退款"),
TRANSFER("transfer","转账");
private final String code;
private final String name;

View File

@@ -1,7 +1,6 @@
package cn.bootx.platform.daxpay.service.common.context;
import cn.bootx.platform.daxpay.code.PayRefundStatusEnum;
import cn.hutool.core.util.IdUtil;
import lombok.Data;
import lombok.experimental.Accessors;
@@ -19,11 +18,6 @@ public class RefundLocal {
*/
private String gatewayOrderNo;
/**
* 支付退款ID, 用于异步支付时传入的退款号, 使用退款单ID
*/
private long refundId = IdUtil.getSnowflakeNextId();
/**
* 退款状态, 默认为成功, 通常含有异步支付时, 才会出现别的状态
*/

View File

@@ -4,7 +4,7 @@ import cn.bootx.platform.common.core.util.CertUtil;
import cn.bootx.platform.common.core.util.LocalDateTimeUtil;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.code.PayStatusEnum;
import cn.bootx.platform.daxpay.service.code.PayCallbackTypeEnum;
import cn.bootx.platform.daxpay.service.code.PaymentTypeEnum;
import cn.bootx.platform.daxpay.service.common.context.CallbackLocal;
import cn.bootx.platform.daxpay.service.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.service.core.channel.alipay.entity.AliPayConfig;
@@ -137,18 +137,18 @@ public class AliPayCallbackService extends AbsCallbackStrategy {
/**
* 判断类型 支付回调/退款回调
*
* @see PayCallbackTypeEnum
* @see PaymentTypeEnum
*/
@Override
public PayCallbackTypeEnum getCallbackType() {
public PaymentTypeEnum getCallbackType() {
CallbackLocal callback = PaymentContextLocal.get().getCallbackInfo();
Map<String, String> callbackParam = callback.getCallbackParam();
String refundFee = callbackParam.get(REFUND_FEE);
// 如果有退款金额,说明是退款回调
if (StrUtil.isNotBlank(refundFee)){
return PayCallbackTypeEnum.REFUND;
return PaymentTypeEnum.REFUND;
} else {
return PayCallbackTypeEnum.PAY;
return PaymentTypeEnum.PAY;
}
}

View File

@@ -5,7 +5,7 @@ import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.service.code.AliPayCode;
import cn.bootx.platform.daxpay.service.common.context.RefundLocal;
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 com.alipay.api.AlipayApiException;
import com.alipay.api.domain.AlipayTradeRefundModel;
import com.alipay.api.response.AlipayTradeRefundResponse;
@@ -29,16 +29,16 @@ public class AliPayRefundService {
/**
* 退款, 调用支付宝退款
*/
public void refund(PayOrder payOrder, int amount) {
public void refund(PayRefundOrder refundOrder, int amount) {
RefundLocal refundInfo = PaymentContextLocal.get().getRefundInfo();
AlipayTradeRefundModel refundModel = new AlipayTradeRefundModel();
refundModel.setOutTradeNo(String.valueOf(payOrder.getId()));
refundModel.setOutTradeNo(String.valueOf(refundOrder.getPaymentId()));
refundModel.setOutRequestNo(String.valueOf(refundOrder.getId()));
// 金额转换
String refundAmount = String.valueOf(amount*0.01);
refundModel.setRefundAmount(refundAmount);
// 设置退款信息
RefundLocal refundInfo = PaymentContextLocal.get().getRefundInfo();
refundModel.setOutRequestNo(String.valueOf(refundInfo.getRefundId()));
try {
AlipayTradeRefundResponse response = AliPayApi.tradeRefundToResponse(refundModel);
if (!Objects.equals(AliPayCode.SUCCESS, response.getCode())) {
@@ -49,9 +49,12 @@ public class AliPayRefundService {
}
// 接口返回fund_change=Y为退款成功fund_change=N或无此字段值返回时需通过退款查询接口进一步确认退款状态
if (response.getFundChange().equals("Y")){
refundInfo.setStatus(PayRefundStatusEnum.SUCCESS)
.setGatewayOrderNo(response.getTradeNo());
// TODO 测试退款同步
// refundInfo.setStatus(PayRefundStatusEnum.SUCCESS)
// .setGatewayOrderNo(response.getTradeNo());
}
refundInfo.setStatus(PayRefundStatusEnum.PROGRESS)
.setGatewayOrderNo(response.getTradeNo());
}
catch (AlipayApiException e) {
log.error("订单退款失败:", e);

View File

@@ -51,6 +51,13 @@ public class AliPaySyncService {
AlipayTradeQueryResponse response = AliPayApi.tradeQueryToResponse(queryModel);
String tradeStatus = response.getTradeStatus();
syncResult.setSyncInfo(JSONUtil.toJsonStr(response));
// 失败
if (!Objects.equals(AliPayCode.SUCCESS, response.getCode())) {
syncResult.setSyncStatus(PaySyncStatusEnum.FAIL);
syncResult.setErrorCode(response.getSubCode());
syncResult.setErrorMsg(response.getSubMsg());
return syncResult;
}
// 支付完成 TODO 部分退款也在这个地方, 但无法进行区分, 需要借助对账进行处理
if (Objects.equals(tradeStatus, AliPayCode.NOTIFY_TRADE_SUCCESS) || Objects.equals(tradeStatus, AliPayCode.NOTIFY_TRADE_FINISHED)) {
PaymentContextLocal.get().getPaySyncInfo().setGatewayOrderNo(response.getTradeNo());
@@ -92,14 +99,25 @@ public class AliPaySyncService {
RefundGatewaySyncResult syncResult = new RefundGatewaySyncResult().setSyncStatus(PayRefundSyncStatusEnum.FAIL);
try {
AlipayTradeFastpayRefundQueryModel queryModel = new AlipayTradeFastpayRefundQueryModel();
queryModel.setOutTradeNo(String.valueOf(refundOrder.getId()));
// 退款请求号
queryModel.setOutRequestNo(String.valueOf(refundOrder.getId()));
// 商户订单号
queryModel.setOutTradeNo(String.valueOf(refundOrder.getPaymentId()));
AlipayTradeFastpayRefundQueryResponse response = AliPayApi.tradeRefundQueryToResponse(queryModel);
syncResult.setSyncInfo(JSONUtil.toJsonStr(response));
// 失败
if (!Objects.equals(AliPayCode.SUCCESS, response.getCode())) {
syncResult.setSyncStatus(PayRefundSyncStatusEnum.FAIL);
syncResult.setErrorCode(response.getSubCode());
syncResult.setErrorMsg(response.getSubMsg());
return syncResult;
}
String tradeStatus = response.getRefundStatus();
// 成功
if (Objects.equals(tradeStatus, AliPayCode.NOTIFY_TRADE_SUCCESS)){
if (Objects.equals(tradeStatus, AliPayCode.REFUND_SUCCESS)){
return syncResult.setSyncStatus(PayRefundSyncStatusEnum.SUCCESS);
} else {
return syncResult.setSyncStatus(PayRefundSyncStatusEnum.FAIL).setErrorMsg("支付宝网关反正退款未成功");
}
} catch (AlipayApiException e) {
log.error("退款订单同步失败:", e);

View File

@@ -3,7 +3,7 @@ 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.PayStatusEnum;
import cn.bootx.platform.daxpay.service.code.PayCallbackTypeEnum;
import cn.bootx.platform.daxpay.service.code.PaymentTypeEnum;
import cn.bootx.platform.daxpay.service.common.context.CallbackLocal;
import cn.bootx.platform.daxpay.service.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.service.core.channel.wechat.entity.WeChatPayConfig;
@@ -62,8 +62,8 @@ public class WeChatPayCallbackService extends AbsCallbackStrategy {
}
// 退款回调不用进行校验
PayCallbackTypeEnum callbackType = this.getCallbackType();
if (callbackType == PayCallbackTypeEnum.REFUND){
PaymentTypeEnum callbackType = this.getCallbackType();
if (callbackType == PaymentTypeEnum.REFUND){
return true;
}
// 支付回调信息校验
@@ -141,16 +141,16 @@ public class WeChatPayCallbackService extends AbsCallbackStrategy {
/**
* 判断类型 支付回调/退款回调
*
* @see PayCallbackTypeEnum
* @see PaymentTypeEnum
*/
@Override
public PayCallbackTypeEnum getCallbackType() {
public PaymentTypeEnum getCallbackType() {
CallbackLocal callbackInfo = PaymentContextLocal.get().getCallbackInfo();
Map<String, String> callbackParam = callbackInfo.getCallbackParam();
if (StrUtil.isNotBlank(callbackParam.get(REQ_INFO))){
return PayCallbackTypeEnum.REFUND;
return PaymentTypeEnum.REFUND;
} else {
return PayCallbackTypeEnum.PAY;
return PaymentTypeEnum.PAY;
}
}

View File

@@ -9,8 +9,9 @@ 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.PayChannelOrder;
import cn.bootx.platform.daxpay.service.core.order.pay.entity.PayOrder;
import cn.bootx.platform.daxpay.service.core.order.refund.dao.PayRefundChannelOrderManager;
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.PayGatewaySyncResult;
import cn.bootx.platform.daxpay.service.core.payment.sync.result.RefundGatewaySyncResult;
@@ -20,7 +21,7 @@ import com.ijpay.core.enums.SignType;
import com.ijpay.core.kit.WxPayKit;
import com.ijpay.wxpay.WxPayApi;
import com.ijpay.wxpay.model.OrderQueryModel;
import com.ijpay.wxpay.model.UnifiedOrderModel;
import com.ijpay.wxpay.model.RefundQueryModel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -40,6 +41,7 @@ import java.util.Objects;
@RequiredArgsConstructor
public class WeChatPaySyncService {
private final PayChannelOrderManager payChannelOrderManager;
private final PayRefundChannelOrderManager refundChannelOrderManager;
/**
* 同步查询
@@ -103,23 +105,25 @@ public class WeChatPaySyncService {
}
/**
* 退款查询
* 退款信息查询
*/
public RefundGatewaySyncResult syncRefundStatus(PayRefundOrder refundOrder, WeChatPayConfig weChatPayConfig){
PayChannelOrder orderChannel = payChannelOrderManager.findByPaymentIdAndChannel(refundOrder.getId(), PayChannelEnum.WECHAT.getCode())
PayRefundChannelOrder orderChannel = refundChannelOrderManager.findByRefundIdAndChannel(refundOrder.getId(), PayChannelEnum.WECHAT.getCode())
.orElseThrow(() -> new PayFailureException("支付订单通道信息不存在"));
Map<String, String> params = UnifiedOrderModel.builder()
Map<String, String> params = RefundQueryModel.builder()
.appid(weChatPayConfig.getWxAppId())
.mch_id(weChatPayConfig.getWxMchId())
.nonce_str(WxPayKit.generateStr())
.out_trade_no(String.valueOf(refundOrder.getId()))
.out_refund_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 RefundGatewaySyncResult().setSyncStatus(PayRefundSyncStatusEnum.REFUNDING);

View File

@@ -8,7 +8,7 @@ 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.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.hutool.core.codec.Base64;
import cn.hutool.core.util.StrUtil;
import com.ijpay.core.enums.SignType;
@@ -39,7 +39,7 @@ public class WechatRefundService {
* 退款方法
* 微信需要同时传输订单金额或退款金额
*/
public void refund(PayOrder payOrder, int amount, PayChannelOrder orderChannel, WeChatPayConfig weChatPayConfig) {
public void refund(PayRefundOrder refundOrder, int amount, PayChannelOrder orderChannel, WeChatPayConfig weChatPayConfig) {
String refundFee = String.valueOf(amount);
String totalFee = String.valueOf(orderChannel.getAmount());
// 设置退款信息
@@ -48,8 +48,8 @@ public class WechatRefundService {
.appid(weChatPayConfig.getWxAppId())
.mch_id(weChatPayConfig.getWxMchId())
.notify_url(weChatPayConfig.getNotifyUrl())
.out_trade_no(String.valueOf(payOrder.getId()))
.out_refund_no(String.valueOf(refundInfo.getRefundId()))
.out_trade_no(String.valueOf(refundOrder.getPaymentId()))
.out_refund_no(String.valueOf(refundOrder.getId()))
.total_fee(totalFee)
.refund_fee(refundFee)
.nonce_str(WxPayKit.generateStr())

View File

@@ -87,7 +87,7 @@ public class PayChannelOrderService {
* 更新异步支付通道退款余额和状态
*/
public void updateAsyncPayRefund(PayChannelOrder payChannelOrder, PayRefundChannelOrder refundChannelOrder){
// 支付通道订单可退余额
// 支付通道订单可退余额
int refundableBalance = payChannelOrder.getRefundableBalance() - refundChannelOrder.getAmount();
payChannelOrder.setRefundableBalance(refundableBalance);
// 支付通道订单状态

View File

@@ -31,7 +31,7 @@ public class PayReconcileOrderService {
/**
* 更新, 开启一个新事务进行更新
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void update(PayReconcileOrder order){
reconcileOrderManager.updateById(order);
}

View File

@@ -29,9 +29,9 @@ public class PayRefundChannelOrderManager extends BaseManager<PayRefundChannelOr
/**
* 根据退款单ID和退款通道查询
*/
public Optional<PayRefundChannelOrder> findByPaymentIdAndChannel(Long paymentId, String channel) {
public Optional<PayRefundChannelOrder> findByRefundIdAndChannel(Long refundId, String channel) {
return lambdaQuery()
.eq(PayRefundChannelOrder::getRefundId,paymentId)
.eq(PayRefundChannelOrder::getRefundId,refundId)
.eq(PayRefundChannelOrder::getChannel,channel)
.oneOpt();
}

View File

@@ -8,6 +8,8 @@ import cn.bootx.platform.daxpay.service.core.order.refund.convert.PayRefundOrder
import cn.bootx.platform.daxpay.service.dto.order.refund.PayRefundOrderDto;
import cn.bootx.table.modify.annotation.DbColumn;
import cn.bootx.table.modify.annotation.DbTable;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -17,6 +19,7 @@ import java.time.LocalDateTime;
/**
* 退款记录
* 主键作为退款的请求号
*
* @author xxm
* @since 2022/3/2
@@ -71,6 +74,7 @@ public class PayRefundOrder extends MpBaseEntity implements EntityBaseFunction<P
/** 剩余可退 */
@DbColumn(comment = "剩余可退")
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private Integer refundableBalance;
/** 请求链路ID */

View File

@@ -1,6 +1,7 @@
package cn.bootx.platform.daxpay.service.core.order.refund.service;
import cn.bootx.platform.common.core.exception.DataNotExistException;
import cn.bootx.platform.daxpay.result.pay.PaySyncResult;
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;
@@ -24,10 +25,10 @@ public class PayRefundOrderService {
/**
* 退款同步
*/
public void syncById(Long id){
public PaySyncResult syncById(Long id){
PayRefundOrder refundOrder = refundOrderManager.findById(id)
.orElseThrow(() -> new DataNotExistException("退款订单不存在"));
refundSyncService.syncPayOrder(refundOrder);
return refundSyncService.syncRefundOrder(refundOrder);
}
}

View File

@@ -18,6 +18,7 @@ import cn.bootx.platform.daxpay.service.core.order.refund.dao.PayRefundChannelOr
import cn.bootx.platform.daxpay.service.core.order.refund.dao.PayRefundOrderManager;
import cn.bootx.platform.daxpay.service.core.order.refund.entity.PayRefundChannelOrder;
import cn.bootx.platform.daxpay.service.core.order.refund.entity.PayRefundOrder;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -31,6 +32,8 @@ import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static cn.bootx.platform.daxpay.code.PayRefundStatusEnum.SUCCESS;
/**
* 支付退款支撑服务
* @author xxm
@@ -109,10 +112,11 @@ public class PayRefundAssistService {
List<String> tradesStatus = Arrays.asList(
PayStatusEnum.PROGRESS.getCode(),
PayStatusEnum.CLOSE.getCode(),
PayStatusEnum.REFUNDING.getCode(),
PayStatusEnum.FAIL.getCode());
if (tradesStatus.contains(payOrder.getStatus())) {
PayStatusEnum statusEnum = PayStatusEnum.findByCode(payOrder.getStatus());
throw new PayFailureException("当前状态["+statusEnum.getName()+"]不允许状态非法, 无法退款");
throw new PayFailureException("当前状态["+statusEnum.getName()+"]不允许发起退款操作");
}
// 过滤掉金额为0的退款参数
@@ -130,36 +134,29 @@ public class PayRefundAssistService {
}
/**
* 保存退款订单 成不成功都记录
* 预先创建退款相关订单并保存, 使用新事务, 防止丢单
*/
public PayRefundOrder generateRefundOrder(RefundParam refundParam, PayOrder payOrder){
RefundLocal asyncRefundInfo = PaymentContextLocal.get().getRefundInfo();
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public PayRefundOrder createOrderAndChannel(RefundParam refundParam, PayOrder payOrder, List<PayRefundChannelOrder> refundChannelOrders) {
// 此次的总退款金额
Integer amount = refundParam.getRefundChannels()
.stream()
.map(RefundChannelParam::getAmount)
.reduce(0, Integer::sum);
int refundableBalance = payOrder.getRefundableBalance();
// 生成退款订单
PayRefundOrder refundOrder = new PayRefundOrder()
.setPaymentId(payOrder.getId())
.setStatus(PayRefundStatusEnum.PROGRESS.getCode())
.setBusinessNo(payOrder.getBusinessNo())
.setRefundNo(refundParam.getRefundNo())
.setOrderAmount(payOrder.getAmount())
.setAmount(amount)
.setRefundableBalance(payOrder.getRefundableBalance())
.setRefundTime(LocalDateTime.now())
.setRefundableBalance(refundableBalance)
.setTitle(payOrder.getTitle())
.setGatewayOrderNo(asyncRefundInfo.getGatewayOrderNo())
.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()
@@ -173,33 +170,43 @@ public class PayRefundAssistService {
}
// 主键使用预先生成的ID, 如果有异步通道, 关联的退款号就是这个ID
long refundId = asyncRefundInfo.getRefundId();
refundOrder.setId(refundId);
refundOrder.setId(IdUtil.getSnowflakeNextId());
// 退款号, 如不传输, 使用ID作为退款号
if(StrUtil.isBlank(refundOrder.getRefundNo())){
refundOrder.setRefundNo(String.valueOf(refundId));
refundOrder.setRefundNo(String.valueOf(refundOrder.getId()));
}
return refundOrder;
refundChannelOrders.forEach(r->r.setRefundId(refundOrder.getId()));
payRefundChannelOrderManager.saveAll(refundChannelOrders);
return payRefundOrderManager.save(refundOrder);
}
/**
* 保存退款记录和对应的通道记录
* 更新退款成功信息
*/
@Transactional(rollbackFor = Exception.class)
public void saveOrderAndChannels(PayRefundOrder refundOrder,List<PayRefundChannelOrder> refundChannelOrders){
payRefundOrderManager.save(refundOrder);
for (PayRefundChannelOrder refundOrderChannel : refundChannelOrders) {
refundOrderChannel.setRefundId(refundOrder.getId());
public void updateOrderAndChannel(PayRefundOrder refundOrder, List<PayRefundChannelOrder> refundChannelOrders){
RefundLocal asyncRefundInfo = PaymentContextLocal.get().getRefundInfo();
refundOrder.setStatus(asyncRefundInfo.getStatus().getCode())
.setGatewayOrderNo(asyncRefundInfo.getGatewayOrderNo());
// 退款成功更新退款时间
if (Objects.equals(refundOrder.getStatus(), SUCCESS.getCode())){
refundOrder.setRefundTime(LocalDateTime.now());
}
payRefundChannelOrderManager.saveAll(refundChannelOrders);
payRefundOrderManager.updateById(refundOrder);
payRefundChannelOrderManager.updateAllById(refundChannelOrders);
}
/**
* 保存退款记录, 开启新事物
* 更新退款错误信息
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveOrder(PayRefundOrder refundOrder){
payRefundOrderManager.save(refundOrder);
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void updateOrderByError(PayRefundOrder refundOrder){
RefundLocal asyncRefundInfo = PaymentContextLocal.get().getRefundInfo();
refundOrder.setErrorCode(asyncRefundInfo.getErrorCode());
refundOrder.setErrorMsg(asyncRefundInfo.getErrorMsg());
// 退款失败不保存剩余可退余额, 否则数据看起开会产生困惑
refundOrder.setRefundableBalance(null);
}
}

View File

@@ -23,11 +23,13 @@ import cn.bootx.platform.daxpay.service.core.payment.refund.factory.PayRefundStr
import cn.bootx.platform.daxpay.service.func.AbsRefundStrategy;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.extra.spring.SpringUtil;
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.Collections;
@@ -126,78 +128,99 @@ public class PayRefundService {
/**
* 分支付通道进行退款
* TODO 增加错误处理, 目前出现错误后存储的数据不全
* 1. 创建退款订单和通道订单并保存(单独事务)
* 2. 调用API发起退款(异步退款)
* 3. 根据API返回信息更新退款订单信息
*/
public RefundResult refundByChannel(RefundParam refundParam, PayOrder payOrder, List<PayChannelOrder> payChannelOrders){
// 0.基础数据准备, 并比对通道支付单是否与可退款记录数量一致
// 1.1 基础数据准备
Map<String, PayChannelOrder> orderChannelMap = payChannelOrders.stream()
.collect(Collectors.toMap(PayChannelOrder::getChannel, Function.identity(), CollectorsFunction::retainLatest));
List<RefundChannelParam> refundChannels = refundParam.getRefundChannels();
// 1.2获取退款参数方式,通过工厂生成对应的策略组
List<AbsRefundStrategy> payRefundStrategies = PayRefundStrategyFactory.createAsyncLast(refundChannels);
if (CollectionUtil.isEmpty(payRefundStrategies)) {
throw new PayUnsupportedMethodException();
}
// 1.3初始化退款策略的参数
for (AbsRefundStrategy refundStrategy : payRefundStrategies) {
PayChannelOrder payChannelOrder = orderChannelMap.get(refundStrategy.getChannel().getCode());
if (Objects.isNull(payChannelOrder)){
throw new PayFailureException("[数据异常]进行退款的通道没有对应的支付单, 无法退款");
}
refundStrategy.initRefundParam(payOrder, refundParam, payChannelOrder);
}
// 对通道支付订单进行预扣款
payRefundStrategies.forEach(AbsRefundStrategy::doPreDeductOrderHandler);
// 退款操作的预处理, 使用独立的新事物进行发起, 返回创建成功的退款订单, 成功后才可以进行下一阶段的操作
PayRefundOrder refundOrder = SpringUtil.getBean(this.getClass()).preRefundMethod(refundParam, payOrder, payRefundStrategies);
// 设置退款订单对象
payRefundStrategies.forEach(r->r.setRefundOrder(refundOrder));
try {
// 1.获取退款参数方式,通过工厂生成对应的策略组
List<AbsRefundStrategy> payRefundStrategies = PayRefundStrategyFactory.createAsyncLast(refundChannels);
if (CollectionUtil.isEmpty(payRefundStrategies)) {
throw new PayUnsupportedMethodException();
}
// 2.初始化退款策略的参数
for (AbsRefundStrategy refundStrategy : payRefundStrategies) {
PayChannelOrder payChannelOrder = orderChannelMap.get(refundStrategy.getChannel().getCode());
if (Objects.isNull(payChannelOrder)){
throw new PayFailureException("[数据异常]进行退款的通道没有对应的支付单, 无法退款");
}
refundStrategy.initRefundParam(payOrder, refundParam, payChannelOrder);
}
// 3.1 退款前准备操作
payRefundStrategies.forEach(AbsRefundStrategy::doBeforeRefundHandler);
// 3.2 生成各通道退款订单
payRefundStrategies.forEach(AbsRefundStrategy::generateChannelOrder);
// 3.3 执行退款策略
// 3.2 执行退款策略
payRefundStrategies.forEach(AbsRefundStrategy::doRefundHandler);
// 3.4 执行退款发起成功后操作
payRefundStrategies.forEach(AbsRefundStrategy::doSuccessHandler);
// 4 更新各支付通道订单的信息
List<PayChannelOrder> channelOrders = payRefundStrategies.stream()
.map(AbsRefundStrategy::getPayChannelOrder)
.collect(Collectors.toList());
payChannelOrderManager.updateAllById(channelOrders);
// 5 获取退款通道订单, 进行保存
// 4.进行成功处理, 分别处理退款订单, 通道退款订单, 支付订单
List<PayRefundChannelOrder> refundChannelOrders = payRefundStrategies.stream()
.map(AbsRefundStrategy::getRefundChannelOrder)
.collect(Collectors.toList());
// 6.进行成功处理, 分别处理退款订单, 通道退款订单, 支付订单
PayRefundOrder refundOrder = this.successHandler(refundParam, refundChannelOrders, payOrder);
this.successHandler(refundOrder, refundChannelOrders, payOrder);
return new RefundResult()
.setRefundId(refundOrder.getId())
.setRefundNo(refundParam.getRefundNo());
}
catch (Exception e) {
// 失败处理
// 5. 失败处理
PaymentContextLocal.get().getRefundInfo().setStatus(PayRefundStatusEnum.FAIL);
this.errorHandler(refundParam, payOrder);
this.errorHandler(refundOrder);
throw e;
}
}
/**
* 退款订单成功处理, 保存退款订单, 通道退款订单, 更新支付订单
* 退款一阶段: 进行支付订单和支付通道订单的预扣, 预创建退款订单并保存, 使用独立的新事物进行发起
*/
private PayRefundOrder successHandler(RefundParam refundParam, List<PayRefundChannelOrder> refundChannelOrders, PayOrder payOrder) {
RefundLocal asyncRefundInfo = PaymentContextLocal.get().getRefundInfo();
// ----------------------- 支付订单处理 ---------------------------------------
@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class )
public PayRefundOrder preRefundMethod(RefundParam refundParam, PayOrder payOrder, List<AbsRefundStrategy> payRefundStrategies) {
// --------------------------- 支付订单 -------------------------------------
// 预扣支付相关订单要退款的金额并进行更新
payRefundStrategies.forEach(AbsRefundStrategy::generateChannelOrder);
List<PayChannelOrder> channelOrders = payRefundStrategies.stream()
.map(AbsRefundStrategy::getPayChannelOrder)
.collect(Collectors.toList());
payChannelOrderManager.updateAllById(channelOrders);
// 此次的总退款金额
Integer amount = refundParam.getRefundChannels().stream()
Integer amount = refundParam.getRefundChannels()
.stream()
.map(RefundChannelParam::getAmount)
.reduce(0, Integer::sum);
// 剩余可退款余额
int refundableBalance = payOrder.getRefundableBalance() - amount;
payOrder.setRefundableBalance(refundableBalance);
int orderRefundableBalance = payOrder.getRefundableBalance() - amount;
payOrder.setRefundableBalance(orderRefundableBalance)
.setStatus(PayStatusEnum.REFUNDING.getCode());
payOrderService.updateById(payOrder);
// ----------------------- 退款订单 -------------------------
List<PayRefundChannelOrder> refundChannelOrders = payRefundStrategies.stream()
.map(AbsRefundStrategy::getRefundChannelOrder)
.collect(Collectors.toList());
return payRefundAssistService.createOrderAndChannel(refundParam, payOrder,refundChannelOrders);
}
/**
* 成功处理, 更新退款订单, 退款通道订单, 支付订单, 支付通道订单
*/
private void successHandler(PayRefundOrder payRefundOrder, List<PayRefundChannelOrder> refundChannelOrders, PayOrder payOrder) {
RefundLocal asyncRefundInfo = PaymentContextLocal.get().getRefundInfo();
// 剩余可退款余额
int refundableBalance = payRefundOrder.getRefundableBalance();
// 设置支付订单状态
if (asyncRefundInfo.getStatus() == PayRefundStatusEnum.PROGRESS) {
// 设置为退款中
@@ -209,22 +232,17 @@ public class PayRefundService {
// 部分退款
payOrder.setStatus(PayStatusEnum.PARTIAL_REFUND.getCode());
}
// ----------------------- 退款订单处理 ---------------------------------------
// 生成退款订单
PayRefundOrder refundOrder = payRefundAssistService.generateRefundOrder(refundParam, payOrder);
// 更新或保存相关订单
payRefundAssistService.saveOrderAndChannels(refundOrder,refundChannelOrders);
payOrderService.updateById(payOrder);
return refundOrder;
// 更新退款订单和相关通道订单
payRefundAssistService.updateOrderAndChannel(payRefundOrder,refundChannelOrders);
}
/**
* 失败处理
* 失败处理, 只更新退款订单, 通道订单不进行错误更新
*/
private void errorHandler(RefundParam refundParam, PayOrder payOrder) {
private void errorHandler(PayRefundOrder refundOrder) {
// 记录退款失败的记录
PayRefundOrder refundOrder = payRefundAssistService.generateRefundOrder(refundParam, payOrder);
payRefundAssistService.saveOrder(refundOrder);
payRefundAssistService.updateOrderByError(refundOrder);
}
}

View File

@@ -12,8 +12,6 @@ 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;
/**
@@ -54,7 +52,7 @@ public class AliPayRefundStrategy extends AbsRefundStrategy {
*/
@Override
public void doRefundHandler() {
aliRefundService.refund(this.getPayOrder(), this.getRefundChannelParam().getAmount());
aliRefundService.refund(this.getRefundOrder(), this.getRefundChannelParam().getAmount());
}
/**

View File

@@ -1,8 +1,8 @@
package cn.bootx.platform.daxpay.service.core.payment.refund.strategy;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.service.core.channel.wallet.service.WalletPayService;
import cn.bootx.platform.daxpay.service.core.channel.wallet.service.WalletPayOrderService;
import cn.bootx.platform.daxpay.service.core.channel.wallet.service.WalletPayService;
import cn.bootx.platform.daxpay.service.core.order.pay.service.PayOrderService;
import cn.bootx.platform.daxpay.service.func.AbsRefundStrategy;
import lombok.RequiredArgsConstructor;

View File

@@ -12,8 +12,6 @@ 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;
/**
@@ -52,12 +50,13 @@ public class WeChatPayRefundStrategy extends AbsRefundStrategy {
this.weChatPayConfig = weChatPayConfigService.getConfig();
}
/**
* 退款操作
*/
@Override
public void doRefundHandler() {
wechatRefundService.refund(this.getPayOrder(), this.getRefundChannelParam().getAmount(), this.getPayChannelOrder(), this.weChatPayConfig);
wechatRefundService.refund(this.getRefundOrder(), this.getRefundChannelParam().getAmount(), this.getPayChannelOrder(), this.weChatPayConfig);
}
/**

View File

@@ -2,7 +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.PaymentTypeEnum;
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;
@@ -161,7 +161,7 @@ public class PayRepairService {
.setOrderNo(order.getBusinessNo())
.setBeforeStatus(repairResult.getBeforeStatus().getCode())
.setAfterStatus(afterStatus)
.setRepairType(PayRepairPayTypeEnum.PAY.getCode())
.setRepairType(PaymentTypeEnum.PAY.getCode())
.setRepairSource(source)
.setRepairWay(recordType.getCode());
payRepairRecord.setId(repairResult.getRepairId());

View File

@@ -3,7 +3,7 @@ package cn.bootx.platform.daxpay.service.core.payment.repair.service;
import cn.bootx.platform.common.core.exception.DataNotExistException;
import cn.bootx.platform.daxpay.code.PayRefundStatusEnum;
import cn.bootx.platform.daxpay.code.PayStatusEnum;
import cn.bootx.platform.daxpay.service.code.PayRepairPayTypeEnum;
import cn.bootx.platform.daxpay.service.code.PaymentTypeEnum;
import cn.bootx.platform.daxpay.service.code.RefundRepairWayEnum;
import cn.bootx.platform.daxpay.service.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.service.core.order.pay.dao.PayChannelOrderManager;
@@ -57,15 +57,16 @@ public class RefundRepairService {
// 获取关联支付单
PayOrder payOrder = payOrderManager.findById(refundOrder.getPaymentId())
.orElseThrow(() -> new RuntimeException("支付单不存在"));
// 关联异步支付通道支付单
// 关联支付通道支付单
PayChannelOrder payChannelOrder = payChannelOrderManager.findByPaymentIdAndChannel(payOrder.getId(), payOrder.getAsyncChannel())
.orElseThrow(DataNotExistException::new);
// 异步通道退款单
PayRefundChannelOrder refundChannelOrder = refundChannelOrderManager.findByPaymentIdAndChannel(refundOrder.getId(), payOrder.getAsyncChannel())
PayRefundChannelOrder refundChannelOrder = refundChannelOrderManager.findByRefundIdAndChannel(refundOrder.getId(), payOrder.getAsyncChannel())
.orElseThrow(DataNotExistException::new);
// 根据不同的类型执行对应的修复逻辑
RefundRepairResult repairResult = new RefundRepairResult();
//TODO 整个退款单是一个状态, 最终结果要么全部成功, 要么全部回退
if (Objects.requireNonNull(repairType) == RefundRepairWayEnum.SUCCESS) {
repairResult = this.success(refundOrder,payOrder,refundChannelOrder,payChannelOrder);
} else if (repairType == RefundRepairWayEnum.FAIL) {
@@ -164,7 +165,6 @@ public class RefundRepairService {
// 退款单设置为部分成功状态, 通道退款单设置为失败状态
refundOrder.setStatus(PayRefundStatusEnum.FAIL.getCode());
refundChannelOrder.setStatus(PayRefundStatusEnum.FAIL.getCode());
repairResult.setAfterRefundStatus(PayRefundStatusEnum.PART_SUCCESS);
}
// 更新订单和退款相关订单
@@ -190,7 +190,7 @@ public class RefundRepairService {
return new PayRepairRecord()
.setRepairId(repairResult.getRepairId())
.setOrderId(order.getId())
.setRepairType(PayRepairPayTypeEnum.PAY.getCode())
.setRepairType(PaymentTypeEnum.PAY.getCode())
.setRepairSource(source)
.setRepairWay(repairType.getCode())
.setAsyncChannel(order.getAsyncChannel())
@@ -214,7 +214,7 @@ public class RefundRepairService {
.setOrderId(refundOrder.getId())
.setRepairId(repairResult.getRepairId())
.setOrderNo(refundOrder.getRefundNo())
.setRepairType(PayRepairPayTypeEnum.REFUND.getCode())
.setRepairType(PaymentTypeEnum.REFUND.getCode())
.setBeforeStatus(repairResult.getBeforeRefundStatus().getCode())
.setAfterStatus(afterStatus)
.setRepairSource(source)

View File

@@ -4,10 +4,15 @@ 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.result.pay.PaySyncResult;
import cn.bootx.platform.daxpay.service.common.local.PaymentContextLocal;
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.repair.result.RefundRepairResult;
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.core.record.sync.entity.PaySyncRecord;
import cn.bootx.platform.daxpay.service.core.record.sync.service.PaySyncRecordService;
import cn.bootx.platform.daxpay.service.func.AbsRefundSyncStrategy;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
@@ -30,12 +35,14 @@ import java.util.Objects;
public class PayRefundSyncService {
private final PayRefundOrderManager refundOrderManager;
private final PaySyncRecordService paySyncRecordService;
private final LockTemplate lockTemplate;
/**
* 退款同步, 开启一个新的事务, 不受外部抛出异常的影响
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void sync(RefundSyncParam param){
// 先获取退款单
PayRefundOrder requestOrder;
@@ -51,12 +58,15 @@ public class PayRefundSyncService {
// TODO 需要限制同步的请求不进行同步
return;
}
this.syncPayOrder(requestOrder);
this.syncRefundOrder(requestOrder);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void syncPayOrder(PayRefundOrder refundOrder) {
/**
* 退款订单信息同步
*/
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public PaySyncResult syncRefundOrder(PayRefundOrder refundOrder) {
// 加锁
LockInfo lock = lockTemplate.lock("sync:refund:" + refundOrder.getId());
if (Objects.isNull(lock)) {
@@ -73,36 +83,66 @@ public class PayRefundSyncService {
// 判断是否同步成功
if (Objects.equals(syncResult.getSyncStatus(), PayRefundSyncStatusEnum.FAIL)) {
// 同步失败, 返回失败响应, 同时记录失败的日志
// return new PaySyncResult().setErrorMsg(syncResult.getErrorMsg());
log.error("同步失败");
return;
return new PaySyncResult().setErrorMsg(syncResult.getErrorMsg());
}
// 判断网关状态是否和支付单一致, 同时特定情况下更新网关同步状态
// boolean statusSync = this.checkAndAdjustSyncStatus(syncResult, payOrder);
// PayRepairResult repairResult = new PayRepairResult();
// try {
// 判断网关状态是否和支付单一致, 同时特定情况下更新网关同步状态
boolean statusSync = this.checkAndAdjustSyncStatus(syncResult, refundOrder);
RefundRepairResult repairResult = new RefundRepairResult();
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());
// }
//
if (!statusSync) {
repairResult = this.repairHandler(syncResult, refundOrder);
}
} catch (PayFailureException e) {
// 同步失败, 返回失败响应, 同时记录失败的日志
syncResult.setSyncStatus(PayRefundSyncStatusEnum.FAIL);
this.saveRecord(refundOrder, 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());
this.saveRecord(refundOrder, syncResult, !statusSync, repairResult.getRepairId(), null);
return new PaySyncResult()
.setGatewayStatus(syncResult.getSyncStatus().getCode())
.setSuccess(true)
.setRepair(!statusSync)
.setRepairId(repairResult.getRepairId());
} finally {
lockTemplate.releaseLock(lock);
}
}
private boolean checkAndAdjustSyncStatus(RefundGatewaySyncResult syncResult, PayRefundOrder order){
return true;
}
private RefundRepairResult repairHandler(RefundGatewaySyncResult syncResult, PayRefundOrder order){
return null;
}
/**
* 保存同步记录
* @param payOrder 支付单
* @param syncResult 同步结果
* @param repair 是否修复
* @param errorMsg 错误信息
*/
private void saveRecord(PayRefundOrder payOrder, RefundGatewaySyncResult syncResult, boolean repair, Long repairOrderId, String errorMsg){
PaySyncRecord paySyncRecord = new PaySyncRecord()
.setOrderId(payOrder.getId())
.setOrderNo(payOrder.getBusinessNo())
.setAsyncChannel(payOrder.getAsyncChannel())
.setSyncInfo(syncResult.getSyncInfo())
.setGatewayStatus(syncResult.getSyncStatus().getCode())
.setRepairOrder(repair)
.setRepairOrderId(repairOrderId)
.setErrorMsg(errorMsg)
.setClientIp(PaymentContextLocal.get().getRequestInfo().getClientIp())
.setReqId(PaymentContextLocal.get().getRequestInfo().getReqId());
paySyncRecordService.saveRecord(paySyncRecord);
}
}

View File

@@ -57,7 +57,7 @@ public class PaySyncService {
/**
* 支付同步, 开启一个新的事务, 不受外部抛出异常的影响
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public PaySyncResult sync(PaySyncParam param) {
PayOrder payOrder = null;
if (Objects.nonNull(param.getPaymentId())){
@@ -81,7 +81,7 @@ public class PaySyncService {
* 2. 如果状态不一致, 调用修复逻辑进行修复
* todo 需要进行异常处理, 现在会有 Transaction rolled back because it has been marked as rollback-only 问题
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public PaySyncResult syncPayOrder(PayOrder payOrder) {
// 加锁
LockInfo lock = lockTemplate.lock("sync:payment" + payOrder.getId());
@@ -108,7 +108,7 @@ public class PaySyncService {
try {
// 状态不一致,执行支付单修复逻辑
if (!statusSync){
repairResult = this.resultHandler(syncResult, payOrder);
repairResult = this.repairHandler(syncResult, payOrder);
}
} catch (PayFailureException e) {
// 同步失败, 返回失败响应, 同时记录失败的日志
@@ -173,9 +173,9 @@ public class PaySyncService {
}
/**
* 根据同步的结果对支付单进行处理
* 根据同步的结果对支付单进行修复处理
*/
private PayRepairResult resultHandler(PayGatewaySyncResult syncResult, PayOrder payOrder){
private PayRepairResult repairHandler(PayGatewaySyncResult syncResult, PayOrder payOrder){
PaySyncStatusEnum syncStatusEnum = syncResult.getSyncStatus();
// 如果没有支付来源, 设置支付来源为同步
RepairLocal repairInfo = PaymentContextLocal.get().getRepairInfo();
@@ -195,6 +195,8 @@ public class PaySyncService {
repair = repairService.repair(payOrder, PayRepairWayEnum.WAIT_PAY);
break;
}
case REFUND:
throw new PayFailureException("支付订单为退款状态,请通过执行对应的退款订单进行同步,来更新具体为什么类型退款状态");
// 交易关闭和未找到, 都对本地支付订单进行关闭, 不需要再调用网关进行关闭
case CLOSED:
case NOT_FOUND: {
@@ -222,7 +224,7 @@ public class PaySyncService {
/**
* 保存同步记录 TODO 目前出现一次请求多次与网关同步, 未全部记录
* 保存同步记录
* @param payOrder 支付单
* @param syncResult 同步结果
* @param repair 是否修复
@@ -230,8 +232,8 @@ public class PaySyncService {
*/
private void saveRecord(PayOrder payOrder, PayGatewaySyncResult syncResult, boolean repair, Long repairOrderId, String errorMsg){
PaySyncRecord paySyncRecord = new PaySyncRecord()
.setPaymentId(payOrder.getId())
.setBusinessNo(payOrder.getBusinessNo())
.setOrderId(payOrder.getId())
.setOrderNo(payOrder.getBusinessNo())
.setAsyncChannel(payOrder.getAsyncChannel())
.setSyncInfo(syncResult.getSyncInfo())
.setGatewayStatus(syncResult.getSyncStatus().getCode())

View File

@@ -1,6 +1,9 @@
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.channel.wechat.entity.WeChatPayConfig;
import cn.bootx.platform.daxpay.service.core.channel.wechat.service.WeChatPayConfigService;
import cn.bootx.platform.daxpay.service.core.channel.wechat.service.WeChatPaySyncService;
import cn.bootx.platform.daxpay.service.core.payment.sync.result.RefundGatewaySyncResult;
import cn.bootx.platform.daxpay.service.func.AbsRefundSyncStrategy;
import lombok.RequiredArgsConstructor;
@@ -18,6 +21,8 @@ import static org.springframework.beans.factory.config.BeanDefinition.SCOPE_PROT
@Component
@RequiredArgsConstructor
public class WeChatRefundSyncStrategy extends AbsRefundSyncStrategy {
private final WeChatPaySyncService weChatPaySyncService;
private final WeChatPayConfigService weChatPayConfigService;
/**
* 策略标识
@@ -32,7 +37,8 @@ public class WeChatRefundSyncStrategy extends AbsRefundSyncStrategy {
*/
@Override
public RefundGatewaySyncResult doSyncStatus() {
return null;
WeChatPayConfig config = weChatPayConfigService.getConfig();
return weChatPaySyncService.syncRefundStatus(this.getRefundOrder(), config);
}
}

View File

@@ -4,7 +4,7 @@ import cn.bootx.platform.common.core.function.EntityBaseFunction;
import cn.bootx.platform.common.mybatisplus.base.MpCreateEntity;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.service.code.PayCallbackStatusEnum;
import cn.bootx.platform.daxpay.service.code.PayCallbackTypeEnum;
import cn.bootx.platform.daxpay.service.code.PaymentTypeEnum;
import cn.bootx.platform.daxpay.service.core.record.callback.convert.PayCallbackRecordConvert;
import cn.bootx.platform.daxpay.service.dto.record.callback.PayCallbackRecordDto;
import cn.bootx.table.modify.annotation.DbColumn;
@@ -46,7 +46,7 @@ public class PayCallbackRecord extends MpCreateEntity implements EntityBaseFunct
/**
* 回调类型
* @see PayCallbackTypeEnum
* @see PaymentTypeEnum
*/
@DbColumn(comment = "回调类型")
private String callbackType;

View File

@@ -42,7 +42,7 @@ public class PayCallbackRecordService {
/**
* 保存回调记录
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void save(PayCallbackRecord record) {
callbackRecordManager.save(record);
}

View File

@@ -43,7 +43,7 @@ public class PayCloseRecordService {
/**
* 新开事务进行记录保存
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void saveRecord(PayCloseRecord record){
manager.save(record);
}

View File

@@ -3,7 +3,7 @@ package cn.bootx.platform.daxpay.service.core.record.repair.entity;
import cn.bootx.platform.common.core.function.EntityBaseFunction;
import cn.bootx.platform.common.mybatisplus.base.MpCreateEntity;
import cn.bootx.platform.daxpay.code.PayStatusEnum;
import cn.bootx.platform.daxpay.service.code.PayRepairPayTypeEnum;
import cn.bootx.platform.daxpay.service.code.PaymentTypeEnum;
import cn.bootx.platform.daxpay.service.code.PayRepairSourceEnum;
import cn.bootx.platform.daxpay.service.code.PayRepairWayEnum;
import cn.bootx.platform.daxpay.service.code.RefundRepairWayEnum;
@@ -48,7 +48,7 @@ public class PayRepairRecord extends MpCreateEntity implements EntityBaseFunctio
/**
* 修复类型 支付修复/退款修复
* @see PayRepairPayTypeEnum
* @see PaymentTypeEnum
*/
@DbColumn(comment = "修复类型")
private String repairType;

View File

@@ -44,14 +44,14 @@ public class PayRepairRecordService {
/**
* 保存记录
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void saveRecord(PayRepairRecord record){
repairRecordManager.save(record);
}
/**
* 保存记录
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void saveAllRecord(List<PayRepairRecord> records){
repairRecordManager.saveAll(records);
}

View File

@@ -27,13 +27,16 @@ import lombok.experimental.Accessors;
@TableName("pay_sync_record")
public class PaySyncRecord extends MpCreateEntity implements EntityBaseFunction<PaySyncRecordDto> {
/** 支付记录id */
@DbColumn(comment = "支付记录id")
private Long paymentId;
/** 本地订单ID */
@DbColumn(comment = "本地订单ID")
private Long orderId;
/** 业务号 */
@DbColumn(comment = "业务号")
private String businessNo;
/** 本地业务号 */
@DbColumn(comment = "本地业务号")
private String orderNo;
@DbColumn(comment = "同步通道")
private String syncChannel;
/**
* 同步通道

View File

@@ -44,7 +44,7 @@ public class PaySyncRecordService {
/**
* 记录同步记录 同步支付单的不进行记录
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void saveRecord(PaySyncRecord paySyncRecord){
orderManager.save(paySyncRecord);
}

View File

@@ -3,7 +3,7 @@ package cn.bootx.platform.daxpay.service.dto.record.callback;
import cn.bootx.platform.common.core.rest.dto.BaseDto;
import cn.bootx.platform.daxpay.code.PayChannelEnum;
import cn.bootx.platform.daxpay.service.code.PayCallbackStatusEnum;
import cn.bootx.platform.daxpay.service.code.PayCallbackTypeEnum;
import cn.bootx.platform.daxpay.service.code.PaymentTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -34,7 +34,7 @@ public class PayCallbackRecordDto extends BaseDto {
/**
* 回调类型
* @see PayCallbackTypeEnum
* @see PaymentTypeEnum
*/
@Schema(description = "回调类型")
private String callbackType;

View File

@@ -1,8 +1,8 @@
package cn.bootx.platform.daxpay.service.func;
import cn.bootx.platform.daxpay.service.code.PayCallbackStatusEnum;
import cn.bootx.platform.daxpay.service.code.PayCallbackTypeEnum;
import cn.bootx.platform.daxpay.service.code.PayRepairSourceEnum;
import cn.bootx.platform.daxpay.service.code.PaymentTypeEnum;
import cn.bootx.platform.daxpay.service.common.context.CallbackLocal;
import cn.bootx.platform.daxpay.service.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.service.core.payment.callback.service.PayCallbackService;
@@ -51,8 +51,8 @@ public abstract class AbsCallbackStrategy implements PayStrategy {
PaymentContextLocal.get().getRepairInfo().setSource(PayRepairSourceEnum.CALLBACK);
// 判断回调类型
PayCallbackTypeEnum callbackType = this.getCallbackType();
if (callbackType == PayCallbackTypeEnum.PAY){
PaymentTypeEnum callbackType = this.getCallbackType();
if (callbackType == PaymentTypeEnum.PAY){
// 解析支付数据并放处理
this.resolvePayData();
payCallbackService.payCallback();
@@ -78,9 +78,9 @@ public abstract class AbsCallbackStrategy implements PayStrategy {
/**
* 判断类型 支付回调/退款回调
* @see PayCallbackTypeEnum
* @see PaymentTypeEnum
*/
public abstract PayCallbackTypeEnum getCallbackType();
public abstract PaymentTypeEnum getCallbackType();
/**
* 解析支付回调数据并放到上下文中
@@ -105,7 +105,7 @@ public abstract class AbsCallbackStrategy implements PayStrategy {
// 回调类型
String callbackType = Optional.ofNullable(this.getCallbackType())
.map(PayCallbackTypeEnum::getCode)
.map(PaymentTypeEnum::getCode)
.orElse(null);
PayCallbackRecord payNotifyRecord = new PayCallbackRecord()

View File

@@ -7,6 +7,7 @@ import cn.bootx.platform.daxpay.param.pay.RefundParam;
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 lombok.Getter;
import lombok.Setter;
@@ -25,6 +26,9 @@ public abstract class AbsRefundStrategy implements PayStrategy{
/** 支付订单 */
private PayOrder payOrder = null;
/** 退款订单 已经持久化, 后续需要更新 */
private PayRefundOrder refundOrder = null;
/** 当前通道的订单 */
private PayChannelOrder payChannelOrder = null;
@@ -34,7 +38,7 @@ public abstract class AbsRefundStrategy implements PayStrategy{
/** 当前通道的退款参数 退款参数中的与这个不一致, 以这个为准 */
private RefundChannelParam refundChannelParam = null;
/** 当前通道的退款订单 */
/** 当前通道的退款订单 未持久化, 需要后续更新 */
private PayRefundChannelOrder refundChannelOrder;
/**
@@ -46,8 +50,19 @@ public abstract class AbsRefundStrategy implements PayStrategy{
this.refundParam = refundParam;
}
/**
* 退款前对处理 包含必要的校验以及对Payment对象的创建和保存操作
* 退款前预扣通道和支付订单的金额
*/
public void doPreDeductOrderHandler(){
PayChannelOrder payChannelOrder = this.getPayChannelOrder();
int refundableBalance = payChannelOrder.getRefundableBalance() - this.getRefundChannelParam().getAmount();
payChannelOrder.setRefundableBalance(refundableBalance)
.setStatus(PayStatusEnum.REFUNDING.getCode());
}
/**
* 退款前对处理
*/
public void doBeforeRefundHandler() {}
@@ -64,7 +79,7 @@ public abstract class AbsRefundStrategy implements PayStrategy{
this.refundChannelOrder.setStatus(PayRefundStatusEnum.SUCCESS.getCode())
.setRefundTime(LocalDateTime.now());
// 支付通道订单可退余额
// 支付通道订单可退余额
int refundableBalance = this.getPayChannelOrder().getRefundableBalance() - this.refundChannelOrder.getAmount();
// 支付通道订单状态
PayStatusEnum status = refundableBalance == 0 ? PayStatusEnum.REFUNDED : PayStatusEnum.PARTIAL_REFUND;

View File

@@ -22,6 +22,10 @@ public abstract class AbsRefundSyncStrategy implements PayStrategy{
this.refundOrder = refundOrder;
}
/**
* 同步前处理, 主要是预防请求过于迅速, 支付网关没有处理完退款请求, 导致返回的状态不正确
*/
public void doBeforeHandler(){}
/**
* 异步支付单与支付网关进行状态比对后的结果
* @see PaySyncStatusEnum