优化退款逻辑

This commit is contained in:
jason 2022-01-03 22:32:39 +08:00
parent 9a5f085369
commit 5bf3045544
16 changed files with 63 additions and 157 deletions

View File

@ -3,3 +3,6 @@ CHANGE COLUMN `channel_notify_data` `channel_notify_data` VARCHAR(2048) CHARACTE
ALTER TABLE `ruoyi-vue-pro`.`pay_refund`
CHANGE COLUMN `req_no` `req_no` VARCHAR(64) NULL COMMENT '退款单请求号' ;
ALTER TABLE `ruoyi-vue-pro`.`pay_refund`
DROP COLUMN `req_no`;

View File

@ -607,7 +607,7 @@ CREATE TABLE `pay_order_extension` (
`user_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户 IP',
`status` tinyint NOT NULL COMMENT '支付状态',
`channel_extras` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '支付渠道的额外参数',
`channel_notify_data` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '支付渠道异步通知的内容',
`channel_notify_data` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '支付渠道异步通知的内容',
`creator` varchar(64) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '更新者',
@ -720,7 +720,6 @@ COMMIT;
DROP TABLE IF EXISTS `pay_refund`;
CREATE TABLE `pay_refund` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '支付退款编号',
`req_no` varchar(64) NOT NULL COMMENT '退款单请求号',
`merchant_id` bigint NOT NULL COMMENT '商户编号',
`app_id` bigint NOT NULL COMMENT '应用编号',
`channel_id` bigint NOT NULL COMMENT '渠道编号',

View File

@ -43,7 +43,6 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
public void testGetRefundPage() {
// mock 数据
PayRefundDO dbRefund = randomPojo(PayRefundDO.class, o -> { // 等会查询到
o.setReqNo("RF0000001");
o.setMerchantId(1L);
o.setAppId(1L);
o.setChannelId(1L);
@ -115,7 +114,6 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
public void testGetRefundList() {
// mock 数据
PayRefundDO dbRefund = randomPojo(PayRefundDO.class, o -> { // 等会查询到
o.setReqNo("RF0000001");
o.setMerchantId(1L);
o.setAppId(1L);
o.setChannelId(1L);

View File

@ -36,20 +36,6 @@ public class PayRefundDO extends BaseDO {
@TableId
private Long id;
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
* 退款请求号
* 标识一次退款请求需要保证在交易号下唯一如需部分退款则此参数必传
* 针对同一次退款请求如果调用接口失败或异常了重试时需要保证退款请求号不能变更
* 防止该笔交易重复退款支付宝会保证同样的退款请求号多次请求只会退一次
* 退款单请求号根据规则生成
* 例如说R202109181134287570000
* 废弃使用 merchantRefundNo 做退款请求号
*/
@Deprecated
private String reqNo;
/**
* 商户编号
*

View File

@ -6,6 +6,10 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* 退款申请单 Request DTO
*/
@ -16,15 +20,17 @@ import lombok.experimental.Accessors;
@AllArgsConstructor
public class PayRefundReqDTO {
// TODO @jason增加下 validation 注解哈
/**
* 支付订单编号
*/
@NotNull(message = "支付订单编号不能为空")
private Long payOrderId;
/**
* 退款金额
*/
@NotNull(message = "退款金额不能为空")
@DecimalMin(value = "0", inclusive = false, message = "退款金额必须大于零")
private Long amount;
/**
@ -35,8 +41,8 @@ public class PayRefundReqDTO {
/**
* 商户退款订单号
*/
// TODO @jasonmerchantRefundNo=merchantRefundId保持和 PayOrder merchantOrderId 一致哈
private String merchantRefundNo;
@NotEmpty(message = "商户退款订单号不能为空")
private String merchantRefundId;
/**
* 用户 IP

View File

@ -16,24 +16,6 @@ import lombok.experimental.Accessors;
@AllArgsConstructor
public class PayRefundRespDTO {
/**
* 渠道返回结果
* 退款处理中和退款成功 返回 1
* 失败和其他情况 返回 2
*/
// TODO @jason这个 result可以使用 CommonResult 里呢
private Integer channelReturnResult;
/**
* 渠道返回 code
*/
private String channelReturnCode;
/**
* 渠道返回消息
*/
private String channelReturnMsg;
/**
* 支付退款单编号自增
*/

View File

@ -23,11 +23,11 @@ import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayRefundReqDT
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundNotifyDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRefundRespEnum;
import cn.iocoder.yudao.framework.pay.core.enums.PayNotifyRefundStatusEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -88,11 +88,8 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
if (Objects.equals(req.getAmount(), order.getAmount())) {
refundType = PayRefundTypeEnum.ALL;
}
PayOrderExtensionDO orderExtensionDO = payOrderExtensionCoreMapper.selectById(order.getSuccessExtensionId());
PayRefundDO payRefundDO = payRefundCoreMapper.selectByTradeNoAndMerchantRefundNo(orderExtensionDO.getNo(), req.getMerchantRefundNo());
// 构造渠道的统一的退款请求参数
PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO();
PayRefundDO payRefundDO = payRefundCoreMapper.selectByTradeNoAndMerchantRefundNo(orderExtensionDO.getNo(), req.getMerchantRefundId());
if(Objects.nonNull(payRefundDO)){
// 退款订单已经提交过
//TODO 校验相同退款单的金额
@ -101,19 +98,10 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
|| Objects.equals(PayRefundStatusEnum.CLOSE.getStatus(), payRefundDO.getStatus())) {
//已成功退款
throw exception(PAY_REFUND_SUCCEED);
} else{
// TODO @jason这里不用 else简洁一些
// 保证商户退款单不变重复向渠道发起退款渠道保持幂等
unifiedReqDTO.setUserIp(req.getUserIp())
.setAmount(payRefundDO.getRefundAmount())
.setChannelOrderNo(payRefundDO.getChannelOrderNo())
.setPayTradeNo(payRefundDO.getTradeNo())
.setRefundReqNo(payRefundDO.getMerchantRefundNo())
.setReason(payRefundDO.getReason());
}
}else{
// 新生成退款单 退款单入库 退款单状态生成
// TODO @jason封装一个小方法插入退款单
//可以重复提交保证 退款请求号 一致由渠道保证幂等
}else {
//成功插入退款单 状态为生成.没有和渠道交互
payRefundDO = PayRefundDO.builder().channelOrderNo(order.getChannelOrderNo())
.appId(order.getAppId())
.channelOrderNo(order.getChannelOrderNo())
@ -121,7 +109,7 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
.channelId(order.getChannelId())
.merchantId(order.getMerchantId())
.orderId(order.getId())
.merchantRefundNo(req.getMerchantRefundNo())
.merchantRefundNo(req.getMerchantRefundId())
.notifyUrl(app.getRefundNotifyUrl())
.payAmount(order.getAmount())
.refundAmount(req.getAmount())
@ -134,38 +122,21 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
.type(refundType.getStatus())
.build();
payRefundCoreMapper.insert(payRefundDO);
// TODO @jason这块的逻辑和已存在的这块貌似是统一的
unifiedReqDTO.setUserIp(req.getUserIp())
.setAmount(payRefundDO.getRefundAmount())
.setChannelOrderNo(payRefundDO.getChannelOrderNo())
.setPayTradeNo(payRefundDO.getTradeNo())
.setRefundReqNo(payRefundDO.getMerchantRefundNo())
.setReason(req.getReason());
}
PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO();
unifiedReqDTO.setUserIp(req.getUserIp())
.setAmount(req.getAmount())
.setChannelOrderNo(order.getChannelOrderNo())
.setPayTradeNo(orderExtensionDO.getNo())
.setMerchantRefundId(req.getMerchantRefundId())
.setReason(req.getReason());
// 向渠道发起退款申请
PayRefundUnifiedRespDTO refundUnifiedRespDTO = client.unifiedRefund(unifiedReqDTO);
// 构造退款申请返回对象
PayRefundRespDTO respDTO = new PayRefundRespDTO();
if (refundUnifiedRespDTO.getChannelResp() == PayChannelRefundRespEnum.SUCCESS
||refundUnifiedRespDTO.getChannelResp() == PayChannelRefundRespEnum.PROCESSING) {
// 成功处理在退款通知中处理, 这里不处理
respDTO.setChannelReturnResult(PayChannelRefundRespEnum.SUCCESS.getStatus());
respDTO.setRefundId(payRefundDO.getId());
}else {
// 失败返回错误给前端可以重新发起退款保证退款请求号这里是商户退款单号) 避免重复退款
// TODO @jason失败的话是不是可以跑出 ServiceException 业务异常这样就是成功返回 refundId失败业务异常
respDTO.setChannelReturnResult(PayChannelRefundRespEnum.FAILURE.getStatus());
// 更新退款单状态
PayRefundDO updatePayRefund = new PayRefundDO();
updatePayRefund.setId(payRefundDO.getId())
.setChannelErrorMsg(refundUnifiedRespDTO.getChannelMsg())
.setChannelErrorCode(refundUnifiedRespDTO.getChannelCode())
.setStatus(PayRefundStatusEnum.FAILURE.getStatus());
payRefundCoreMapper.updateById(updatePayRefund);
}
respDTO.setChannelReturnCode(refundUnifiedRespDTO.getChannelCode())
.setChannelReturnMsg(refundUnifiedRespDTO.getChannelMsg());
return respDTO;
PayCommonResult<PayRefundUnifiedRespDTO> refundUnifiedResult = client.unifiedRefund(unifiedReqDTO);
//检查是否失败失败抛出业务异常
//TODO 渠道的异常记录
refundUnifiedResult.checkError();
//成功在 退款回调中处理
return PayRefundRespDTO.builder().refundId(payRefundDO.getId()).build();
}
@ -187,7 +158,7 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
payRefundSuccess(refundNotify);
} else {
//TODO 支付异常 支付宝似乎没有支付异常的通知
// TODO @jason那这里可以考虑打个 error logger
// TODO @jason那这里可以考虑打个 error logger @芋艿 微信是否存在支付异常通知
}
}
@ -199,22 +170,22 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
throw exception(PAY_REFUND_NOT_FOUND);
}
// 计算订单的状态如果全部退款则订单处于关闭TODO @jason建议这里按照金额来判断因为可能退款多次
Integer type = refundDO.getType();
PayOrderStatusEnum orderStatus = PayOrderStatusEnum.SUCCESS;
if (PayRefundTypeEnum.ALL.getStatus().equals(type)){
orderStatus = PayOrderStatusEnum.CLOSED;
}
// 需更新已退金额
// 得到已退金额
PayOrderDO payOrderDO = payOrderCoreMapper.selectById(refundDO.getOrderId());
Long refundedAmount = payOrderDO.getRefundAmount();
PayOrderStatusEnum orderStatus = PayOrderStatusEnum.SUCCESS;
if(Objects.equals(payOrderDO.getAmount(), refundedAmount+ refundDO.getRefundAmount())){
//支付金额 = 已退金额 + 本次退款金额
orderStatus = PayOrderStatusEnum.CLOSED;
}
// 更新支付订单
PayOrderDO updateOrderDO = new PayOrderDO();
updateOrderDO.setId(refundDO.getOrderId())
.setRefundAmount(refundedAmount + refundDO.getRefundAmount())
.setStatus(orderStatus.getStatus())
.setRefundTimes(payOrderDO.getRefundTimes() + 1)
.setRefundStatus(type);
.setRefundStatus(refundDO.getType());
payOrderCoreMapper.updateById(updateOrderDO);
// 更新退款订单

View File

@ -39,7 +39,7 @@ public interface PayClient {
* @param reqDTO 统一退款请求信息
* @return 各支付渠道的统一返回结果
*/
PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
PayCommonResult<PayRefundUnifiedRespDTO> unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
/**
* 解析支付退款通知数据

View File

@ -43,7 +43,6 @@ public class PayRefundUnifiedReqDTO {
*/
private String payTradeNo;
// TODO @jason这个字段要不就使用 merchantRefundId更直接
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
@ -51,7 +50,7 @@ public class PayRefundUnifiedReqDTO {
* 使用 商户的退款单号{PayRefundDO 字段 merchantRefundNo}
*/
@NotEmpty(message = "退款请求单号")
private String refundReqNo;
private String merchantRefundId;
/**
* 退款原因

View File

@ -18,22 +18,8 @@ import lombok.experimental.Accessors;
@Data
public class PayRefundUnifiedRespDTO {
// TODO @jason可以合并下退款处理中成功都是成功其它就业务失败这样可以复用 PayCommonResult这个 RespDTO 可以返回渠道的退款编号
/**
* 渠道的退款结果
* 渠道退款单编号
*/
private PayChannelRefundRespEnum channelResp;
// TODO @jsonchannelReturnCode channelReturnMsg 放到 PayCommonResult 里噶
/**
* 渠道返回码
*/
private String channelCode;
/**
* 渠道返回信息
*/
private String channelMsg;
//TODO 退款资金渠
private String channelRefundId;
}

View File

@ -103,19 +103,19 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
@Override
public PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
PayRefundUnifiedRespDTO resp;
public PayCommonResult<PayRefundUnifiedRespDTO> unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
PayCommonResult<PayRefundUnifiedRespDTO> resp;
try {
resp = doUnifiedRefund(reqDTO);
} catch (Throwable ex) {
// 记录异常日志
log.error("[unifiedRefund][request({}) 发起退款失败]", toJsonString(reqDTO), ex);
throw exception(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR);
resp = PayCommonResult.error(ex);
}
return resp;
}
protected abstract PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
protected abstract PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
}

View File

@ -3,9 +3,9 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
import cn.iocoder.yudao.framework.pay.core.client.dto.*;
import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRefundRespEnum;
import cn.iocoder.yudao.framework.pay.core.enums.PayNotifyRefundStatusEnum;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayConfig;
@ -100,39 +100,32 @@ public abstract class AbstractAlipayClient extends AbstractPayClient<AlipayPayCl
* @return 退款请求 Response
*/
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
AlipayTradeRefundModel model=new AlipayTradeRefundModel();
model.setTradeNo(reqDTO.getChannelOrderNo());
model.setOutTradeNo(reqDTO.getPayTradeNo());
model.setOutRequestNo(reqDTO.getRefundReqNo());
model.setOutRequestNo(reqDTO.getMerchantRefundId());
model.setRefundAmount(calculateAmount(reqDTO.getAmount()).toString());
model.setRefundReason(reqDTO.getReason());
AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
refundRequest.setBizModel(model);
PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();
try {
AlipayTradeRefundResponse response = client.execute(refundRequest);
log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response));
if (response.isSuccess()) {
//退款成功,更新为PROCESSING_NOTIFY 而不是 SYNC_SUCCESS 通过支付宝回调接口处理退款导致触发的异步通知
//退款导致触发的异步通知是发送到支付接口中设置的notify_url
//TODO 沙箱环境 返回 的tradeNo(渠道退款单号 订单的tradNo 是一个值是不是理解不对?
respDTO.setChannelResp(PayChannelRefundRespEnum.SUCCESS)
.setChannelCode(response.getCode())
.setChannelMsg(response.getMsg());
//支付宝不返回退款单号设置为空
PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();
respDTO.setChannelRefundId("");
return PayCommonResult.build(response.getCode(), response.getMsg(), respDTO, codeMapping);
}else{
respDTO.setChannelResp(PayChannelRefundRespEnum.FAILURE)
.setChannelCode(response.getSubCode())
.setChannelMsg(response.getSubMsg());
//失败需要抛出异常
return PayCommonResult.build(response.getCode(), response.getMsg(), null, codeMapping);
}
return respDTO;
} catch (AlipayApiException e) {
//TODO 记录异常日志
log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e);
respDTO.setChannelResp(PayChannelRefundRespEnum.FAILURE)
.setChannelCode(e.getErrCode())
.setChannelMsg(e.getErrMsg());
return respDTO;
return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
}
}

View File

@ -149,7 +149,7 @@ public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
@Override
protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
protected PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
//TODO 需要实现
throw new UnsupportedOperationException();
}

View File

@ -37,8 +37,8 @@ public class PayRefundController {
PayRefundReqDTO req = PayRefundConvert.INSTANCE.convert(reqVO);
req.setUserIp(getClientIP());
//TODO 测试暂时模拟生成商户退款订单
if(StrUtil.isEmpty(reqVO.getMerchantRefundNo())) {
req.setMerchantRefundNo(PaySeqUtils.genMerchantRefundNo());
if(StrUtil.isEmpty(reqVO.getMerchantRefundId())) {
req.setMerchantRefundId(PaySeqUtils.genMerchantRefundNo());
}
return CommonResult.success( PayRefundConvert.INSTANCE.convert(payRefundCoreService.submitRefundOrder(req)));
}

View File

@ -29,6 +29,6 @@ public class PayRefundReqVO {
@ApiModelProperty(value = "商户退款订单号", required = true, example = "MR202111180000000001")
//TODO 测试暂时模拟生成
//@NotEmpty(message = "商户退款订单号")
private String merchantRefundNo;
private String merchantRefundId;
}

View File

@ -15,23 +15,6 @@ import lombok.experimental.Accessors;
@AllArgsConstructor
public class PayRefundRespVO {
/**
* 渠道返回结果
* 退款处理中和退款成功 返回 1
* 失败和其他情况 返回 2
*/
private Integer channelReturnResult;
/**
* 渠道返回code
*/
private String channelReturnCode;
/**
* 渠道返回消息
*/
private String channelReturnMsg;
/**
* 支付退款单编号 自增
*/