Commit 1c282bd3cb7ada650947b6978d6943df392c1e13

Authored by YunaiV
1 parent 6f475f8c

mall + pay:

1. 优化 PayClient 退款逻辑,返回业务失败 errorCode + errorMsg 错误码
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java
1 package cn.iocoder.yudao.framework.pay.core.client.dto.refund; 1 package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
2 2
  3 +import cn.iocoder.yudao.framework.pay.core.client.exception.PayException;
3 import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; 4 import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
4 import lombok.Data; 5 import lombok.Data;
5 6
@@ -44,4 +45,71 @@ public class PayRefundRespDTO { @@ -44,4 +45,71 @@ public class PayRefundRespDTO {
44 */ 45 */
45 private Object rawData; 46 private Object rawData;
46 47
  48 + /**
  49 + * 调用渠道的错误码
  50 + *
  51 + * 注意:这里返回的是业务异常,而是不系统异常。
  52 + * 如果是系统异常,则会抛出 {@link PayException}
  53 + */
  54 + private String channelErrorCode;
  55 + /**
  56 + * 调用渠道报错时,错误信息
  57 + */
  58 + private String channelErrorMsg;
  59 +
  60 + private PayRefundRespDTO() {
  61 + }
  62 +
  63 + /**
  64 + * 创建【WAITING】状态的退款返回
  65 + */
  66 + public static PayRefundRespDTO waitingOf(String channelRefundNo,
  67 + String outRefundNo, Object rawData) {
  68 + PayRefundRespDTO respDTO = new PayRefundRespDTO();
  69 + respDTO.status = PayRefundStatusRespEnum.WAITING.getStatus();
  70 + respDTO.channelRefundNo = channelRefundNo;
  71 + // 相对通用的字段
  72 + respDTO.outRefundNo = outRefundNo;
  73 + respDTO.rawData = rawData;
  74 + return respDTO;
  75 + }
  76 +
  77 + /**
  78 + * 创建【SUCCESS】状态的退款返回
  79 + */
  80 + public static PayRefundRespDTO successOf(String channelRefundNo, LocalDateTime successTime,
  81 + String outRefundNo, Object rawData) {
  82 + PayRefundRespDTO respDTO = new PayRefundRespDTO();
  83 + respDTO.status = PayRefundStatusRespEnum.SUCCESS.getStatus();
  84 + respDTO.channelRefundNo = channelRefundNo;
  85 + respDTO.successTime = successTime;
  86 + // 相对通用的字段
  87 + respDTO.outRefundNo = outRefundNo;
  88 + respDTO.rawData = rawData;
  89 + return respDTO;
  90 + }
  91 +
  92 + /**
  93 + * 创建【FAILURE】状态的退款返回
  94 + */
  95 + public static PayRefundRespDTO failureOf(String outRefundNo, Object rawData) {
  96 + return failureOf(null, null,
  97 + outRefundNo, rawData);
  98 + }
  99 +
  100 + /**
  101 + * 创建【FAILURE】状态的退款返回
  102 + */
  103 + public static PayRefundRespDTO failureOf(String channelErrorCode, String channelErrorMsg,
  104 + String outRefundNo, Object rawData) {
  105 + PayRefundRespDTO respDTO = new PayRefundRespDTO();
  106 + respDTO.status = PayRefundStatusRespEnum.FAILURE.getStatus();
  107 + respDTO.channelErrorCode = channelErrorCode;
  108 + respDTO.channelErrorMsg = channelErrorMsg;
  109 + // 相对通用的字段
  110 + respDTO.outRefundNo = outRefundNo;
  111 + respDTO.rawData = rawData;
  112 + return respDTO;
  113 + }
  114 +
47 } 115 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
@@ -98,7 +98,8 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen @@ -98,7 +98,8 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
98 try { 98 try {
99 return doParseOrderNotify(params, body); 99 return doParseOrderNotify(params, body);
100 } catch (Throwable ex) { 100 } catch (Throwable ex) {
101 - log.error("[parseOrderNotify][params({}) body({}) 解析失败]", params, body, ex); 101 + log.error("[parseOrderNotify][客户端({}) params({}) body({}) 解析失败]",
  102 + getId(), params, body, ex);
102 throw buildPayException(ex); 103 throw buildPayException(ex);
103 } 104 }
104 } 105 }
@@ -129,6 +130,20 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen @@ -129,6 +130,20 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
129 130
130 protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable; 131 protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
131 132
  133 + @Override
  134 + public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
  135 + try {
  136 + return doParseRefundNotify(params, body);
  137 + } catch (Throwable ex) {
  138 + log.error("[parseRefundNotify][客户端({}) params({}) body({}) 解析失败]",
  139 + getId(), params, body, ex);
  140 + throw buildPayException(ex);
  141 + }
  142 + }
  143 +
  144 + protected abstract PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body)
  145 + throws Throwable;
  146 +
