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); PayRefundCoreConvert INSTANCE = Mappers.getMapper(PayRefundCoreConvert.class);
//TODO 太多需要处理了 暂时不用 //TODO 太多需要处理了 暂时不用
@Mappings(value = { @Mappings(value = {
@Mapping(source = "amount", target = "payAmount"), @Mapping(source = "amount", target = "payAmount"),
@ -21,4 +19,5 @@ public interface PayRefundCoreConvert {
@Mapping(target = "status",ignore = true) @Mapping(target = "status",ignore = true)
}) })
PayRefundDO convert(PayOrderDO orderDO); 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.PayAppDO;
import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO; 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.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.pay.core.enums.PayChannelEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
@ -79,7 +81,6 @@ public class PayRefundDO extends BaseDO {
*/ */
private Long orderId; private Long orderId;
/** /**
* 交易订单号根据规则生成 * 交易订单号根据规则生成
* 调用支付渠道时使用该字段作为对接的订单号 * 调用支付渠道时使用该字段作为对接的订单号
@ -110,7 +111,6 @@ public class PayRefundDO extends BaseDO {
* 防止该笔交易重复退款支付宝会保证同样的退款请求号多次请求只会退一次 * 防止该笔交易重复退款支付宝会保证同样的退款请求号多次请求只会退一次
* 退款单请求号根据规则生成 * 退款单请求号根据规则生成
* 例如说R202109181134287570000 * 例如说R202109181134287570000
*
*/ */
private String merchantRefundNo; private String merchantRefundNo;
@ -129,19 +129,22 @@ public class PayRefundDO extends BaseDO {
/** /**
* 退款状态 * 退款状态
* *
* 枚举 {@link PayRefundStatusEnum}
*/ */
private Integer status; private Integer status;
/** /**
* 退款类型(部分退款全部退款) * 退款类型(部分退款全部退款)
*
* 枚举 {@link PayRefundTypeEnum}
*/ */
private Integer type; private Integer type;
/** /**
* 支付金额,单位 * 支付金额单位
*/ */
private Long payAmount; private Long payAmount;
/** /**
* 退款金额,单位 * 退款金额单位
*/ */
private Long refundAmount; private Long refundAmount;
@ -150,7 +153,6 @@ public class PayRefundDO extends BaseDO {
*/ */
private String reason; private String reason;
/** /**
* 用户 IP * 用户 IP
*/ */

View File

@ -19,4 +19,5 @@ public interface PayRefundCoreMapper extends BaseMapperX<PayRefundDO> {
default PayRefundDO selectByTradeNoAndMerchantRefundNo(String tradeNo, String merchantRefundNo){ 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 @Getter
@AllArgsConstructor @AllArgsConstructor
public enum PayRefundStatusEnum { public enum PayRefundStatusEnum {
CREATE(0, "退款订单生成"), CREATE(0, "退款订单生成"),
SUCCESS(1, "退款成功"), SUCCESS(1, "退款成功"),
FAILURE(2, "退款失败"), FAILURE(2, "退款失败"),

View File

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

View File

@ -21,6 +21,7 @@ public class PayRefundRespDTO {
* 退款处理中和退款成功 返回 1 * 退款处理中和退款成功 返回 1
* 失败和其他情况 返回 2 * 失败和其他情况 返回 2
*/ */
// TODO @jason这个 result可以使用 CommonResult 里呢
private Integer channelReturnResult; private Integer channelReturnResult;
/** /**
@ -37,4 +38,5 @@ public class PayRefundRespDTO {
* 支付退款单编号自增 * 支付退款单编号自增
*/ */
private Long refundId; 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.dal.mysql.order.PayRefundCoreMapper;
import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum; 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.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.PayOrderStatusEnum;
import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum; 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.PayAppCoreService;
import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayChannelCoreService; 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.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.coreservice.modules.pay.service.order.dto.PayRefundRespDTO;
import cn.iocoder.yudao.framework.pay.core.client.PayClient; 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.PayClientFactory;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO; 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.PayRefundUnifiedReqDTO;
import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO; 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.PayChannelRefundRespEnum;
@ -34,7 +34,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource; 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.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -95,11 +96,13 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
if(Objects.nonNull(payRefundDO)){ if(Objects.nonNull(payRefundDO)){
// 退款订单已经提交过 // 退款订单已经提交过
//TODO 校验相同退款单的金额 //TODO 校验相同退款单的金额
// TODO @jason咱要不封装一个 ObjectUtils.equalsAny
if (Objects.equals(PayRefundStatusEnum.SUCCESS.getStatus(), payRefundDO.getStatus()) if (Objects.equals(PayRefundStatusEnum.SUCCESS.getStatus(), payRefundDO.getStatus())
|| Objects.equals(PayRefundStatusEnum.CLOSE.getStatus(), payRefundDO.getStatus())) { || Objects.equals(PayRefundStatusEnum.CLOSE.getStatus(), payRefundDO.getStatus())) {
//已成功退款 //已成功退款
throw exception(PAY_REFUND_SUCCEED); throw exception(PAY_REFUND_SUCCEED);
} else{ } else{
// TODO @jason这里不用 else简洁一些
// 保证商户退款单不变重复向渠道发起退款渠道保持幂等 // 保证商户退款单不变重复向渠道发起退款渠道保持幂等
unifiedReqDTO.setUserIp(req.getUserIp()) unifiedReqDTO.setUserIp(req.getUserIp())
.setAmount(payRefundDO.getRefundAmount()) .setAmount(payRefundDO.getRefundAmount())
@ -110,6 +113,7 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
} }
}else{ }else{
// 新生成退款单 退款单入库 退款单状态生成 // 新生成退款单 退款单入库 退款单状态生成
// TODO @jason封装一个小方法插入退款单
payRefundDO = PayRefundDO.builder().channelOrderNo(order.getChannelOrderNo()) payRefundDO = PayRefundDO.builder().channelOrderNo(order.getChannelOrderNo())
.appId(order.getAppId()) .appId(order.getAppId())
.channelOrderNo(order.getChannelOrderNo()) .channelOrderNo(order.getChannelOrderNo())
@ -130,6 +134,7 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
.type(refundType.getStatus()) .type(refundType.getStatus())
.build(); .build();
payRefundCoreMapper.insert(payRefundDO); payRefundCoreMapper.insert(payRefundDO);
// TODO @jason这块的逻辑和已存在的这块貌似是统一的
unifiedReqDTO.setUserIp(req.getUserIp()) unifiedReqDTO.setUserIp(req.getUserIp())
.setAmount(payRefundDO.getRefundAmount()) .setAmount(payRefundDO.getRefundAmount())
.setChannelOrderNo(payRefundDO.getChannelOrderNo()) .setChannelOrderNo(payRefundDO.getChannelOrderNo())
@ -148,6 +153,7 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
respDTO.setRefundId(payRefundDO.getId()); respDTO.setRefundId(payRefundDO.getId());
}else { }else {
// 失败返回错误给前端可以重新发起退款保证退款请求号这里是商户退款单号) 避免重复退款 // 失败返回错误给前端可以重新发起退款保证退款请求号这里是商户退款单号) 避免重复退款
// TODO @jason失败的话是不是可以跑出 ServiceException 业务异常这样就是成功返回 refundId失败业务异常
respDTO.setChannelReturnResult(PayChannelRefundRespEnum.FAILURE.getStatus()); respDTO.setChannelReturnResult(PayChannelRefundRespEnum.FAILURE.getStatus());
// 更新退款单状态 // 更新退款单状态
PayRefundDO updatePayRefund = new PayRefundDO(); PayRefundDO updatePayRefund = new PayRefundDO();
@ -181,35 +187,37 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
payRefundSuccess(refundNotify); payRefundSuccess(refundNotify);
} else { } else {
//TODO 支付异常 支付宝似乎没有支付异常的通知 //TODO 支付异常 支付宝似乎没有支付异常的通知
// TODO @jason那这里可以考虑打个 error logger
} }
} }
private void payRefundSuccess(PayRefundNotifyDTO refundNotify) { private void payRefundSuccess(PayRefundNotifyDTO refundNotify) {
// 校验退款单存在
PayRefundDO refundDO = payRefundCoreMapper.selectByTradeNoAndMerchantRefundNo(refundNotify.getTradeNo(), refundNotify.getReqNo()); PayRefundDO refundDO = payRefundCoreMapper.selectByTradeNoAndMerchantRefundNo(refundNotify.getTradeNo(), refundNotify.getReqNo());
if (refundDO == null) { if (refundDO == null) {
log.error("不存在 seqNo 为{} 的支付退款单",refundNotify.getReqNo()); log.error("[payRefundSuccess][不存在 seqNo 为{} 的支付退款单]", refundNotify.getReqNo());
throw exception(PAY_REFUND_NOT_FOUND); throw exception(PAY_REFUND_NOT_FOUND);
} }
Long refundAmount = refundDO.getRefundAmount();
// 计算订单的状态如果全部退款则订单处于关闭TODO @jason建议这里按照金额来判断因为可能退款多次
Integer type = refundDO.getType(); Integer type = refundDO.getType();
PayOrderStatusEnum orderStatus = PayOrderStatusEnum.SUCCESS; PayOrderStatusEnum orderStatus = PayOrderStatusEnum.SUCCESS;
if (PayRefundTypeEnum.ALL.getStatus().equals(type)){ if (PayRefundTypeEnum.ALL.getStatus().equals(type)){
orderStatus = PayOrderStatusEnum.CLOSED; orderStatus = PayOrderStatusEnum.CLOSED;
} }
// 更新支付订单
PayOrderDO payOrderDO = payOrderCoreMapper.selectById(refundDO.getOrderId());
// 需更新已退金额 // 需更新已退金额
PayOrderDO payOrderDO = payOrderCoreMapper.selectById(refundDO.getOrderId());
Long refundedAmount = payOrderDO.getRefundAmount(); Long refundedAmount = payOrderDO.getRefundAmount();
// 更新支付订单
PayOrderDO updateOrderDO = new PayOrderDO(); PayOrderDO updateOrderDO = new PayOrderDO();
updateOrderDO.setId(refundDO.getOrderId()) updateOrderDO.setId(refundDO.getOrderId())
.setRefundAmount(refundedAmount + refundAmount) .setRefundAmount(refundedAmount + refundDO.getRefundAmount())
.setStatus(orderStatus.getStatus()) .setStatus(orderStatus.getStatus())
.setRefundTimes(payOrderDO.getRefundTimes() + 1) .setRefundTimes(payOrderDO.getRefundTimes() + 1)
.setRefundStatus(type); .setRefundStatus(type);
payOrderCoreMapper.updateById(updateOrderDO); payOrderCoreMapper.updateById(updateOrderDO);
// 新退款订单 // 新退款订单
PayRefundDO updateRefundDO = new PayRefundDO(); PayRefundDO updateRefundDO = new PayRefundDO();
updateRefundDO.setId(refundDO.getId()) updateRefundDO.setId(refundDO.getId())
.setSuccessTime(refundNotify.getRefundSuccessTime()) .setSuccessTime(refundNotify.getRefundSuccessTime())

View File

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

View File

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

View File

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