Commit 67aaf28832bd86a75e6fd9b54e286f7a94768428

Authored by YunaiV
1 parent d556eae5

code review 退款逻辑

yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/convert/order/PayRefundCoreConvert.java
... ... @@ -12,8 +12,6 @@ public interface PayRefundCoreConvert {
12 12  
13 13 PayRefundCoreConvert INSTANCE = Mappers.getMapper(PayRefundCoreConvert.class);
14 14  
15   -
16   -
17 15 //TODO 太多需要处理了, 暂时不用
18 16 @Mappings(value = {
19 17 @Mapping(source = "amount", target = "payAmount"),
... ... @@ -21,4 +19,5 @@ public interface PayRefundCoreConvert {
21 19 @Mapping(target = "status",ignore = true)
22 20 })
23 21 PayRefundDO convert(PayOrderDO orderDO);
  22 +
24 23 }
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/order/PayRefundDO.java
... ... @@ -3,6 +3,8 @@ package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order;
3 3 import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
4 4 import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
5 5 import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayMerchantDO;
  6 +import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
  7 +import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundTypeEnum;
6 8 import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
7 9 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
8 10 import com.baomidou.mybatisplus.annotation.TableId;
... ... @@ -79,7 +81,6 @@ public class PayRefundDO extends BaseDO {
79 81 */
80 82 private Long orderId;
81 83  
82   -
83 84 /**
84 85 * 交易订单号,根据规则生成
85 86 * 调用支付渠道时,使用该字段作为对接的订单号。
... ... @@ -110,7 +111,6 @@ public class PayRefundDO extends BaseDO {
110 111 * 防止该笔交易重复退款。支付宝会保证同样的退款请求号多次请求只会退一次。
111 112 * 退款单请求号,根据规则生成
112 113 * 例如说,R202109181134287570000
113   - *
114 114 */
115 115 private String merchantRefundNo;
116 116  
... ... @@ -129,19 +129,22 @@ public class PayRefundDO extends BaseDO {
129 129 /**
130 130 * 退款状态
131 131 *
  132 + * 枚举 {@link PayRefundStatusEnum}
132 133 */
133 134 private Integer status;
134 135  
135 136 /**
136 137 * 退款类型(部分退款,全部退款)
  138 + *
  139 + * 枚举 {@link PayRefundTypeEnum}
137 140 */
138 141 private Integer type;
139 142 /**
140   - * 支付金额,单位分
  143 + * 支付金额,单位:分
141 144 */
142 145 private Long payAmount;
143 146 /**
144   - * 退款金额,单位分
  147 + * 退款金额,单位:分
145 148 */
146 149 private Long refundAmount;
147 150  
... ... @@ -150,7 +153,6 @@ public class PayRefundDO extends BaseDO {
150 153 */
151 154 private String reason;
152 155  
153   -
154 156 /**
155 157 * 用户 IP
156 158 */
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/order/PayRefundCoreMapper.java
... ... @@ -17,6 +17,7 @@ public interface PayRefundCoreMapper extends BaseMapperX<PayRefundDO> {
17 17 }
18 18  
19 19 default PayRefundDO selectByTradeNoAndMerchantRefundNo(String tradeNo, String merchantRefundNo){
20   - return selectOne("trade_no", tradeNo, "merchant_refund_no", merchantRefundNo);
  20 + return selectOne("trade_no", tradeNo, "merchant_refund_no", merchantRefundNo);
21 21 }
  22 +
22 23 }
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/order/PayRefundStatusEnum.java
... ... @@ -6,6 +6,7 @@ import lombok.Getter;
6 6 @Getter
7 7 @AllArgsConstructor
8 8 public enum PayRefundStatusEnum {
  9 +
9 10 CREATE(0, "退款订单生成"),
10 11 SUCCESS(1, "退款成功"),
11 12 FAILURE(2, "退款失败"),
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/dto/PayRefundReqDTO.java
... ... @@ -16,8 +16,9 @@ import lombok.experimental.Accessors;
16 16 @AllArgsConstructor
17 17 public class PayRefundReqDTO {
18 18  
  19 + // TODO @jason:增加下 validation 注解哈
19 20 /**
20   - * 支付订单编号自增
  21 + * 支付订单编号
21 22 */
22 23 private Long payOrderId;
23 24  
... ... @@ -31,10 +32,10 @@ public class PayRefundReqDTO {
31 32 */
32 33 private String reason;
33 34  
34   -
35 35 /**
36 36 * 商户退款订单号
37 37 */
  38 + // TODO @jason:merchantRefundNo=》merchantRefundId,保持和 PayOrder 的 merchantOrderId 一致哈
38 39 private String merchantRefundNo;
39 40  
40 41 /**
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/dto/PayRefundRespDTO.java
... ... @@ -21,10 +21,11 @@ public class PayRefundRespDTO {
21 21 * 退款处理中和退款成功 返回 1
22 22 * 失败和其他情况 返回 2
23 23 */
  24 + // TODO @jason:这个 result,可以使用 CommonResult 里呢
24 25 private Integer channelReturnResult;
25 26  
26 27 /**
27   - * 渠道返回code
  28 + * 渠道返回 code
28 29 */
29 30 private String channelReturnCode;
30 31  
... ... @@ -34,7 +35,8 @@ public class PayRefundRespDTO {
34 35 private String channelReturnMsg;
35 36  
36 37 /**
37   - * 支付退款单编号, 自增
  38 + * 支付退款单编号,自增
38 39 */
39 40 private Long refundId;
  41 +
40 42 }
... ...
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayRefundCoreServiceImpl.java
... ... @@ -11,20 +11,20 @@ import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderExtensio
11 11 import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayRefundCoreMapper;
12 12 import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum;
13 13 import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderNotifyStatusEnum;
14   -import cn.iocoder.yudao.coreservice.modules.pay.service.notify.PayNotifyCoreService;
15   -import cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
16   -import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayRefundReqDTO;
17   -import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundNotifyDTO;
18   -import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundTypeEnum;
19 14 import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderStatusEnum;
20 15 import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundStatusEnum;
  16 +import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayRefundTypeEnum;
21 17 import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayAppCoreService;
22 18 import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayChannelCoreService;
  19 +import cn.iocoder.yudao.coreservice.modules.pay.service.notify.PayNotifyCoreService;
  20 +import cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
23 21 import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayRefundCoreService;
  22 +import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayRefundReqDTO;
24 23 import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayRefundRespDTO;
25 24 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
26 25 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
27 26 import cn.iocoder.yudao.framework.pay.core.client.dto.PayNotifyDataDTO;
  27 +import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundNotifyDTO;
28 28 import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedReqDTO;
29 29 import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO;
30 30 import cn.iocoder.yudao.framework.pay.core.enums.PayChannelRefundRespEnum;
... ... @@ -34,7 +34,8 @@ import org.springframework.stereotype.Service;
34 34 import org.springframework.transaction.annotation.Transactional;
35 35  
36 36 import javax.annotation.Resource;
37   -import java.util.*;
  37 +import java.util.Date;
  38 +import java.util.Objects;
38 39  
39 40 import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.*;
40 41 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
... ... @@ -90,17 +91,19 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
90 91  
91 92 PayOrderExtensionDO orderExtensionDO = payOrderExtensionCoreMapper.selectById(order.getSuccessExtensionId());
92 93 PayRefundDO payRefundDO = payRefundCoreMapper.selectByTradeNoAndMerchantRefundNo(orderExtensionDO.getNo(), req.getMerchantRefundNo());
93   - //构造渠道的统一的退款请求参数
  94 + // 构造渠道的统一的退款请求参数
94 95 PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO();
95 96 if(Objects.nonNull(payRefundDO)){
96   - //退款订单已经提交过。
  97 + // 退款订单已经提交过。
97 98 //TODO 校验相同退款单的金额
98   - if(Objects.equals(PayRefundStatusEnum.SUCCESS.getStatus(), payRefundDO.getStatus())
99   - || Objects.equals(PayRefundStatusEnum.CLOSE.getStatus(), payRefundDO.getStatus())){
  99 + // TODO @jason:咱要不封装一个 ObjectUtils.equalsAny
  100 + if (Objects.equals(PayRefundStatusEnum.SUCCESS.getStatus(), payRefundDO.getStatus())
  101 + || Objects.equals(PayRefundStatusEnum.CLOSE.getStatus(), payRefundDO.getStatus())) {
100 102 //已成功退款
101 103 throw exception(PAY_REFUND_SUCCEED);
102   - }else{
103   - //保证商户退款单不变,重复向渠道发起退款。渠道保持幂等
  104 + } else{
  105 + // TODO @jason:这里不用 else,简洁一些
  106 + // 保证商户退款单不变,重复向渠道发起退款。渠道保持幂等
104 107 unifiedReqDTO.setUserIp(req.getUserIp())
105 108 .setAmount(payRefundDO.getRefundAmount())
106 109 .setChannelOrderNo(payRefundDO.getChannelOrderNo())
... ... @@ -109,7 +112,8 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
109 112 .setReason(payRefundDO.getReason());
110 113 }
111 114 }else{
112   - //新生成退款单。 退款单入库 退款单状态:生成
  115 + // 新生成退款单。 退款单入库 退款单状态:生成
  116 + // TODO @jason:封装一个小方法。插入退款单
113 117 payRefundDO = PayRefundDO.builder().channelOrderNo(order.getChannelOrderNo())
114 118 .appId(order.getAppId())
115 119 .channelOrderNo(order.getChannelOrderNo())
... ... @@ -130,6 +134,7 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
130 134 .type(refundType.getStatus())
131 135 .build();
132 136 payRefundCoreMapper.insert(payRefundDO);
  137 + // TODO @jason:这块的逻辑,和已存在的这块,貌似是统一的?
133 138 unifiedReqDTO.setUserIp(req.getUserIp())
134 139 .setAmount(payRefundDO.getRefundAmount())
135 140 .setChannelOrderNo(payRefundDO.getChannelOrderNo())
... ... @@ -137,19 +142,20 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
137 142 .setRefundReqNo(payRefundDO.getMerchantRefundNo())
138 143 .setReason(req.getReason());
139 144 }
140   - //向渠道发起退款申请
  145 + // 向渠道发起退款申请
141 146 PayRefundUnifiedRespDTO refundUnifiedRespDTO = client.unifiedRefund(unifiedReqDTO);
142   - //构造退款申请返回对象
  147 + // 构造退款申请返回对象
143 148 PayRefundRespDTO respDTO = new PayRefundRespDTO();
144   - if(refundUnifiedRespDTO.getChannelResp() == PayChannelRefundRespEnum.SUCCESS
145   - ||refundUnifiedRespDTO.getChannelResp() == PayChannelRefundRespEnum.PROCESSING ){
146   - //成功处理, 在退款通知中处理, 这里不处理
  149 + if (refundUnifiedRespDTO.getChannelResp() == PayChannelRefundRespEnum.SUCCESS
  150 + ||refundUnifiedRespDTO.getChannelResp() == PayChannelRefundRespEnum.PROCESSING) {
  151 + // 成功处理,在退款通知中处理, 这里不处理
147 152 respDTO.setChannelReturnResult(PayChannelRefundRespEnum.SUCCESS.getStatus());
148 153 respDTO.setRefundId(payRefundDO.getId());
149 154 }else {
150   - //失败返回错误给前端,可以重新发起退款,保证退款请求号(这里是商户退款单号), 避免重复退款。
  155 + // 失败返回错误给前端,可以重新发起退款,保证退款请求号(这里是商户退款单号), 避免重复退款。
  156 + // TODO @jason:失败的话,是不是可以跑出 ServiceException 业务异常。这样就是成功返回 refundId,失败业务异常
151 157 respDTO.setChannelReturnResult(PayChannelRefundRespEnum.FAILURE.getStatus());
152   - //更新退款单状态
  158 + // 更新退款单状态
153 159 PayRefundDO updatePayRefund = new PayRefundDO();
154 160 updatePayRefund.setId(payRefundDO.getId())
155 161 .setChannelErrorMsg(refundUnifiedRespDTO.getChannelMsg())
... ... @@ -181,35 +187,37 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
181 187 payRefundSuccess(refundNotify);
182 188 } else {
183 189 //TODO 支付异常, 支付宝似乎没有支付异常的通知。
  190 + // TODO @jason:那这里可以考虑打个 error logger
184 191 }
185 192 }
186 193  
187 194 private void payRefundSuccess(PayRefundNotifyDTO refundNotify) {
  195 + // 校验退款单存在
188 196 PayRefundDO refundDO = payRefundCoreMapper.selectByTradeNoAndMerchantRefundNo(refundNotify.getTradeNo(), refundNotify.getReqNo());
189 197 if (refundDO == null) {
190   - log.error("不存在 seqNo 为{} 的支付退款单",refundNotify.getReqNo());
  198 + log.error("[payRefundSuccess][不存在 seqNo 为{} 的支付退款单]", refundNotify.getReqNo());
191 199 throw exception(PAY_REFUND_NOT_FOUND);
192 200 }
193   - Long refundAmount = refundDO.getRefundAmount();
  201 +
  202 + // 计算订单的状态。如果全部退款,则订单处于关闭。TODO @jason:建议这里按照金额来判断,因为可能退款多次
194 203 Integer type = refundDO.getType();
195 204 PayOrderStatusEnum orderStatus = PayOrderStatusEnum.SUCCESS;
196   - if(PayRefundTypeEnum.ALL.getStatus().equals(type)){
  205 + if (PayRefundTypeEnum.ALL.getStatus().equals(type)){
197 206 orderStatus = PayOrderStatusEnum.CLOSED;
198 207 }
199   - // 更新支付订单
200   - PayOrderDO payOrderDO = payOrderCoreMapper.selectById(refundDO.getOrderId());
201 208 // 需更新已退金额
  209 + PayOrderDO payOrderDO = payOrderCoreMapper.selectById(refundDO.getOrderId());
202 210 Long refundedAmount = payOrderDO.getRefundAmount();
  211 + // 更新支付订单
203 212 PayOrderDO updateOrderDO = new PayOrderDO();
204 213 updateOrderDO.setId(refundDO.getOrderId())
205   - .setRefundAmount(refundedAmount + refundAmount)
  214 + .setRefundAmount(refundedAmount + refundDO.getRefundAmount())
206 215 .setStatus(orderStatus.getStatus())
207   - .setRefundTimes(payOrderDO.getRefundTimes()+1)
  216 + .setRefundTimes(payOrderDO.getRefundTimes() + 1)
208 217 .setRefundStatus(type);
209   -
210 218 payOrderCoreMapper.updateById(updateOrderDO);
211 219  
212   - // 跟新退款订单
  220 + // 更新退款订单
213 221 PayRefundDO updateRefundDO = new PayRefundDO();
214 222 updateRefundDO.setId(refundDO.getId())
215 223 .setSuccessTime(refundNotify.getRefundSuccessTime())
... ... @@ -219,7 +227,7 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
219 227 .setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
220 228 payRefundCoreMapper.updateById(updateRefundDO);
221 229  
222   - //插入退款通知记录
  230 + // 插入退款通知记录
223 231 // TODO 通知商户成功或者失败. 现在通知似乎没有实现, 只是回调
224 232 payNotifyCoreService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
225 233 .type(PayNotifyTypeEnum.REFUND.getType()).dataId(refundDO.getId()).build());
... ... @@ -235,12 +243,12 @@ public class PayRefundCoreServiceImpl implements PayRefundCoreService {
235 243 if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
236 244 throw exception(PAY_ORDER_STATUS_IS_NOT_SUCCESS);
237 245 }
238   - //是否已经全额退款
  246 + // 是否已经全额退款
239 247 if (PayRefundTypeEnum.ALL.getStatus().equals(order.getRefundStatus())) {
240 248 throw exception(PAY_REFUND_ALL_REFUNDED);
241 249 }
242 250 // 校验金额 退款金额不能大于 原定的金额
243   - if(req.getAmount() + order.getRefundAmount() > order.getAmount()){
  251 + if (req.getAmount() + order.getRefundAmount() > order.getAmount()){
244 252 throw exception(PAY_REFUND_AMOUNT_EXCEED);
245 253 }
246 254 // 校验渠道订单号
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayRefundUnifiedReqDTO.java
... ... @@ -28,6 +28,7 @@ public class PayRefundUnifiedReqDTO {
28 28 */
29 29 private String userIp;
30 30  
  31 + // TODO @jason:这个是否为非必传字段呀,只需要传递 payTradeNo 字段即可。尽可能精简
31 32 /**
32 33 * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 transaction_id
33 34 * https://opendocs.alipay.com/apis alipay.trade.refund 中的 trade_no
... ... @@ -35,7 +36,6 @@ public class PayRefundUnifiedReqDTO {
35 36 */
36 37 private String channelOrderNo;
37 38  
38   -
39 39 /**
40 40 * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_trade_no
41 41 * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
... ... @@ -43,24 +43,22 @@ public class PayRefundUnifiedReqDTO {
43 43 */
44 44 private String payTradeNo;
45 45  
46   -
  46 + // TODO @jason:这个字段,要不就使用 merchantRefundId,更直接
47 47 /**
48 48 * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
49   - * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
  49 + * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
50 50 * 退款请求单号 同一退款请求单号多次请求只退一笔。
51 51 * 使用 商户的退款单号。{PayRefundDO 字段 merchantRefundNo}
52 52 */
53 53 @NotEmpty(message = "退款请求单号")
54 54 private String refundReqNo;
55 55  
56   -
57 56 /**
58 57 * 退款原因
59 58 */
60 59 @NotEmpty(message = "退款原因不能为空")
61 60 private String reason;
62 61  
63   -
64 62 /**
65 63 * 退款金额,单位:分
66 64 */
... ... @@ -68,12 +66,10 @@ public class PayRefundUnifiedReqDTO {
68 66 @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
69 67 private Long amount;
70 68  
71   -
72   -
73   -
74 69 /**
75 70 * 退款结果 notify 回调地址, 支付宝退款不需要回调地址, 微信需要
76 71 */
77 72 @URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
78 73 private String notifyUrl;
  74 +
79 75 }
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayRefundUnifiedRespDTO.java
... ... @@ -17,11 +17,14 @@ import lombok.experimental.Accessors;
17 17 @AllArgsConstructor
18 18 @Data
19 19 public class PayRefundUnifiedRespDTO {
  20 +
  21 + // TODO @jason:可以合并下。退款处理中、成功,都是成功;其它就业务失败。这样,可以复用 PayCommonResult;这个 RespDTO 可以返回渠道的退款编号
20 22 /**
21 23 * 渠道的退款结果
22 24 */
23 25 private PayChannelRefundRespEnum channelResp;
24 26  
  27 + // TODO @json:channelReturnCode 和 channelReturnMsg 放到 PayCommonResult 里噶
25 28 /**
26 29 * 渠道返回码
27 30 */
... ...
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayChannelRefundRespEnum.java
... ... @@ -11,6 +11,7 @@ import lombok.Getter;
11 11 @Getter
12 12 @AllArgsConstructor
13 13 public enum PayChannelRefundRespEnum {
  14 +
14 15 SUCCESS(1, "退款成功"),
15 16 FAILURE(2, "退款失败"),
16 17 PROCESSING(3,"退款处理中"),
... ... @@ -18,4 +19,5 @@ public enum PayChannelRefundRespEnum {
18 19  
19 20 private final Integer status;
20 21 private final String name;
  22 +
21 23 }
... ...