Commit 68a4ef98ca52361dc31ff861a2eb27903b31b72f

Authored by YunaiV
1 parent 66ed61c6

mall + pay:

1. 拆分支付回调、退款回调的 URL
2. 修复微信支付回调的时间解析错误
Showing 14 changed files with 166 additions and 76 deletions
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java
@@ -13,14 +13,25 @@ import javax.validation.constraints.NotEmpty; @@ -13,14 +13,25 @@ import javax.validation.constraints.NotEmpty;
13 public class PayProperties { 13 public class PayProperties {
14 14
15 /** 15 /**
16 - * 回调地址 16 + * 支付回调地址
17 * 17 *
18 - * 实际上,对应的 PayNotifyController 的 notifyCallback 方法的 URL 18 + * 实际上,对应的 PayNotifyController 的 notifyOrder 方法的 URL
19 * 19 *
20 - * 注意,支付渠道统一回调到 payNotifyUrl 地址,由支付模块统一处理;然后,自己的支付模块,在回调 PayAppDO.payNotifyUrl 地址 20 + * 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 orderNotifyUrl 地址 => 业务的 PayAppDO.orderNotifyUrl 地址
21 */ 21 */
22 - @NotEmpty(message = "回调地址不能为空")  
23 - @URL(message = "回调地址的格式必须是 URL")  
24 - private String callbackUrl; 22 + @NotEmpty(message = "支付回调地址不能为空")
  23 + @URL(message = "支付回调地址的格式必须是 URL")
  24 + private String orderNotifyUrl;
  25 +
  26 + /**
  27 + * 退款回调地址
  28 + *
  29 + * 实际上,对应的 PayNotifyController 的 notifyRefund 方法的 URL
  30 + *
  31 + * 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 refundNotifyUrl 地址 => 业务的 PayAppDO.notifyRefundUrl 地址
  32 + */
  33 + @NotEmpty(message = "支付回调地址不能为空")
  34 + @URL(message = "支付回调地址的格式必须是 URL")
  35 + private String refundNotifyUrl;
25 36
26 } 37 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
@@ -22,6 +22,8 @@ public interface PayClient { @@ -22,6 +22,8 @@ public interface PayClient {
22 */ 22 */
23 Long getId(); 23 Long getId();
24 24
  25 + // ============ 支付相关 ==========
  26 +
25 /** 27 /**
26 * 调用支付渠道,统一下单 28 * 调用支付渠道,统一下单
27 * 29 *
@@ -31,6 +33,17 @@ public interface PayClient { @@ -31,6 +33,17 @@ public interface PayClient {
31 PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO); 33 PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
32 34
33 /** 35 /**
  36 + * 解析 order 回调数据
  37 + *
  38 + * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
  39 + * @param body HTTP 回调接口的 request body
  40 + * @return 支付订单信息
  41 + */
  42 + PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body);
  43 +
  44 + // ============ 退款相关 ==========
  45 +
  46 + /**
34 * 调用支付渠道,进行退款 47 * 调用支付渠道,进行退款
35 * 48 *
36 * @param reqDTO 统一退款请求信息 49 * @param reqDTO 统一退款请求信息
@@ -39,16 +52,12 @@ public interface PayClient { @@ -39,16 +52,12 @@ public interface PayClient {
39 PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO); 52 PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
40 53
41 /** 54 /**
42 - * 解析回调数据 55 + * 解析 refund 回调数据
43 * 56 *
44 * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 57 * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
45 * @param body HTTP 回调接口的 request body 58 * @param body HTTP 回调接口的 request body
46 - * @return 回调对象  
47 - * 1. {@link PayRefundRespDTO} 退款通知  
48 - * 2. {@link PayOrderRespDTO} 支付通知 59 + * @return 支付订单信息
49 */ 60 */
50 - default Object parseNotify(Map<String, String> params, String body) {  
51 - throw new UnsupportedOperationException("未实现 parseNotify 方法!");  
52 - } 61 + PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body);
53 62
54 } 63 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
@@ -97,13 +97,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa @@ -97,13 +97,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa
97 97
98 @Override 98 @Override
99 @SneakyThrows 99 @SneakyThrows
100 - public Object parseNotify(Map<String, String> params, String body) {  
101 - // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。  
102 - // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调  
103 - // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有  
104 - // 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。  
105 - // 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。  
106 - 100 + public PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
107 // 1. 校验回调数据 101 // 1. 校验回调数据
108 Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8); 102 Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
109 AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(), 103 AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
@@ -127,6 +121,16 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa @@ -127,6 +121,16 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa
127 .build(); 121 .build();
128 } 122 }
129 123
  124 + @Override
  125 + public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
  126 + // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
  127 + // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
  128 + // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
  129 + // 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。
  130 + // 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。
  131 + throw new UnsupportedOperationException("支付宝无退款回调");
  132 + }
  133 +
