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 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 22 */
23 23 Long getId();
24 24  
  25 + // ============ 支付相关 ==========
  26 +
25 27 /**
26 28 * 调用支付渠道,统一下单
27 29 *
... ... @@ -31,6 +33,17 @@ public interface PayClient {
31 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 49 * @param reqDTO 统一退款请求信息
... ... @@ -39,16 +52,12 @@ public interface PayClient {
39 52 PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
40 53  
41 54 /**
42   - * 解析回调数据
  55 + * 解析 refund 回调数据
43 56 *
44 57 * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
45 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 97  
98 98 @Override
99 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 101 // 1. 校验回调数据
108 102 Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
109 103 AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
... ... @@ -127,6 +121,16 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient&lt;AlipayPa
127 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 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 14 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
15 15 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
16 16 import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
  17 +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
17 18 import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
18 19 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
19 20 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
  21 +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
20 22 import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
21 23 import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
22 24 import com.github.binarywang.wxpay.config.WxPayConfig;
... ... @@ -30,8 +32,7 @@ import java.time.ZoneId;
30 32 import java.util.Map;
31 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 36 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
36 37 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
37 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 62 WxPayConfig payConfig = new WxPayConfig();
62 63 BeanUtil.copyProperties(config, payConfig, "keyContent");
63 64 payConfig.setTradeType(tradeType);
64   -// if (WxPayClientConfig.API_VERSION_V2.equals(config.getApiVersion())) {
65   -// payConfig.setSignType(WxPayConstants.SignType.MD5);
66   -// }
67 65 // weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
68 66 if (Base64.isBase64(config.getKeyContent())) {
69 67 payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
... ... @@ -162,8 +160,7 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
162 160 }
163 161  
164 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 164 try {
168 165 // 微信支付 v2 回调结果处理
169 166 switch (config.getApiVersion()) {
... ... @@ -183,21 +180,24 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
183 180 }
184 181  
185 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 194 .build();
196 195 }
197 196  
198 197 private PayOrderRespDTO parseOrderNotifyV3(String body) throws WxPayException {
199 198 WxPayOrderNotifyV3Result notifyResult = client.parseOrderNotifyV3Result(body, null);
200 199 WxPayOrderNotifyV3Result.DecryptNotifyResult result = notifyResult.getResult();
  200 + // TODO 芋艿:翻译下 state
201 201 // 转换结果
202 202 Assert.isTrue(Objects.equals(notifyResult.getResult().getTradeState(), "SUCCESS"),
203 203 "支付结果非 SUCCESS");
... ... @@ -209,6 +209,49 @@ public abstract class AbstractWxPayClient extends AbstractPayClient&lt;WxPayClientC
209 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 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 296 static String formatDateV3(LocalDateTime time) {
250 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 2  
3 3 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
4 4 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  5 +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
5 6 import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
6 7 import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
7 8 import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
... ... @@ -9,7 +10,6 @@ import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult;
9 10 import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
10 11 import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
11 12 import com.github.binarywang.wxpay.config.WxPayConfig;
12   -import com.github.binarywang.wxpay.constant.WxPayConstants;
13 13 import com.github.binarywang.wxpay.exception.WxPayException;
14 14 import com.github.binarywang.wxpay.service.WxPayService;
15 15 import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
... ... @@ -54,6 +54,20 @@ public class WxBarPayClientIntegrationTest {
54 54 }
55 55  
56 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 71 public void testRefundV2() throws WxPayException {
58 72 // 创建 config 配置
59 73 WxPayConfig config = buildWxPayConfigV2();
... ... @@ -101,7 +115,7 @@ public class WxBarPayClientIntegrationTest {
101 115 config.setAppId("wx62056c0d5e8db250");
102 116 config.setMchId("1545083881");
103 117 config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim");
104   - config.setSignType(WxPayConstants.SignType.MD5);
  118 +// config.setSignType(WxPayConstants.SignType.MD5);
105 119 config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12");
106 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 3 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
4 4 import cn.iocoder.yudao.framework.pay.core.client.PayClient;
5 5 import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
6   -import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
7 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 8 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
9 9 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
10 10 import io.swagger.v3.oas.annotations.Operation;
... ... @@ -18,7 +18,6 @@ import javax.annotation.security.PermitAll;
18 18 import java.util.Map;
19 19  
20 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 21 import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND;
23 22  
24 23 @Tag(name = "管理后台 - 支付通知")
... ... @@ -36,22 +35,14 @@ public class PayNotifyController {
36 35 @Resource
37 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 40 @PermitAll
50 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 46 // 1. 校验支付渠道是否存在
56 47 PayClient payClient = payClientFactory.getPayClient(channelId);
57 48 if (payClient == null) {
... ... @@ -60,20 +51,30 @@ public class PayNotifyController {
60 51 }
61 52  
62 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 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 20 public class PayNotifyJob implements JobHandler {
21 21  
22 22 @Resource
23   - private PayNotifyService payNotifyCoreService;
  23 + private PayNotifyService payNotifyService;
24 24  
25 25 @Override
26 26 public String execute(String param) throws Exception {
27   - int notifyCount = payNotifyCoreService.executeNotify();
  27 + int notifyCount = payNotifyService.executeNotify();
28 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 125 // 创建支付交易单
126 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 130 .setStatus(PayOrderStatusEnum.WAITING.getStatus())
131 131 // 退款相关字段
... ... @@ -206,7 +206,7 @@ public class PayOrderServiceImpl implements PayOrderService {
206 206 * @return 支付渠道的回调地址 配置地址 + "/" + channel id
207 207 */
208 208 private String genChannelPayNotifyUrl(PayChannelDO channel) {
209   - return payProperties.getCallbackUrl() + "/" + channel.getId();
  209 + return payProperties.getOrderNotifyUrl() + "/" + channel.getId();
210 210 }
211 211  
212 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 134 .setRefundPrice(reqDTO.getPrice())
135 135 .setOutTradeNo(orderExtension.getNo())
136 136 .setOutRefundNo(refund.getNo())
137   - .setNotifyUrl(genChannelPayNotifyUrl(channel)) // TODO 芋艿:优化下 notifyUrl
  137 + .setNotifyUrl(genChannelPayNotifyUrl(channel))