132 // ========== 各种工具方法 ========== 147 // ========== 各种工具方法 ==========
133 148
134 private PayException buildPayException(Throwable ex) { 149 private PayException buildPayException(Throwable ex) {
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
@@ -29,7 +29,6 @@ import java.util.Objects; @@ -29,7 +29,6 @@ import java.util.Objects;
29 import java.util.function.Supplier; 29 import java.util.function.Supplier;
30 30
31 import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER; 31 import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER;
32 -import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;  
33 32
34 /** 33 /**
35 * 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款) 34 * 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款)
@@ -94,7 +93,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa @@ -94,7 +93,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa
94 * @return 退款请求 Response 93 * @return 退款请求 Response
95 */ 94 */
96 @Override 95 @Override
97 - protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { 96 + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws AlipayApiException {
98 // 1.1 构建 AlipayTradeRefundModel 请求 97 // 1.1 构建 AlipayTradeRefundModel 请求
99 AlipayTradeRefundModel model = new AlipayTradeRefundModel(); 98 AlipayTradeRefundModel model = new AlipayTradeRefundModel();
100 model.setOutTradeNo(reqDTO.getOutTradeNo()); 99 model.setOutTradeNo(reqDTO.getOutTradeNo());
@@ -104,31 +103,22 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa @@ -104,31 +103,22 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa
104 // 1.2 构建 AlipayTradePayRequest 请求 103 // 1.2 构建 AlipayTradePayRequest 请求
105 AlipayTradeRefundRequest request = new AlipayTradeRefundRequest(); 104 AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
106 request.setBizModel(model); 105 request.setBizModel(model);
107 - try {  
108 - // 2.1 执行请求  
109 - AlipayTradeRefundResponse response = client.execute(request);  
110 - // 2.2 创建返回结果  
111 - PayRefundRespDTO refund = new PayRefundRespDTO()  
112 - .setOutRefundNo(reqDTO.getOutRefundNo())  
113 - .setRawData(response);  
114 - // 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。  
115 - // 另外,支付宝没有退款单号,所以不用设置  
116 - if (response.isSuccess()) {  
117 - refund.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())  
118 - .setSuccessTime(LocalDateTimeUtil.of(response.getGmtRefundPay()));  
119 - Assert.notNull(refund.getSuccessTime(), "退款成功时间不能为空");  
120 - } else {  
121 - refund.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus());  
122 - }  
123 - return refund;  
124 - } catch (AlipayApiException e) {  
125 - log.error("[doUnifiedRefund][request({}) 发起退款异常]", toJsonString(reqDTO), e);  
126 - return null; 106 +
  107 + // 2.1 执行请求
  108 + AlipayTradeRefundResponse response = client.execute(request);
  109 + // 2.2 创建返回结果
  110 + // 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。
  111 + // 另外,支付宝没有退款单号,所以不用设置
  112 + if (response.isSuccess()) {
  113 + return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()),
  114 + reqDTO.getOutRefundNo(), response);
  115 + } else {
  116 + return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
127 } 117 }
128 } 118 }
129 119
130 @Override 120 @Override
131 - public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) { 121 + public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) {
132 // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。 122 // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
133 // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调 123 // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
134 // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有 124 // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
@@ -12,7 +12,6 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; @@ -12,7 +12,6 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
12 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; 12 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
13 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; 13 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
14 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; 14 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
15 -import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;  
16 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; 15 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
17 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result; 16 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
18 import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult; 17 import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
@@ -163,9 +162,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -163,9 +162,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
163 throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); 162 throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
164 } 163 }
165 } catch (WxPayException e) { 164 } catch (WxPayException e) {
166 - // todo 芋艿:异常的处理;  
167 -// throw buildUnifiedOrderException(null, e);  
168 - return null; 165 + String errorCode = getErrorCode(e);
  166 + String errorMessage = getErrorMessage(e);
  167 + return PayRefundRespDTO.failureOf(errorCode, errorMessage,
  168 + reqDTO.getOutTradeNo(), e.getXmlString());
169 } 169 }
170 } 170 }
171 171
@@ -181,17 +181,11 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -181,17 +181,11 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
181 // 2.1 执行请求 181 // 2.1 执行请求
182 WxPayRefundResult response = client.refundV2(request); 182 WxPayRefundResult response = client.refundV2(request);
183 // 2.2 创建返回结果 183 // 2.2 创建返回结果
184 - PayRefundRespDTO refund = new PayRefundRespDTO()  
185 - .setOutRefundNo(reqDTO.getOutRefundNo())  
186 - .setRawData(response);  
187 if (Objects.equals("SUCCESS", response.getResultCode())) { 184 if (Objects.equals("SUCCESS", response.getResultCode())) {
188 - refund.setStatus(PayRefundStatusRespEnum.WAITING.getStatus())  
189 - .setChannelRefundNo(response.getRefundId());  
190 - } else {  
191 - refund.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()); 185 + return PayRefundRespDTO.waitingOf(response.getRefundId(),
  186 + reqDTO.getOutRefundNo(), response);
192 } 187 }
193 - // TODO 芋艿;异常的处理;  
194 - return refund; 188 + return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
195 } 189 }
196 190
197 private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable { 191 private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
@@ -206,78 +200,51 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -206,78 +200,51 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
206 // 2.1 执行请求 200 // 2.1 执行请求
207 WxPayRefundV3Result response = client.refundV3(request); 201 WxPayRefundV3Result response = client.refundV3(request);
208 // 2.2 创建返回结果 202 // 2.2 创建返回结果
209 - PayRefundRespDTO refund = new PayRefundRespDTO()  
210 - .setOutRefundNo(reqDTO.getOutRefundNo())  
211 - .setRawData(response);  
212 if (Objects.equals("SUCCESS", response.getStatus())) { 203 if (Objects.equals("SUCCESS", response.getStatus())) {
213 - refund.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())  
214 - .setChannelRefundNo(response.getRefundId())  
215 - .setSuccessTime(parseDateV3(response.getSuccessTime()));  
216 - } else if (Objects.equals("PROCESSING", response.getStatus())) {  
217 - refund.setStatus(PayRefundStatusRespEnum.WAITING.getStatus())  
218 - .setChannelRefundNo(response.getRefundId());  
219 - } else {  
220 - refund.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()); 204 + return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()),
  205 + reqDTO.getOutRefundNo(), response);