130 // ========== 各种工具方法 ========== 134 // ========== 各种工具方法 ==========
131 135
132 protected String formatAmount(Integer amount) { 136 protected String formatAmount(Integer amount) {
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
@@ -14,9 +14,11 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; @@ -14,9 +14,11 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
14 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; 14 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
15 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; 15 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
16 import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants; 16 import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
  17 +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
17 import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; 18 import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
18 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; 19 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
19 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result; 20 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
  21 +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
20 import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest; 22 import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
21 import com.github.binarywang.wxpay.bean.result.WxPayRefundResult; 23 import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
22 import com.github.binarywang.wxpay.config.WxPayConfig; 24 import com.github.binarywang.wxpay.config.WxPayConfig;
@@ -30,8 +32,7 @@ import java.time.ZoneId; @@ -30,8 +32,7 @@ import java.time.ZoneId;
30 import java.util.Map; 32 import java.util.Map;
31 import java.util.Objects; 33 import java.util.Objects;
32 34
33 -import static cn.hutool.core.date.DatePattern.PURE_DATETIME_PATTERN;  
34 -import static cn.hutool.core.date.DatePattern.UTC_WITH_XXX_OFFSET_PATTERN; 35 +import static cn.hutool.core.date.DatePattern.*;
35 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 36 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
36 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; 37 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
37 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; 38 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
@@ -61,9 +62,6 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -61,9 +62,6 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
61 WxPayConfig payConfig = new WxPayConfig(); 62 WxPayConfig payConfig = new WxPayConfig();
62 BeanUtil.copyProperties(config, payConfig, "keyContent"); 63 BeanUtil.copyProperties(config, payConfig, "keyContent");
63 payConfig.setTradeType(tradeType); 64 payConfig.setTradeType(tradeType);
64 -// if (WxPayClientConfig.API_VERSION_V2.equals(config.getApiVersion())) {  
65 -// payConfig.setSignType(WxPayConstants.SignType.MD5);  
66 -// }  
67 // weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决 65 // weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
68 if (Base64.isBase64(config.getKeyContent())) { 66 if (Base64.isBase64(config.getKeyContent())) {
69 payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath()); 67 payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
@@ -162,8 +160,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -162,8 +160,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
162 } 160 }
163 161
164 @Override 162 @Override
165 - public Object parseNotify(Map<String, String> params, String body) {  
166 - log.info("[parseNotify][微信支付回调 data 数据: {}]", body); 163 + public PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body) {
167 try { 164 try {
168 // 微信支付 v2 回调结果处理 165 // 微信支付 v2 回调结果处理
169 switch (config.getApiVersion()) { 166 switch (config.getApiVersion()) {
@@ -183,21 +180,24 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -183,21 +180,24 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
183 } 180 }
184 181
185 private PayOrderRespDTO parseOrderNotifyV2(String body) throws WxPayException { 182 private PayOrderRespDTO parseOrderNotifyV2(String body) throws WxPayException {
186 - WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(body);  
187 - Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");  
188 - // 转换结果  
189 - return PayOrderRespDTO  
190 - .builder()  
191 - .outTradeNo(notifyResult.getOutTradeNo())  
192 - .channelOrderNo(notifyResult.getTransactionId())  
193 - .channelUserId(notifyResult.getOpenid())  
194 - .successTime(parseDateV2(notifyResult.getTimeEnd())) 183 + // 1. 解析回调
  184 + WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body);
  185 + // 2. 构建结果
  186 + return PayOrderRespDTO.builder()
  187 + .outTradeNo(response.getOutTradeNo())
  188 + .channelOrderNo(response.getTransactionId())
  189 + .channelUserId(response.getOpenid())
  190 + .status(Objects.equals(response.getResultCode(), "SUCCESS") ?
  191 + PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus())
  192 + .successTime(parseDateV2(response.getTimeEnd()))
  193 + .rawData(response)
195 .build(); 194 .build();
196 } 195 }
197 196
198 private PayOrderRespDTO parseOrderNotifyV3(String body) throws WxPayException { 197 private PayOrderRespDTO parseOrderNotifyV3(String body) throws WxPayException {
199 WxPayOrderNotifyV3Result notifyResult = client.parseOrderNotifyV3Result(body, null); 198 WxPayOrderNotifyV3Result notifyResult = client.parseOrderNotifyV3Result(body, null);
200 WxPayOrderNotifyV3Result.DecryptNotifyResult result = notifyResult.getResult(); 199 WxPayOrderNotifyV3Result.DecryptNotifyResult result = notifyResult.getResult();
  200 + // TODO 芋艿:翻译下 state
201 // 转换结果 201 // 转换结果
202 Assert.isTrue(Objects.equals(notifyResult.getResult().getTradeState(), "SUCCESS"), 202 Assert.isTrue(Objects.equals(notifyResult.getResult().getTradeState(), "SUCCESS"),
203 "支付结果非 SUCCESS"); 203 "支付结果非 SUCCESS");
@@ -209,6 +209,49 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -209,6 +209,49 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
209 .build(); 209 .build();
210 } 210 }
211 211
  212 + @Override
  213 + public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
  214 + try {
  215 + // 微信支付 v2 回调结果处理
  216 + switch (config.getApiVersion()) {
  217 + case API_VERSION_V2:
  218 + return parseRefundNotifyV2(body);
  219 + case WxPayClientConfig.API_VERSION_V3:
  220 + return parseRefundNotifyV3(body);
  221 + default:
  222 + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
  223 + }
  224 + } catch (WxPayException e) {
  225 + log.error("[parseNotify][params({}) body({}) 解析失败]", params, body, e);
  226 +// throw buildPayException(e);
  227 + throw new RuntimeException(e);
  228 + // TODO 芋艿:缺一个异常翻译
  229 + }
  230 + }
  231 +
  232 + private PayRefundRespDTO parseRefundNotifyV2(String body) throws WxPayException {
  233 + // 1. 解析回调
  234 + WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body);
  235 + WxPayRefundNotifyResult.ReqInfo reqInfo = response.getReqInfo();
  236 + // 2. 构建结果
  237 + PayRefundRespDTO notify = new PayRefundRespDTO()
  238 + .setChannelRefundNo(reqInfo.getRefundId())
  239 + .setOutRefundNo(reqInfo.getOutRefundNo())
  240 + .setRawData(response);
  241 + if (Objects.equals("SUCCESS", reqInfo.getRefundStatus())) {
  242 + notify.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())
  243 + .setSuccessTime(parseDateV2B(reqInfo.getSuccessTime()));
  244 + } else {
  245 + notify.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
  246 + }
  247 + return notify;
  248 + }
  249 +
  250 + private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException {
  251 + // TODO 芋艿:未实现
  252 + return null;
  253 + }
  254 +
