Commit 1c282bd3cb7ada650947b6978d6943df392c1e13
1 parent
6f475f8c
mall + pay:
1. 优化 PayClient 退款逻辑,返回业务失败 errorCode + errorMsg 错误码
Showing
4 changed files
with
130 additions
and
90 deletions
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java
| 1 | 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 | 4 | import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; |
| 4 | 5 | import lombok.Data; |
| 5 | 6 | |
| ... | ... | @@ -44,4 +45,71 @@ public class PayRefundRespDTO { |
| 44 | 45 | */ |
| 45 | 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 | 98 | try { |
| 99 | 99 | return doParseOrderNotify(params, body); |
| 100 | 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 | 103 | throw buildPayException(ex); |
| 103 | 104 | } |
| 104 | 105 | } |
| ... | ... | @@ -129,6 +130,20 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen |
| 129 | 130 | |
| 130 | 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 | 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 | 29 | import java.util.function.Supplier; |
| 30 | 30 | |
| 31 | 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<AlipayPa |
| 94 | 93 | * @return 退款请求 Response |
| 95 | 94 | */ |
| 96 | 95 | @Override |
| 97 | - protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { | |
| 96 | + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws AlipayApiException { | |
| 98 | 97 | // 1.1 构建 AlipayTradeRefundModel 请求 |
| 99 | 98 | AlipayTradeRefundModel model = new AlipayTradeRefundModel(); |
| 100 | 99 | model.setOutTradeNo(reqDTO.getOutTradeNo()); |
| ... | ... | @@ -104,31 +103,22 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa |
| 104 | 103 | // 1.2 构建 AlipayTradePayRequest 请求 |
| 105 | 104 | AlipayTradeRefundRequest request = new AlipayTradeRefundRequest(); |
| 106 | 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 | 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 | 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 | 12 | import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; |
| 13 | 13 | import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; |
| 14 | 14 | import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; |
| 15 | -import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; | |
| 16 | 15 | import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; |
| 17 | 16 | import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result; |
| 18 | 17 | import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult; |
| ... | ... | @@ -163,9 +162,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC |
| 163 | 162 | throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); |
| 164 | 163 | } |
| 165 | 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<WxPayClientC |
| 181 | 181 | // 2.1 执行请求 |
| 182 | 182 | WxPayRefundResult response = client.refundV2(request); |
| 183 | 183 | // 2.2 创建返回结果 |
| 184 | - PayRefundRespDTO refund = new PayRefundRespDTO() | |
| 185 | - .setOutRefundNo(reqDTO.getOutRefundNo()) | |
| 186 | - .setRawData(response); | |
| 187 | 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 | 191 | private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable { |
| ... | ... | @@ -206,78 +200,51 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC |
| 206 | 200 | // 2.1 执行请求 |
| 207 | 201 | WxPayRefundV3Result response = client.refundV3(request); |
| 208 | 202 | // 2.2 创建返回结果 |
| 209 | - PayRefundRespDTO refund = new PayRefundRespDTO() | |
| 210 | - .setOutRefundNo(reqDTO.getOutRefundNo()) | |
| 211 | - .setRawData(response); | |
| 212 | 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 | 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 | 227 | // 1. 解析回调 |
| 248 | 228 | WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body); |
| 249 | - WxPayRefundNotifyResult.ReqInfo responseResult = response.getReqInfo(); | |
| 229 | + WxPayRefundNotifyResult.ReqInfo result = response.getReqInfo(); | |
| 250 | 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 | 238 | private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException { |
| 266 | 239 | // 1. 解析回调 |
| 267 | 240 | WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null); |
| 268 | - WxPayRefundNotifyV3Result.DecryptNotifyResult responseResult = response.getResult(); | |
| 241 | + WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult(); | |
| 269 | 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 | // ========== 各种工具方法 ========== | ... | ... |