221 } 206 }
222 - // TODO 芋艿;异常的处理;  
223 - return refund; 207 + if (Objects.equals("PROCESSING", response.getStatus())) {
  208 + return PayRefundRespDTO.waitingOf(response.getRefundId(),
  209 + reqDTO.getOutRefundNo(), response);
  210 + }
  211 + return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response);
224 } 212 }
225 213
226 @Override 214 @Override
227 - public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {  
228 - try {  
229 - // 微信支付 v2 回调结果处理  
230 - switch (config.getApiVersion()) {  
231 - case API_VERSION_V2:  
232 - return parseRefundNotifyV2(body);  
233 - case WxPayClientConfig.API_VERSION_V3:  
234 - return parseRefundNotifyV3(body);  
235 - default:  
236 - throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));  
237 - }  
238 - } catch (WxPayException e) {  
239 - log.error("[parseNotify][params({}) body({}) 解析失败]", params, body, e);  
240 - throw new RuntimeException(e);  
241 - // TODO 芋艿:缺一个异常翻译 215 + public PayRefundRespDTO doParseRefundNotify(Map<String, String> params, String body) throws WxPayException {
  216 + switch (config.getApiVersion()) {
  217 + case API_VERSION_V2:
  218 + return doParseRefundNotifyV2(body);
  219 + case WxPayClientConfig.API_VERSION_V3:
  220 + return parseRefundNotifyV3(body);
  221 + default:
  222 + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
242 } 223 }
243 } 224 }
244 225
245 - @SuppressWarnings("DuplicatedCode")  
246 - private PayRefundRespDTO parseRefundNotifyV2(String body) throws WxPayException { 226 + private PayRefundRespDTO doParseRefundNotifyV2(String body) throws WxPayException {
247 // 1. 解析回调 227 // 1. 解析回调
248 WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body); 228 WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body);
249 - WxPayRefundNotifyResult.ReqInfo responseResult = response.getReqInfo(); 229 + WxPayRefundNotifyResult.ReqInfo result = response.getReqInfo();
250 // 2. 构建结果 230 // 2. 构建结果
251 - PayRefundRespDTO notify = new PayRefundRespDTO()  
252 - .setChannelRefundNo(responseResult.getRefundId())  
253 - .setOutRefundNo(responseResult.getOutRefundNo())  
254 - .setRawData(response);  
255 - if (Objects.equals("SUCCESS", responseResult.getRefundStatus())) {  
256 - notify.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())  
257 - .setSuccessTime(parseDateV2B(responseResult.getSuccessTime()));  
258 - } else {  
259 - notify.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()); 231 + if (Objects.equals("SUCCESS", result.getRefundStatus())) {
  232 + return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV2B(result.getSuccessTime()),
  233 + result.getOutRefundNo(), response);
260 } 234 }
261 - return notify; 235 + return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
262 } 236 }
263 237
264 - @SuppressWarnings("DuplicatedCode")  
265 private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException { 238 private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException {
266 // 1. 解析回调 239 // 1. 解析回调
267 WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null); 240 WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null);
268 - WxPayRefundNotifyV3Result.DecryptNotifyResult responseResult = response.getResult(); 241 + WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult();
269 // 2. 构建结果 242 // 2. 构建结果
270 - PayRefundRespDTO notify = new PayRefundRespDTO()  
271 - .setChannelRefundNo(responseResult.getRefundId())  
272 - .setOutRefundNo(responseResult.getOutRefundNo())  
273 - .setRawData(response);  
274 - if (Objects.equals("SUCCESS", responseResult.getRefundStatus())) {  
275 - notify.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())  
276 - .setSuccessTime(parseDateV3(responseResult.getSuccessTime()));  
277 - } else {  
278 - notify.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()); 243 + if (Objects.equals("SUCCESS", result.getRefundStatus())) {
  244 + return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV3(result.getSuccessTime()),
  245 + result.getOutRefundNo(), response);
279 } 246 }
280 - return notify; 247 + return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response);
281 } 248 }
282 249
283 // ========== 各种工具方法 ========== 250 // ========== 各种工具方法 ==========