212 // ========== 各种工具方法 ========== 255 // ========== 各种工具方法 ==========
213 256
214 /** 257 /**
@@ -246,6 +289,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC @@ -246,6 +289,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
246 return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN); 289 return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN);
247 } 290 }
248 291
  292 + static LocalDateTime parseDateV2B(String time) {
  293 + return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN);
  294 + }
  295 +
249 static String formatDateV3(LocalDateTime time) { 296 static String formatDateV3(LocalDateTime time) {
250 return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN); 297 return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN);
251 } 298 }
yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java
@@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.weixin; @@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
2 2
3 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; 3 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
4 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 4 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  5 +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
5 import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest; 6 import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
6 import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest; 7 import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
7 import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request; 8 import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
@@ -9,7 +10,6 @@ import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult; @@ -9,7 +10,6 @@ import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult;
9 import com.github.binarywang.wxpay.bean.result.WxPayRefundResult; 10 import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
10 import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result; 11 import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
11 import com.github.binarywang.wxpay.config.WxPayConfig; 12 import com.github.binarywang.wxpay.config.WxPayConfig;
12 -import com.github.binarywang.wxpay.constant.WxPayConstants;  
13 import com.github.binarywang.wxpay.exception.WxPayException; 13 import com.github.binarywang.wxpay.exception.WxPayException;
14 import com.github.binarywang.wxpay.service.WxPayService; 14 import com.github.binarywang.wxpay.service.WxPayService;
15 import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; 15 import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
@@ -54,6 +54,20 @@ public class WxBarPayClientIntegrationTest { @@ -54,6 +54,20 @@ public class WxBarPayClientIntegrationTest {
54 } 54 }
55 55
56 @Test 56 @Test
  57 + public void testParseRefundNotifyV2() throws WxPayException {
  58 + // 创建 config 配置
  59 + WxPayConfig config = buildWxPayConfigV2();
  60 + // 创建 WxPayService 客户端
  61 + WxPayService client = new WxPayServiceImpl();
  62 + client.setConfig(config);
  63 +
  64 + // 执行解析
  65 + String xml = "<xml><return_code>SUCCESS</return_code><appid><![CDATA[wx62056c0d5e8db250]]></appid><mch_id><![CDATA[1545083881]]></mch_id><nonce_str><![CDATA[ed8f02c21d15635cede114a42d0525a0]]></nonce_str><req_info><![CDATA[bGp+wB9DAHjoOO9Nw1iSmmIFdN2zZDhsoRWZBYdf/8bcpjowr4T8i2qjLsbMtvKQeVC5kBZOL/Agal3be6UPwnoantil+L+ojZgvLch7dXFKs/AcoxIYcVYyGka+wmnRJfUmuFRBgzt++8HOFsmJz6e2brYv1EAz+93fP2AsJtRuw1FEzodcg8eXm52hbE0KhLNqC2OyNVkn8AbOOrwIxSYobg2jVbuJ4JllYbEGIQ/6kWzNbVmMKhGJGYBy/NbUGKoQsoe4QeTQqcqQqVp08muxaOfJGThaN3B9EEMFSrog/3yT7ykVV6WQ5+Ygt89LplOf5ucWa4Ird7VJhHWtzI92ZePj4Omy1XkT1TRlwtDegA0S5MeQpM4WZ1taMrhxgmNkTUJ0JXFncx5e2KLQvbvD/HOcccx48Xv1c16JBz6G3501k8E++LWXgZ2TeNXwGsk6FyRZb0ApLyQHIx5ZtPo/UET9z3AmJCPXkrUsZ4WK46fDtbzxVPU2r8nTOcGCPbO0LUsGT6wpsuQVC4CisXDJwoZmL6kKwHfKs6mmUL2YZYzNfgoB/KgpJYSpC96kcpQyFvw+xuwqK2SXGZbAl9lADT+a83z04feQHSSIG3PCrX4QEWzpCZZ4+ySEz1Y34aoU20X9GtX+1LSwUjmQgwHrMBSvFm3/B7+IFM8OUqDB+Uvkr9Uvy7P2/KDvfy3Ih7GFcGd0C5NXpSvVTTfu1IlK/T3/t6MR/8iq78pp/2ZTYvO6eNDRJWaXYU+x6sl2dTs9n+2Z4W4AfYTvEyuxlx+aI19SqCJh7WmaFcAxidFl/9iqDjWiplb9+C6ijZv2hJtVjSCuoptIWpGDYItH7RAqlKHrx6flJD+M/5BceMHBv2w4OWCD9vPRLo8gl9o06ip0iflzO1dixhOAgLFjsQmQHNGFtR3EvCID+iS4FUlilwK+hcKNxrr0wp9Btkl9W1R9aTo289CUiIxx45skfCYzHwb+7Hqj3uTiXnep6zhCKZBAnPsDOvISXfBgXKufcFsTNtts09jX8H5/uMc9wyJ179H1cp+At1mIK2duwfo4Q9asfEoffl6Zn1olGdtEruxHGeVU0NwJ8V7RflC/Cx5RXtJ3sPJ/sHmVnBlVyR0=]]></req_info></xml>";
  66 + WxPayRefundNotifyResult response = client.parseRefundNotifyResult(xml);
  67 + System.out.println(response.getReqInfo());
  68 + }
  69 +
  70 + @Test
57 public void testRefundV2() throws WxPayException { 71 public void testRefundV2() throws WxPayException {
58 // 创建 config 配置 72 // 创建 config 配置
59 WxPayConfig config = buildWxPayConfigV2(); 73 WxPayConfig config = buildWxPayConfigV2();
@@ -101,7 +115,7 @@ public class WxBarPayClientIntegrationTest { @@ -101,7 +115,7 @@ public class WxBarPayClientIntegrationTest {
101 config.setAppId("wx62056c0d5e8db250"); 115 config.setAppId("wx62056c0d5e8db250");
102 config.setMchId("1545083881"); 116 config.setMchId("1545083881");
103 config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim"); 117 config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim");
104 - config.setSignType(WxPayConstants.SignType.MD5); 118 +// config.setSignType(WxPayConstants.SignType.MD5);
105 config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12"); 119 config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12");
106 return config; 120 return config;
107 } 121 }
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java
@@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.pay.controller.admin.notify; @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.pay.controller.admin.notify;
3 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; 3 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
4 import cn.iocoder.yudao.framework.pay.core.client.PayClient; 4 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
5 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; 5 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
6 -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;  
7 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; 6 import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
  7 +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
8 import cn.iocoder.yudao.module.pay.service.order.PayOrderService; 8 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
9 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService; 9 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
10 import io.swagger.v3.oas.annotations.Operation; 10 import io.swagger.v3.oas.annotations.Operation;
@@ -18,7 +18,6 @@ import javax.annotation.security.PermitAll; @@ -18,7 +18,6 @@ import javax.annotation.security.PermitAll;
18 import java.util.Map; 18 import java.util.Map;
19 19
20 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 20 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
21 -import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;  
22 import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND; 21 import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND;
23 22
24 @Tag(name = "管理后台 - 支付通知") 23 @Tag(name = "管理后台 - 支付通知")
@@ -36,22 +35,14 @@ public class PayNotifyController { @@ -36,22 +35,14 @@ public class PayNotifyController {
36 @Resource 35 @Resource
37 private PayClientFactory payClientFactory; 36 private PayClientFactory payClientFactory;
38 37
39 - /**  
40 - * 统一的渠道支付回调,支付宝的退款回调  
41 - *  
42 - * @param channelId 渠道编号  
43 - * @param params form 参数  
44 - * @param body request body  
45 - * @return 成功返回 "success"  
46 - */  
47 - @PostMapping(value = "/callback/{channelId}")  
48 - @Operation(summary = "支付渠道的统一回调接口 - 包括支付回调,退款回调") 38 + @PostMapping(value = "/order/{channelId}")
  39 + @Operation(summary = "支付渠道的统一【支付】回调")
