ref 支付退款重构, 支付上下文调整

This commit is contained in:
xxm1995
2023-12-26 16:32:56 +08:00
parent e276d3597c
commit 21bf0e40a3
14 changed files with 353 additions and 57 deletions

View File

@@ -20,7 +20,7 @@ public enum PayStatusEnum {
/** 超时取消 */
TIMEOUT("timeout","超时取消"),
PARTIAL_REFUND("partial_refund","部分退款"),
REFUNDED("REFUNDED","退款");
REFUNDED("REFUNDED","全部退款");
/** 编码 */
private final String code;

View File

@@ -9,6 +9,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@@ -27,10 +28,11 @@ public class RefundChannelParam {
*/
@Schema(description = "支付通道编码")
@NotBlank(message = "支付通道编码不可为空")
private String payChannel;
private String channel;
@Schema(description = "退款金额")
@NotNull(message = "退款金额不可为空")
@Min(1)
private Integer amount;
/**

View File

@@ -40,7 +40,7 @@ public class RefundParam extends PayCommonParam {
*/
@Valid
@Schema(description = "退款参数列表")
private List<RefundChannelParam> refundModes;
private List<RefundChannelParam> refundChannels;
@Schema(description = "退款原因")
private String reason;

View File

@@ -22,18 +22,15 @@ import javax.validation.constraints.NotNull;
@Schema(title = "简单退款参数")
public class SimpleRefundParam extends PayCommonParam {
/**
* 优先级高于业务号
*/
@Schema(description = "支付单ID")
private Long paymentId;
@Schema(description = "业务号")
private String businessNo;
/**
* 部分退款需要传输refundModes参数
*/
@Schema(description = "是否全部退款")
private boolean refundAll;
/**
* 部分退款时此项必填
*/
@@ -46,6 +43,13 @@ public class SimpleRefundParam extends PayCommonParam {
@NotBlank(message = "支付通道编码不可为空")
private String payChannel;
/**
* 部分退款需要传输refundModes参数
*/
@Schema(description = "是否全部退款")
private boolean refundAll;
@Schema(description = "退款金额")
@NotNull(message = "退款金额不可为空")
private Integer amount;

View File

@@ -0,0 +1,14 @@
package cn.bootx.platform.daxpay.common.context;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 退款请求上下文
* @author xxm
* @since 2023/12/26
*/
@Data
@Accessors(chain = true)
public class RefundRequestLocal {
}

View File

@@ -31,4 +31,5 @@ public class RequestLocal {
/** 请求链路id */
private String reqId;
}

View File

@@ -3,6 +3,7 @@ package cn.bootx.platform.daxpay.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.handler.JacksonRawTypeHandler;
import cn.bootx.platform.daxpay.code.PayRefundStatusEnum;
import cn.bootx.platform.daxpay.common.entity.OrderRefundableInfo;
import cn.bootx.platform.daxpay.core.order.refund.convert.RefundConvert;
import cn.bootx.platform.daxpay.dto.order.refund.PayRefundOrderDto;
@@ -59,9 +60,9 @@ public class PayRefundOrder extends MpBaseEntity implements EntityBaseFunction<P
/**
* 退款状态
* @see PayStatus#REFUND_PROCESS_FAIL
* @see PayRefundStatusEnum
*/
private String refundStatus;
private String status;
/** 错误码 */
private String errorCode;

View File

@@ -0,0 +1,61 @@
package cn.bootx.platform.daxpay.core.payment.common.service;
import cn.bootx.platform.common.core.code.CommonCode;
import cn.bootx.platform.daxpay.common.context.PlatformLocal;
import cn.bootx.platform.daxpay.common.context.RequestLocal;
import cn.bootx.platform.daxpay.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.core.system.entity.PlatformConfig;
import cn.bootx.platform.daxpay.core.system.service.PlatformConfigService;
import cn.bootx.platform.daxpay.param.pay.PayCommonParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Service;
/**
* 支付、退款等各类操作支持服务
* @author xxm
* @since 2023/12/26
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class PaymentAssistService {
private final PlatformConfigService platformConfigService;
/**
* 初始化上下文
*/
public void initContext(PayCommonParam payCommonParam){
this.initPlatform();
this.initRequest(payCommonParam);
}
/**
* 初始化平台配置上下文
*/
private void initPlatform(){
PlatformConfig config = platformConfigService.getConfig();
PlatformLocal platform = PaymentContextLocal.get().getPlatform();
platform.setSignType(config.getSignType());
platform.setSignSecret(config.getSignSecret());
platform.setNotifyUrl(config.getNotifyUrl());
platform.setOrderTimeout(config.getOrderTimeout());
platform.setWebsiteUrl(config.getWebsiteUrl());
}
/**
* 初始化请求相关信息上下文
*/
private void initRequest(PayCommonParam payCommonParam){
RequestLocal request = PaymentContextLocal.get().getRequest();
request.setClientIp(payCommonParam.getClientIp())
.setExtraParam(payCommonParam.getExtraParam())
.setSign(payCommonParam.getSign())
.setVersion(payCommonParam.getVersion())
.setReqTime(payCommonParam.getReqTime())
.setReqId(MDC.get(CommonCode.TRACE_ID));
}
}

View File

@@ -4,7 +4,6 @@ import cn.bootx.platform.daxpay.code.PaySignTypeEnum;
import cn.bootx.platform.daxpay.common.context.ApiInfoLocal;
import cn.bootx.platform.daxpay.common.context.PlatformLocal;
import cn.bootx.platform.daxpay.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.core.payment.pay.service.PayAssistService;
import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.param.pay.PayCommonParam;
import cn.hutool.core.util.StrUtil;
@@ -28,25 +27,25 @@ public class PaymentSignService {
private static final String FIELD_SIGN = "sign";
private final PayAssistService payAssistService;;
private final PaymentAssistService paymentAssistService;;
/**
* 签名
*/
public void verifySign(PayCommonParam param) {
// 先触发一下平台配置上下文的初始化
payAssistService.initPlatform();
// 先触发上下文的初始化
paymentAssistService.initContext(param);
ApiInfoLocal apiInfo = PaymentContextLocal.get().getApiInfo();
PlatformLocal platform = PaymentContextLocal.get().getPlatform();
// 判断当前接口是否不需要签名
if (!apiInfo.isReqSign()){
return;
}
// 参数转换为Map对象
PlatformLocal platform = PaymentContextLocal.get().getPlatform();
Map<String, String> params = param.toMap();
String signType = platform.getSignType();
// 生成签名前先去除sign
// 生成签名前先去除sign参数
params.remove(FIELD_SIGN);
String data = PayKit.createLinkString(params);
if (Objects.equals(PaySignTypeEnum.HMAC_SHA256.getCode(), signType)){

View File

@@ -1,20 +1,15 @@
package cn.bootx.platform.daxpay.core.payment.pay.service;
import cn.bootx.platform.common.core.code.CommonCode;
import cn.bootx.platform.daxpay.common.context.AsyncPayLocal;
import cn.bootx.platform.daxpay.common.context.NoticeLocal;
import cn.bootx.platform.daxpay.common.context.PlatformLocal;
import cn.bootx.platform.daxpay.common.context.RequestLocal;
import cn.bootx.platform.daxpay.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.core.order.pay.entity.PayOrder;
import cn.bootx.platform.daxpay.core.system.entity.PlatformConfig;
import cn.bootx.platform.daxpay.core.system.service.PlatformConfigService;
import cn.bootx.platform.daxpay.param.pay.PayParam;
import cn.bootx.platform.daxpay.util.PayUtil;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@@ -29,19 +24,6 @@ import java.util.Objects;
@Service
@RequiredArgsConstructor
public class PayAssistService {
private final PlatformConfigService platformConfigService;
/**
* 初始化平台配置上下文
*/
public void initPlatform(){
PlatformConfig config = platformConfigService.getConfig();
PlatformLocal platform = PaymentContextLocal.get().getPlatform();
platform.setSignType(config.getSignType());
platform.setSignSecret(config.getSignSecret());
platform.setNotifyUrl(config.getNotifyUrl());
platform.setOrderTimeout(config.getOrderTimeout());
platform.setWebsiteUrl(config.getWebsiteUrl());
}
/**
* 初始化支付相关上下文
@@ -51,8 +33,6 @@ public class PayAssistService {
this.initExpiredTime(order,payParam);
// 初始化通知相关上下文
this.initNotice(payParam);
// 初始化请求相关上下文
this.initRequest(payParam);
}
@@ -103,16 +83,4 @@ public class PayAssistService {
noticeInfo.setQuitUrl(payParam.getQuitUrl());
}
/**
* 初始化支付请求相关信息
*/
public void initRequest(PayParam payParam){
RequestLocal request = PaymentContextLocal.get().getRequest();
request.setClientIp(payParam.getClientIp())
.setExtraParam(payParam.getExtraParam())
.setSign(payParam.getSign())
.setVersion(payParam.getVersion())
.setReqTime(payParam.getReqTime())
.setReqId(MDC.get(CommonCode.TRACE_ID));
}
}

View File

@@ -30,8 +30,8 @@ public class PayRefundStrategyFactory {
*/
public static AbsPayRefundStrategy create(RefundChannelParam refundChannelParam) {
AbsPayRefundStrategy strategy = null;
PayChannelEnum channelEnum = PayChannelEnum.findByCode(refundChannelParam.getPayChannel());
AbsPayRefundStrategy strategy;
PayChannelEnum channelEnum = PayChannelEnum.findByCode(refundChannelParam.getChannel());
switch (channelEnum) {
case ALI:
strategy = SpringUtil.getBean(AliPayRefundStrategy.class);
@@ -86,13 +86,13 @@ public class PayRefundStrategyFactory {
// 同步支付
val syncRefundModeParams = refundChannelParams.stream()
.filter(Objects::nonNull)
.filter(payModeParam -> !ASYNC_TYPE_CODE.contains(payModeParam.getPayChannel()))
.filter(payModeParam -> !ASYNC_TYPE_CODE.contains(payModeParam.getChannel()))
.collect(Collectors.toList());
// 异步支付
val asyncRefundModeParams = refundChannelParams.stream()
.filter(Objects::nonNull)
.filter(payModeParam -> ASYNC_TYPE_CODE.contains(payModeParam.getPayChannel()))
.filter(payModeParam -> ASYNC_TYPE_CODE.contains(payModeParam.getChannel()))
.collect(Collectors.toList());
List<RefundChannelParam> sortList = new ArrayList<>(refundChannelParams.size());

View File

@@ -0,0 +1,102 @@
package cn.bootx.platform.daxpay.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.PayStatusEnum;
import cn.bootx.platform.daxpay.common.context.NoticeLocal;
import cn.bootx.platform.daxpay.common.context.PlatformLocal;
import cn.bootx.platform.daxpay.common.local.PaymentContextLocal;
import cn.bootx.platform.daxpay.core.order.pay.dao.PayOrderManager;
import cn.bootx.platform.daxpay.core.order.pay.entity.PayOrder;
import cn.bootx.platform.daxpay.exception.pay.PayFailureException;
import cn.bootx.platform.daxpay.param.pay.RefundParam;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* 支付退款支撑服务
* @author xxm
* @since 2023/12/26
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class PayRefundAssistService {
private final PayOrderManager payOrderManager;
/**
* 初始化上下文
*/
public void initRefundContext(RefundParam param, PayOrder payOrder){
// 初始化通知相关上下文
this.initNotice(param);
// 初始化请求相关上下文
this.initRequest(param);
}
/**
* 初始化通知相关上下文
*/
private void initNotice(RefundParam param) {
NoticeLocal noticeInfo = PaymentContextLocal.get().getNoticeInfo();
PlatformLocal platform = PaymentContextLocal.get().getPlatform();
// 异步回调
if (!param.isNotNotify()){
noticeInfo.setNotifyUrl(param.getReturnUrl());
if (StrUtil.isNotBlank(param.getNotifyUrl())){
noticeInfo.setNotifyUrl(platform.getNotifyUrl());
}
}
}
public void initRequest(RefundParam param){
}
/**
* 根据退款参数获取支付订单, 并进行检查
*/
public PayOrder getPayOrderAndCheckByRefundParam(RefundParam param, boolean simple){
if (!param.isRefundAll()){
if (CollUtil.isEmpty(param.getRefundChannels())){
throw new ValidationFailedException("退款通道参数不能为空");
}
if (Objects.isNull(param.getRefundNo())){
throw new ValidationFailedException("部分退款时退款单号必传");
}
}
PayOrder payOrder = null;
if (Objects.nonNull(param.getPaymentId())){
payOrder = payOrderManager.findById(param.getPaymentId())
.orElseThrow(() -> new PayFailureException("未查询到支付订单"));
}
if (Objects.isNull(payOrder)){
payOrder = payOrderManager.findByBusinessNo(param.getBusinessNo())
.orElseThrow(() -> new PayFailureException("未查询到支付订单"));
}
// 简单退款校验
if (payOrder.isCombinationPayMode() != simple){
throw new PayFailureException("组合支付不可以使用简单退款方式");
}
// 状态判断, 支付中/失败/取消等不能进行退款
List<String> tradesStatus = Arrays.asList(
PayStatusEnum.PROGRESS.getCode(),
PayStatusEnum.CLOSE.getCode(),
PayStatusEnum.CANCEL.getCode(),
PayStatusEnum.TIMEOUT.getCode(),
PayStatusEnum.FAIL.getCode());
if (tradesStatus.contains(payOrder.getStatus())) {
throw new PayFailureException("状态非法, 无法退款");
}
return payOrder;
}
}

View File

@@ -1,10 +1,29 @@
package cn.bootx.platform.daxpay.core.payment.refund.service;
import cn.bootx.platform.common.core.util.ValidationUtil;
import cn.bootx.platform.daxpay.code.PayStatusEnum;
import cn.bootx.platform.daxpay.core.order.pay.dao.PayOrderManager;
import cn.bootx.platform.daxpay.core.order.pay.entity.PayOrder;
import cn.bootx.platform.daxpay.core.payment.refund.factory.PayRefundStrategyFactory;
import cn.bootx.platform.daxpay.core.payment.refund.func.AbsPayRefundStrategy;
import cn.bootx.platform.daxpay.core.payment.refund.func.PayRefundStrategyConsumer;
import cn.bootx.platform.daxpay.exception.pay.PayUnsupportedMethodException;
import cn.bootx.platform.daxpay.param.pay.RefundChannelParam;
import cn.bootx.platform.daxpay.param.pay.RefundParam;
import cn.bootx.platform.daxpay.param.pay.SimpleRefundParam;
import cn.bootx.platform.daxpay.result.pay.RefundResult;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollectionUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
/**
* 支付退款服务
@@ -16,13 +35,137 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor
public class PayRefundService {
private final PayRefundAssistService payRefundAssistService;;
private final PayOrderManager payOrderManager;
/**
* 支付退款
*/
@Transactional(rollbackFor = Exception.class )
public RefundResult refund(RefundParam param){
return null;
return this.refund(param,false);
}
/**
* 简单退款
*/
@Transactional(rollbackFor = Exception.class )
public RefundResult simpleRefund(SimpleRefundParam param){
ValidationUtil.validateParam(param);
// 构建退款参数
RefundParam refundParam = new RefundParam();
BeanUtil.copyProperties(param,refundParam);
RefundChannelParam channelParam = new RefundChannelParam()
.setAmount(param.getAmount())
.setChannel(param.getPayChannel())
.setChannelExtra(param.getChannelExtra());
refundParam.setRefundChannels(Collections.singletonList(channelParam));
return this.refund(refundParam,true);
}
/**
* 支付退款方法
* @param param 退款参数
* @param simple 是否简单退款
*/
private RefundResult refund(RefundParam param, boolean simple){
// 参数校验
ValidationUtil.validateParam(param);
PayOrder payOrder = payRefundAssistService.getPayOrderAndCheckByRefundParam(param, simple);
// 退款上下文初始化
payRefundAssistService.initRefundContext(param,payOrder);
// 是否全部退款
if (param.isRefundAll()){
// 全部退款根据支付订单的退款信息构造退款参数
List<RefundChannelParam> channelParams = payOrder.getRefundableInfos()
.stream()
.map(o -> new RefundChannelParam().setChannel(o.getChannel())
.setAmount(o.getAmount()))
.collect(Collectors.toList());
param.setRefundChannels(channelParams);
}
return this.refundByChannel(param,payOrder);
}
/**
* 分支付通道进行退款
*/
public RefundResult refundByChannel(RefundParam param,PayOrder order){
List<RefundChannelParam> refundChannels = param.getRefundChannels();
// 1.获取退款参数方式,通过工厂生成对应的策略组
List<AbsPayRefundStrategy> payRefundStrategies = PayRefundStrategyFactory.create(refundChannels);
if (CollectionUtil.isEmpty(payRefundStrategies)) {
throw new PayUnsupportedMethodException();
}
// 2.初始化支付的参数
for (AbsPayRefundStrategy refundStrategy : payRefundStrategies) {
refundStrategy.initPayParam(order, param);
}
// 3.支付前准备
this.doHandler(refundChannels, order, payRefundStrategies, AbsPayRefundStrategy::doBeforeRefundHandler, null);
// 4.执行退款
this.doHandler(refundChannels,order, payRefundStrategies, AbsPayRefundStrategy::doRefundHandler, (strategyList, payOrder) -> {
this.paymentHandler(payOrder, refundChannels);
});
return new RefundResult();
}
/**
* 处理方法
* @param channelParams 退款方式参数
* @param payOrder 支付记录
* @param strategyList 退款策略
* @param refundStrategy 执行方法
* @param successCallback 成功操作
*/
private void doHandler( List<RefundChannelParam> channelParams,
PayOrder payOrder,
List<AbsPayRefundStrategy> strategyList,
Consumer<AbsPayRefundStrategy> refundStrategy,
PayRefundStrategyConsumer<List<AbsPayRefundStrategy>, PayOrder> successCallback) {
try {
// 执行策略操作,如退款前/退款时
// 等同strategyList.forEach(payMethod.accept(PaymentStrategy))
strategyList.forEach(refundStrategy);
// 执行操作成功的处
Optional.ofNullable(successCallback).ifPresent(fun -> fun.accept(strategyList, payOrder));
}
catch (Exception e) {
// 记录退款失败的记录
Integer i = channelParams.stream()
.map(RefundChannelParam::getAmount)
.reduce(0,Integer::sum);
// TODO 保存
// SpringUtil.getBean(this.getClass()).saveRefund(payOrder, amount, channelParams);
throw e;
}
}
/**
* 支付订单处理
*/
private void paymentHandler(PayOrder payOrder, List<RefundChannelParam> refundModeParams) {
Integer amount = refundModeParams.stream()
.map(RefundChannelParam::getAmount)
.reduce(0, Integer::sum);
// 剩余可退款余额
int refundableBalance = payOrder.getRefundableBalance() - amount;
// 退款完成
if (refundableBalance == 0) {
payOrder.setStatus(PayStatusEnum.REFUNDED.getCode());
}
else {
payOrder.setStatus(PayStatusEnum.PARTIAL_REFUND.getCode());
}
payOrder.setRefundableBalance(refundableBalance);
payOrderManager.updateById(payOrder);
// TODO 记录退款成功的记录
// SpringUtil.getBean(this.getClass()).saveRefund(payOrder, amount, refundModeParams);
}
}

View File

@@ -1,6 +1,7 @@
package cn.bootx.platform.daxpay.dto.order.refund;
import cn.bootx.platform.common.core.rest.dto.BaseDto;
import cn.bootx.platform.daxpay.code.PayRefundStatusEnum;
import cn.bootx.platform.daxpay.common.entity.OrderRefundableInfo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -51,10 +52,10 @@ public class PayRefundOrderDto extends BaseDto {
private List<OrderRefundableInfo> refundableInfo;
/**
* @see PayStatusCode#REFUND_PROCESS_FAIL
* @see PayRefundStatusEnum
*/
@Schema(description = "退款状态")
private String refundStatus;
private String status;
@Schema(description = "错误码")
private String errorCode;