code review 退款逻辑

This commit is contained in:
YunaiV 2021-12-30 09:08:11 +08:00
parent d556eae556
commit 67aaf28832
10 changed files with 67 additions and 52 deletions

View File

@ -12,8 +12,6 @@ public interface PayRefundCoreConvert {
PayRefundCoreConvert INSTANCE = Mappers.getMapper(PayRefundCoreConvert.class);
//TODO 太多需要处理了 暂时不用
@Mappings(value = {
@Mapping(source = "amount", target = "payAmount"),
@ -21,4 +19,5 @@ public interface PayRefundCoreConvert {
@Mapping(target = "status",ignore = true)
})
PayRefundDO convert(PayOrderDO orderDO);
}

View File

@ -3,6 +3,8 @@ package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayMerchantDO;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundTypeEnum;
import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
@ -79,7 +81,6 @@ public class PayRefundDO extends BaseDO {
*/
private Long orderId;
/**
* 交易订单号根据规则生成
* 调用支付渠道时使用该字段作为对接的订单号
@ -110,7 +111,6 @@ public class PayRefundDO extends BaseDO {
* 防止该笔交易重复退款支付宝会保证同样的退款请求号多次请求只会退一次
* 退款单请求号根据规则生成
* 例如说R202109181134287570000
*
*/
private String merchantRefundNo;
@ -129,19 +129,22 @@ public class PayRefundDO extends BaseDO {
/**
* 退款状态
*
* 枚举 {@link PayRefundStatusEnum}
*/
private Integer status;
/**
* 退款类型(部分退款全部退款)
*
* 枚举 {@link PayRefundTypeEnum}
*/
private Integer type;
/**
* 支付金额,单位
* 支付金额单位
*/
private Long payAmount;
/**
* 退款金额,单位
* 退款金额单位
*/
private Long refundAmount;
@ -150,7 +153,6 @@ public class PayRefundDO extends BaseDO {
*/
private String reason;
/**
* 用户 IP
*/

View File

@ -17,6 +17,7 @@ public interface PayRefundCoreMapper extends BaseMapperX<PayRefundDO> {
}
default PayRefundDO selectByTradeNoAndMerchantRefundNo(String tradeNo, String merchantRefundNo){
return selectOne("trade_no", tradeNo, "merchant_refund_no", merchantRefundNo);
return selectOne("trade_no", tradeNo, "merchant_refund_no", merchantRefundNo);
}
}

View File

@ -6,6 +6,7 @@ import lombok.Getter;
@Getter
@AllArgsConstructor
public enum PayRefundStatusEnum {
CREATE(0, "退款订单生成"),
SUCCESS(1, "退款成功"),
FAILURE(2, "退款失败"),

View File

@ -16,8 +16,9 @@ import lombok.experimental.Accessors;
@AllArgsConstructor
public class PayRefundReqDTO {
// TODO @jason增加下 validation 注解哈
/**
* 支付订单编号自增
* 支付订单编号
*/
private Long payOrderId;
@ -31,10 +32,10 @@ public class PayRefundReqDTO {
*/
private String reason;
/**
* 商户退款订单号
*/
// TODO @jasonmerchantRefundNo=merchantRefundId保持和 PayOrder merchantOrderId 一致哈
private String merchantRefundNo;
/**

View File

@ -21,10 +21,11 @@ public class PayRefundRespDTO {
* 退款处理中和退款成功 返回 1
* 失败和其他情况 返回 2
*/
// TODO @jason这个 result可以使用 CommonResult 里呢
private Integer channelReturnResult;
/**
* 渠道返回code
* 渠道返回 code
*/
private String channelReturnCode;
@ -34,7 +35,8 @@ public class PayRefundRespDTO {
private String channelReturnMsg;
/**
* 支付退款单编号 自增
* 支付退款单编号自增
*/
private Long refundId;
}

View File

@ -11,20 +11,20 @@ import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderExtensio
import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayRefundCoreMapper;
import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderNotifyStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.service.notify.PayNotifyCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayRefundReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundNotifyDTO;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundTypeEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundTypeEnum;
import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayAppCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayChannelCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.notify.PayNotifyCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayRefundReqDTO;
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.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;
@ -34,7 +34,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.*;
import java.util.Date;
import java.util.Objects;
import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -90,17 +91,19 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
PayOrderExtensionDO orderExtensionDO = payOrderExtensionCoreMapper.selectById(order.getSuccessExtensionId());
PayRefundDO payRefundDO = payRefundCoreMapper.selectByTradeNoAndMerchantRefundNo(orderExtensionDO.getNo(), req.getMerchantRefundNo());
//构造渠道的统一的退款请求参数
// 构造渠道的统一的退款请求参数
PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO();
if(Objects.nonNull(payRefundDO)){
//退款订单已经提交过
// 退款订单已经提交过
//TODO 校验相同退款单的金额
if(Objects.equals(PayRefundStatusEnum.SUCCESS.getStatus(), payRefundDO.getStatus())
|| Objects.equals(PayRefundStatusEnum.CLOSE.getStatus(), payRefundDO.getStatus())){
// TODO @jason咱要不封装一个 ObjectUtils.equalsAny
if (Objects.equals(PayRefundStatusEnum.SUCCESS.getStatus(), payRefundDO.getStatus())
|| Objects.equals(PayRefundStatusEnum.CLOSE.getStatus(), payRefundDO.getStatus())) {
//已成功退款
throw exception(PAY_REFUND_SUCCEED);
}else{
//保证商户退款单不变重复向渠道发起退款渠道保持幂等
} else{
// TODO @jason这里不用 else简洁一些
// 保证商户退款单不变重复向渠道发起退款渠道保持幂等
unifiedReqDTO.setUserIp(req.getUserIp())
.setAmount(payRefundDO.getRefundAmount())
.setChannelOrderNo(payRefundDO.getChannelOrderNo())
@ -109,7 +112,8 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
.setReason(payRefundDO.getReason());
}
}else{
//新生成退款单 退款单入库 退款单状态生成
// 新生成退款单 退款单入库 退款单状态生成
// TODO @jason封装一个小方法插入退款单
payRefundDO = PayRefundDO.builder().channelOrderNo(order.getChannelOrderNo())
.appId(order.getAppId())
.channelOrderNo(order.getChannelOrderNo())
@ -130,6 +134,7 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
.type(refundType.getStatus())
.build();
payRefundCoreMapper.insert(payRefundDO);
// TODO @jason这块的逻辑和已存在的这块貌似是统一的
unifiedReqDTO.setUserIp(req.getUserIp())
.setAmount(payRefundDO.getRefundAmount())
.setChannelOrderNo(payRefundDO.getChannelOrderNo())
@ -137,19 +142,20 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
.setRefundReqNo(payRefundDO.getMerchantRefundNo())
.setReason(req.getReason());
}
//向渠道发起退款申请
// 向渠道发起退款申请
PayRefundUnifiedRespDTO refundUnifiedRespDTO = client.unifiedRefund(unifiedReqDTO);
//构造退款申请返回对象
// 构造退款申请返回对象
PayRefundRespDTO respDTO = new PayRefundRespDTO();
if(refundUnifiedRespDTO.getChannelResp() == PayChannelRefundRespEnum.SUCCESS
||refundUnifiedRespDTO.getChannelResp() == PayChannelRefundRespEnum.PROCESSING ){
//成功处理 在退款通知中处理, 这里不处理
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())
@ -181,35 +187,37 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
payRefundSuccess(refundNotify);
} else {
//TODO 支付异常 支付宝似乎没有支付异常的通知
// TODO @jason那这里可以考虑打个 error logger
}
}
private void payRefundSuccess(PayRefundNotifyDTO refundNotify) {
// 校验退款单存在
PayRefundDO refundDO = payRefundCoreMapper.selectByTradeNoAndMerchantRefundNo(refundNotify.getTradeNo(), refundNotify.getReqNo());
if (refundDO == null) {
log.error("不存在 seqNo 为{} 的支付退款单",refundNotify.getReqNo());
log.error("[payRefundSuccess][不存在 seqNo 为{} 的支付退款单]", refundNotify.getReqNo());
throw exception(PAY_REFUND_NOT_FOUND);
}
Long refundAmount = refundDO.getRefundAmount();
// 计算订单的状态如果全部退款则订单处于关闭TODO @jason建议这里按照金额来判断因为可能退款多次
Integer type = refundDO.getType();
PayOrderStatusEnum orderStatus = PayOrderStatusEnum.SUCCESS;
if(PayRefundTypeEnum.ALL.getStatus().equals(type)){
if (PayRefundTypeEnum.ALL.getStatus().equals(type)){
orderStatus = PayOrderStatusEnum.CLOSED;
}
// 更新支付订单
PayOrderDO payOrderDO = payOrderCoreMapper.selectById(refundDO.getOrderId());
// 需更新已退金额
PayOrderDO payOrderDO = payOrderCoreMapper.selectById(refundDO.getOrderId());
Long refundedAmount = payOrderDO.getRefundAmount();
// 更新支付订单
PayOrderDO updateOrderDO = new PayOrderDO();
updateOrderDO.setId(refundDO.getOrderId())
.setRefundAmount(refundedAmount + refundAmount)
.setRefundAmount(refundedAmount + refundDO.getRefundAmount())
.setStatus(orderStatus.getStatus())
.setRefundTimes(payOrderDO.getRefundTimes()+1)
.setRefundTimes(payOrderDO.getRefundTimes() + 1)
.setRefundStatus(type);
payOrderCoreMapper.updateById(updateOrderDO);
// 新退款订单
// 新退款订单
PayRefundDO updateRefundDO = new PayRefundDO();
updateRefundDO.setId(refundDO.getId())
.setSuccessTime(refundNotify.getRefundSuccessTime())
@ -219,7 +227,7 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
payRefundCoreMapper.updateById(updateRefundDO);
//插入退款通知记录
// 插入退款通知记录
// TODO 通知商户成功或者失败. 现在通知似乎没有实现 只是回调
payNotifyCoreService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
.type(PayNotifyTypeEnum.REFUND.getType()).dataId(refundDO.getId()).build());
@ -235,12 +243,12 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
throw exception(PAY_ORDER_STATUS_IS_NOT_SUCCESS);
}
//是否已经全额退款
// 是否已经全额退款
if (PayRefundTypeEnum.ALL.getStatus().equals(order.getRefundStatus())) {
throw exception(PAY_REFUND_ALL_REFUNDED);
}
// 校验金额 退款金额不能大于 原定的金额
if(req.getAmount() + order.getRefundAmount() > order.getAmount()){
if (req.getAmount() + order.getRefundAmount() > order.getAmount()){
throw exception(PAY_REFUND_AMOUNT_EXCEED);
}
// 校验渠道订单号

View File

@ -28,6 +28,7 @@ public class PayRefundUnifiedReqDTO {
*/
private String userIp;
// TODO @jason这个是否为非必传字段呀只需要传递 payTradeNo 字段即可尽可能精简
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 transaction_id
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 trade_no
@ -35,7 +36,6 @@ public class PayRefundUnifiedReqDTO {
*/
private String channelOrderNo;
/**
* https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_trade_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
@ -43,24 +43,22 @@ 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_request_no
* https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
* 退款请求单号 同一退款请求单号多次请求只退一笔
* 使用 商户的退款单号{PayRefundDO 字段 merchantRefundNo}
*/
@NotEmpty(message = "退款请求单号")
private String refundReqNo;
/**
* 退款原因
*/
@NotEmpty(message = "退款原因不能为空")
private String reason;
/**
* 退款金额单位
*/
@ -68,12 +66,10 @@ public class PayRefundUnifiedReqDTO {
@DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
private Long amount;
/**
* 退款结果 notify 回调地址 支付宝退款不需要回调地址 微信需要
*/
@URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
private String notifyUrl;
}

View File

@ -17,11 +17,14 @@ import lombok.experimental.Accessors;
@AllArgsConstructor
@Data
public class PayRefundUnifiedRespDTO {
// TODO @jason可以合并下退款处理中成功都是成功其它就业务失败这样可以复用 PayCommonResult这个 RespDTO 可以返回渠道的退款编号
/**
* 渠道的退款结果
*/
private PayChannelRefundRespEnum channelResp;
// TODO @jsonchannelReturnCode channelReturnMsg 放到 PayCommonResult 里噶
/**
* 渠道返回码
*/

View File

@ -11,6 +11,7 @@ import lombok.Getter;
@Getter
@AllArgsConstructor
public enum PayChannelRefundRespEnum {
SUCCESS(1, "退款成功"),
FAILURE(2, "退款失败"),
PROCESSING(3,"退款处理中"),
@ -18,4 +19,5 @@ public enum PayChannelRefundRespEnum {
private final Integer status;
private final String name;
}