49 @PermitAll 40 @PermitAll
50 @OperateLog(enable = false) // 回调地址,无需记录操作日志 41 @OperateLog(enable = false) // 回调地址,无需记录操作日志
51 - public String notifyCallback(@PathVariable("channelId") Long channelId,  
52 - @RequestParam(required = false) Map<String, String> params,  
53 - @RequestBody(required = false) String body) {  
54 - log.info("[notifyCallback][channelId({}) 回调数据({}/{})]", channelId, params, body); 42 + public String notifyOrder(@PathVariable("channelId") Long channelId,
  43 + @RequestParam(required = false) Map<String, String> params,
  44 + @RequestBody(required = false) String body) {
  45 + log.info("[notifyOrder][channelId({}) 回调数据({}/{})]", channelId, params, body);
55 // 1. 校验支付渠道是否存在 46 // 1. 校验支付渠道是否存在
56 PayClient payClient = payClientFactory.getPayClient(channelId); 47 PayClient payClient = payClientFactory.getPayClient(channelId);
57 if (payClient == null) { 48 if (payClient == null) {
@@ -60,20 +51,30 @@ public class PayNotifyController { @@ -60,20 +51,30 @@ public class PayNotifyController {
60 } 51 }
61 52
62 // 2. 解析通知数据 53 // 2. 解析通知数据
63 - Object notify = payClient.parseNotify(params, body); 54 + PayOrderRespDTO notify = payClient.parseOrderNotify(params, body);
  55 + orderService.notifyOrder(channelId, notify);
  56 + return "success";
  57 + }
64 58
65 - // 3. 处理通知  
66 - // 3.1:退款通知  
67 - if (notify instanceof PayRefundRespDTO) {  
68 - refundService.notifyRefund(channelId, (PayRefundRespDTO) notify);  
69 - return "success";  
70 - }  
71 - // 3.2:支付通知  
72 - if (notify instanceof PayOrderRespDTO) {  
73 - orderService.notifyOrder(channelId, (PayOrderRespDTO) notify);  
74 - return "success"; 59 + @PostMapping(value = "/refund/{channelId}")
  60 + @Operation(summary = "支付渠道的统一【退款】回调")
  61 + @PermitAll
  62 + @OperateLog(enable = false) // 回调地址,无需记录操作日志
  63 + public String notifyRefund(@PathVariable("channelId") Long channelId,
  64 + @RequestParam(required = false) Map<String, String> params,
  65 + @RequestBody(required = false) String body) {
  66 + log.info("[notifyRefund][channelId({}) 回调数据({}/{})]", channelId, params, body);
  67 + // 1. 校验支付渠道是否存在
  68 + PayClient payClient = payClientFactory.getPayClient(channelId);
  69 + if (payClient == null) {
  70 + log.error("[notifyCallback][渠道编号({}) 找不到对应的支付客户端]", channelId);
  71 + throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
75 } 72 }
76 - throw new UnsupportedOperationException("未知通知:" + toJsonString(notify)); 73 +
  74 + // 2. 解析通知数据
  75 + PayRefundRespDTO notify = payClient.parseRefundNotify(params, body);
  76 + refundService.notifyRefund(channelId, notify);
  77 + return "success";
77 } 78 }
78 79
79 } 80 }
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java
@@ -48,7 +48,7 @@ public class PayAppDO extends BaseDO { @@ -48,7 +48,7 @@ public class PayAppDO extends BaseDO {
48 /** 48 /**
49 * 支付结果的回调地址 49 * 支付结果的回调地址
50 */ 50 */
51 - private String payNotifyUrl; 51 + private String orderNotifyUrl;
52 /** 52 /**
53 * 退款结果的回调地址 53 * 退款结果的回调地址
54 */ 54 */
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/notify/PayNotifyJob.java
@@ -20,11 +20,11 @@ import javax.annotation.Resource; @@ -20,11 +20,11 @@ import javax.annotation.Resource;
20 public class PayNotifyJob implements JobHandler { 20 public class PayNotifyJob implements JobHandler {
21 21
22 @Resource 22 @Resource
23 - private PayNotifyService payNotifyCoreService; 23 + private PayNotifyService payNotifyService;
24 24
25 @Override 25 @Override
26 public String execute(String param) throws Exception { 26 public String execute(String param) throws Exception {
27 - int notifyCount = payNotifyCoreService.executeNotify(); 27 + int notifyCount = payNotifyService.executeNotify();
28 return String.format("执行支付通知 %s 个", notifyCount); 28 return String.format("执行支付通知 %s 个", notifyCount);
29 } 29 }
30 30
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java
@@ -125,7 +125,7 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -125,7 +125,7 @@ public class PayOrderServiceImpl implements PayOrderService {
125 // 创建支付交易单 125 // 创建支付交易单
126 order = PayOrderConvert.INSTANCE.convert(reqDTO).setAppId(app.getId()) 126 order = PayOrderConvert.INSTANCE.convert(reqDTO).setAppId(app.getId())
127 // 商户相关字段 127 // 商户相关字段
128 - .setNotifyUrl(app.getPayNotifyUrl()).setNotifyStatus(PayOrderNotifyStatusEnum.NO.getStatus()) 128 + .setNotifyUrl(app.getOrderNotifyUrl()).setNotifyStatus(PayOrderNotifyStatusEnum.NO.getStatus())
129 // 订单相关字段 129 // 订单相关字段
130 .setStatus(PayOrderStatusEnum.WAITING.getStatus()) 130 .setStatus(PayOrderStatusEnum.WAITING.getStatus())
131 // 退款相关字段 131 // 退款相关字段
@@ -206,7 +206,7 @@ public class PayOrderServiceImpl implements PayOrderService { @@ -206,7 +206,7 @@ public class PayOrderServiceImpl implements PayOrderService {
206 * @return 支付渠道的回调地址 配置地址 + "/" + channel id 206 * @return 支付渠道的回调地址 配置地址 + "/" + channel id
207 */ 207 */
208 private String genChannelPayNotifyUrl(PayChannelDO channel) { 208 private String genChannelPayNotifyUrl(PayChannelDO channel) {
209 - return payProperties.getCallbackUrl() + "/" + channel.getId(); 209 + return payProperties.getOrderNotifyUrl() + "/" + channel.getId();
210 } 210 }
211 211
212 private String generateOrderExtensionNo() { 212 private String generateOrderExtensionNo() {
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java
@@ -134,7 +134,7 @@ public class PayRefundServiceImpl implements PayRefundService { @@ -134,7 +134,7 @@ public class PayRefundServiceImpl implements PayRefundService {
134 .setRefundPrice(reqDTO.getPrice()) 134 .setRefundPrice(reqDTO.getPrice())
135 .setOutTradeNo(orderExtension.getNo()) 135 .setOutTradeNo(orderExtension.getNo())
136 .setOutRefundNo(refund.getNo()) 136 .setOutRefundNo(refund.getNo())
137 - .setNotifyUrl(genChannelPayNotifyUrl(channel)) // TODO 芋艿:优化下 notifyUrl 137 + .setNotifyUrl(genChannelPayNotifyUrl(channel))
138 .setReason(reqDTO.getReason()); 138 .setReason(reqDTO.getReason());
139 PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); // TODO 增加一个 channelErrorCode、channelErrorMsg 字段 139 PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); // TODO 增加一个 channelErrorCode、channelErrorMsg 字段
140 // 2.3 处理退款返回 140 // 2.3 处理退款返回
@@ -183,7 +183,7 @@ public class PayRefundServiceImpl implements PayRefundService { @@ -183,7 +183,7 @@ public class PayRefundServiceImpl implements PayRefundService {
183 * @return 支付渠道的回调地址 配置地址 + "/" + channel id 183 * @return 支付渠道的回调地址 配置地址 + "/" + channel id
184 */ 184 */
185 private String genChannelPayNotifyUrl(PayChannelDO channel) { 185 private String genChannelPayNotifyUrl(PayChannelDO channel) {
186 - return payProperties.getCallbackUrl() + "/" + channel.getId(); 186 + return payProperties.getRefundNotifyUrl() + "/" + channel.getId();
187 } 187 }
188 188
189 private String generateRefundNo() { 189 private String generateRefundNo() {
yudao-server/src/main/resources/application-local.yaml
@@ -194,7 +194,8 @@ yudao: @@ -194,7 +194,8 @@ yudao:
194 - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 194 - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求
195 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 195 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
196 pay: 196 pay:
197 - callback-url: http://yunai.natapp1.cc/admin-api/pay/notify/callback 197 + order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
  198 + refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
198 access-log: # 访问日志的配置项 199 access-log: # 访问日志的配置项
199 enable: false 200 enable: false
200 error-code: # 错误码相关配置项 201 error-code: # 错误码相关配置项
yudao-server/src/main/resources/application.yaml
@@ -142,7 +142,7 @@ yudao: @@ -142,7 +142,7 @@ yudao:
142 - /admin-api/system/captcha/check # 校验图片验证码,和租户无关 142 - /admin-api/system/captcha/check # 校验图片验证码,和租户无关
143 - /admin-api/infra/file/*/get/** # 获取图片,和租户无关 143 - /admin-api/infra/file/*/get/** # 获取图片,和租户无关
144 - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号 144 - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号
145 - - /admin-api/pay/notify/callback/* # 支付回调通知,不携带租户编号 145 + - /admin-api/pay/notify/** # 支付回调通知,不携带租户编号
146 - /jmreport/* # 积木报表,无法携带租户编号 146 - /jmreport/* # 积木报表,无法携带租户编号
147 - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号 147 - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号
148 ignore-tables: 148 ignore-tables:
yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue
1 <template> 1 <template>
2 <div> 2 <div>
3 <el-dialog :visible.sync="transferParam.wechatOpen" :title="title" @close="close" append-to-body width="800px"> 3 <el-dialog :visible.sync="transferParam.wechatOpen" :title="title" @close="close" append-to-body width="800px">
4 - <el-form ref="wechatJsApiForm" :model="form" :rules="rules" size="medium" label-width="100px" 4 + <el-form ref="wechatJsApiForm" :model="form" :rules="rules" size="medium" label-width="120px"
5 v-loading="transferParam.loading"> 5 v-loading="transferParam.loading">
6 <el-form-item label-width="180px" label="渠道费率" prop="feeRate"> 6 <el-form-item label-width="180px" label="渠道费率" prop="feeRate">
7 <el-input v-model="form.feeRate" placeholder="请输入渠道费率" clearable :style="{width: '100%'}"> 7 <el-input v-model="form.feeRate" placeholder="请输入渠道费率" clearable :style="{width: '100%'}">
yudao-ui-admin/src/views/pay/cashier/index.vue
@@ -331,6 +331,9 @@ export default { @@ -331,6 +331,9 @@ export default {
331 * ③ close:支付已关闭 331 * ③ close:支付已关闭
332 */ 332 */
333 goReturnUrl(payResult) { 333 goReturnUrl(payResult) {
  334 + // 清理任务
  335 + this.clearQueryInterval();
  336 +
334 // 未配置的情况下,只能关闭 337 // 未配置的情况下,只能关闭
335 if (!this.returnUrl) { 338 if (!this.returnUrl) {
336 this.$tab.closePage(); 339 this.$tab.closePage();