Commit 68a4ef98ca52361dc31ff861a2eb27903b31b72f
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<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<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<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<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<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<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<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
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