138 138 .setReason(reqDTO.getReason());
139 139 PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); // TODO 增加一个 channelErrorCode、channelErrorMsg 字段
140 140 // 2.3 处理退款返回
... ... @@ -183,7 +183,7 @@ public class PayRefundServiceImpl implements PayRefundService {
183 183 * @return 支付渠道的回调地址 配置地址 + "/" + channel id
184 184 */
185 185 private String genChannelPayNotifyUrl(PayChannelDO channel) {
186   - return payProperties.getCallbackUrl() + "/" + channel.getId();
  186 + return payProperties.getRefundNotifyUrl() + "/" + channel.getId();
187 187 }
188 188  
189 189 private String generateRefundNo() {
... ...
yudao-server/src/main/resources/application-local.yaml
... ... @@ -194,7 +194,8 @@ yudao:
194 194 - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求
195 195 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
196 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 199 access-log: # 访问日志的配置项
199 200 enable: false
200 201 error-code: # 错误码相关配置项
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -142,7 +142,7 @@ yudao:
142 142 - /admin-api/system/captcha/check # 校验图片验证码,和租户无关
143 143 - /admin-api/infra/file/*/get/** # 获取图片,和租户无关
144 144 - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号
145   - - /admin-api/pay/notify/callback/* # 支付回调通知,不携带租户编号
  145 + - /admin-api/pay/notify/** # 支付回调通知,不携带租户编号
146 146 - /jmreport/* # 积木报表,无法携带租户编号
147 147 - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号
148 148 ignore-tables:
... ...
yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue
1 1 <template>
2 2 <div>
3 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 5 v-loading="transferParam.loading">
6 6 <el-form-item label-width="180px" label="渠道费率" prop="feeRate">
7 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 331 * ③ close:支付已关闭
332 332 */
333 333 goReturnUrl(payResult) {
  334 + // 清理任务
  335 + this.clearQueryInterval();
  336 +
334 337 // 未配置的情况下,只能关闭
335 338 if (!this.returnUrl) {
336 339 this.$tab.closePage();
... ...