From 68a4ef98ca52361dc31ff861a2eb27903b31b72f Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 17 Jul 2023 11:47:45 +0800 Subject: [PATCH] =?UTF-8?q?mall=20+=20pay=EF=BC=9A=201.=20=E6=8B=86?= =?UTF-8?q?=E5=88=86=E6=94=AF=E4=BB=98=E5=9B=9E=E8=B0=83=E3=80=81=E9=80=80?= =?UTF-8?q?=E6=AC=BE=E5=9B=9E=E8=B0=83=E7=9A=84=20URL=202.=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=BE=AE=E4=BF=A1=E6=94=AF=E4=BB=98=E5=9B=9E=E8=B0=83?= =?UTF-8?q?=E7=9A=84=E6=97=B6=E9=97=B4=E8=A7=A3=E6=9E=90=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/pay/config/PayProperties.java | 23 ++++-- .../framework/pay/core/client/PayClient.java | 23 ++++-- .../impl/alipay/AbstractAlipayPayClient.java | 18 +++-- .../impl/weixin/AbstractWxPayClient.java | 79 +++++++++++++++---- .../weixin/WxBarPayClientIntegrationTest.java | 18 ++++- .../admin/notify/PayNotifyController.java | 57 ++++++------- .../pay/dal/dataobject/app/PayAppDO.java | 2 +- .../module/pay/job/notify/PayNotifyJob.java | 4 +- .../service/order/PayOrderServiceImpl.java | 4 +- .../service/refund/PayRefundServiceImpl.java | 4 +- .../src/main/resources/application-local.yaml | 3 +- .../src/main/resources/application.yaml | 2 +- .../pay/app/components/wechatChannelForm.vue | 2 +- .../src/views/pay/cashier/index.vue | 3 + 14 files changed, 166 insertions(+), 76 deletions(-) diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java index 4784e926b..dc7380865 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java +++ b/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; public class PayProperties { /** - * 回调地址 + * 支付回调地址 * - * 实际上,对应的 PayNotifyController 的 notifyCallback 方法的 URL + * 实际上,对应的 PayNotifyController 的 notifyOrder 方法的 URL * - * 注意,支付渠道统一回调到 payNotifyUrl 地址,由支付模块统一处理;然后,自己的支付模块,在回调 PayAppDO.payNotifyUrl 地址 + * 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 orderNotifyUrl 地址 => 业务的 PayAppDO.orderNotifyUrl 地址 */ - @NotEmpty(message = "回调地址不能为空") - @URL(message = "回调地址的格式必须是 URL") - private String callbackUrl; + @NotEmpty(message = "支付回调地址不能为空") + @URL(message = "支付回调地址的格式必须是 URL") + private String orderNotifyUrl; + + /** + * 退款回调地址 + * + * 实际上,对应的 PayNotifyController 的 notifyRefund 方法的 URL + * + * 回调顺序:支付渠道(支付宝支付、微信支付) => yudao-module-pay 的 refundNotifyUrl 地址 => 业务的 PayAppDO.notifyRefundUrl 地址 + */ + @NotEmpty(message = "支付回调地址不能为空") + @URL(message = "支付回调地址的格式必须是 URL") + private String refundNotifyUrl; } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java index 0d0139000..ee5d11832 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java +++ b/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 { */ Long getId(); + // ============ 支付相关 ========== + /** * 调用支付渠道,统一下单 * @@ -30,6 +32,17 @@ public interface PayClient { */ PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO); + /** + * 解析 order 回调数据 + * + * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 + * @param body HTTP 回调接口的 request body + * @return 支付订单信息 + */ + PayOrderRespDTO parseOrderNotify(Map params, String body); + + // ============ 退款相关 ========== + /** * 调用支付渠道,进行退款 * @@ -39,16 +52,12 @@ public interface PayClient { PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO); /** - * 解析回调数据 + * 解析 refund 回调数据 * * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 * @param body HTTP 回调接口的 request body - * @return 回调对象 - * 1. {@link PayRefundRespDTO} 退款通知 - * 2. {@link PayOrderRespDTO} 支付通知 + * @return 支付订单信息 */ - default Object parseNotify(Map params, String body) { - throw new UnsupportedOperationException("未实现 parseNotify 方法!"); - } + PayRefundRespDTO parseRefundNotify(Map params, String body); } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java index a252e6dc2..6bfb5c886 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java +++ b/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 params, String body) { - // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。 - // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调 - // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有 - // 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。 - // 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。 - + public PayOrderRespDTO parseOrderNotify(Map params, String body) { // 1. 校验回调数据 Map bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8); AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(), @@ -127,6 +121,16 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient params, String body) { + // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。 + // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调 + // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有 + // 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。 + // 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。 + throw new UnsupportedOperationException("支付宝无退款回调"); + } + // ========== 各种工具方法 ========== protected String formatAmount(Integer amount) { diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java index 01ae466c2..98e826c72 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java +++ b/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; import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient; import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum; import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result; +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult; import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest; import com.github.binarywang.wxpay.bean.result.WxPayRefundResult; import com.github.binarywang.wxpay.config.WxPayConfig; @@ -30,8 +32,7 @@ import java.time.ZoneId; import java.util.Map; import java.util.Objects; -import static cn.hutool.core.date.DatePattern.PURE_DATETIME_PATTERN; -import static cn.hutool.core.date.DatePattern.UTC_WITH_XXX_OFFSET_PATTERN; +import static cn.hutool.core.date.DatePattern.*; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; @@ -61,9 +62,6 @@ public abstract class AbstractWxPayClient extends AbstractPayClient params, String body) { - log.info("[parseNotify][微信支付回调 data 数据: {}]", body); + public PayOrderRespDTO parseOrderNotify(Map params, String body) { try { // 微信支付 v2 回调结果处理 switch (config.getApiVersion()) { @@ -183,21 +180,24 @@ public abstract class AbstractWxPayClient extends AbstractPayClient params, String body) { + try { + // 微信支付 v2 回调结果处理 + switch (config.getApiVersion()) { + case API_VERSION_V2: + return parseRefundNotifyV2(body); + case WxPayClientConfig.API_VERSION_V3: + return parseRefundNotifyV3(body); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + log.error("[parseNotify][params({}) body({}) 解析失败]", params, body, e); +// throw buildPayException(e); + throw new RuntimeException(e); + // TODO 芋艿:缺一个异常翻译 + } + } + + private PayRefundRespDTO parseRefundNotifyV2(String body) throws WxPayException { + // 1. 解析回调 + WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body); + WxPayRefundNotifyResult.ReqInfo reqInfo = response.getReqInfo(); + // 2. 构建结果 + PayRefundRespDTO notify = new PayRefundRespDTO() + .setChannelRefundNo(reqInfo.getRefundId()) + .setOutRefundNo(reqInfo.getOutRefundNo()) + .setRawData(response); + if (Objects.equals("SUCCESS", reqInfo.getRefundStatus())) { + notify.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()) + .setSuccessTime(parseDateV2B(reqInfo.getSuccessTime())); + } else { + notify.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()); + } + return notify; + } + + private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException { + // TODO 芋艿:未实现 + return null; + } + // ========== 各种工具方法 ========== /** @@ -246,6 +289,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient params, - @RequestBody(required = false) String body) { - log.info("[notifyCallback][channelId({}) 回调数据({}/{})]", channelId, params, body); + public String notifyOrder(@PathVariable("channelId") Long channelId, + @RequestParam(required = false) Map params, + @RequestBody(required = false) String body) { + log.info("[notifyOrder][channelId({}) 回调数据({}/{})]", channelId, params, body); // 1. 校验支付渠道是否存在 PayClient payClient = payClientFactory.getPayClient(channelId); if (payClient == null) { @@ -60,20 +51,30 @@ public class PayNotifyController { } // 2. 解析通知数据 - Object notify = payClient.parseNotify(params, body); + PayOrderRespDTO notify = payClient.parseOrderNotify(params, body); + orderService.notifyOrder(channelId, notify); + return "success"; + } - // 3. 处理通知 - // 3.1:退款通知 - if (notify instanceof PayRefundRespDTO) { - refundService.notifyRefund(channelId, (PayRefundRespDTO) notify); - return "success"; + @PostMapping(value = "/refund/{channelId}") + @Operation(summary = "支付渠道的统一【退款】回调") + @PermitAll + @OperateLog(enable = false) // 回调地址,无需记录操作日志 + public String notifyRefund(@PathVariable("channelId") Long channelId, + @RequestParam(required = false) Map params, + @RequestBody(required = false) String body) { + log.info("[notifyRefund][channelId({}) 回调数据({}/{})]", channelId, params, body); + // 1. 校验支付渠道是否存在 + PayClient payClient = payClientFactory.getPayClient(channelId); + if (payClient == null) { + log.error("[notifyCallback][渠道编号({}) 找不到对应的支付客户端]", channelId); + throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND); } - // 3.2:支付通知 - if (notify instanceof PayOrderRespDTO) { - orderService.notifyOrder(channelId, (PayOrderRespDTO) notify); - return "success"; - } - throw new UnsupportedOperationException("未知通知:" + toJsonString(notify)); + + // 2. 解析通知数据 + PayRefundRespDTO notify = payClient.parseRefundNotify(params, body); + refundService.notifyRefund(channelId, notify); + return "success"; } } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java index 9b073cc46..977eff93a 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java +++ b/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 { /** * 支付结果的回调地址 */ - private String payNotifyUrl; + private String orderNotifyUrl; /** * 退款结果的回调地址 */ diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/notify/PayNotifyJob.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/notify/PayNotifyJob.java index 2907b75f3..c8b188951 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/notify/PayNotifyJob.java +++ b/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; public class PayNotifyJob implements JobHandler { @Resource - private PayNotifyService payNotifyCoreService; + private PayNotifyService payNotifyService; @Override public String execute(String param) throws Exception { - int notifyCount = payNotifyCoreService.executeNotify(); + int notifyCount = payNotifyService.executeNotify(); return String.format("执行支付通知 %s 个", notifyCount); } diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java index df3c6ce90..fd15f7821 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java +++ b/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 { // 创建支付交易单 order = PayOrderConvert.INSTANCE.convert(reqDTO).setAppId(app.getId()) // 商户相关字段 - .setNotifyUrl(app.getPayNotifyUrl()).setNotifyStatus(PayOrderNotifyStatusEnum.NO.getStatus()) + .setNotifyUrl(app.getOrderNotifyUrl()).setNotifyStatus(PayOrderNotifyStatusEnum.NO.getStatus()) // 订单相关字段 .setStatus(PayOrderStatusEnum.WAITING.getStatus()) // 退款相关字段 @@ -206,7 +206,7 @@ public class PayOrderServiceImpl implements PayOrderService { * @return 支付渠道的回调地址 配置地址 + "/" + channel id */ private String genChannelPayNotifyUrl(PayChannelDO channel) { - return payProperties.getCallbackUrl() + "/" + channel.getId(); + return payProperties.getOrderNotifyUrl() + "/" + channel.getId(); } private String generateOrderExtensionNo() { diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java index 7624f203c..6eb93e0bb 100755 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java +++ b/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 { .setRefundPrice(reqDTO.getPrice()) .setOutTradeNo(orderExtension.getNo()) .setOutRefundNo(refund.getNo()) - .setNotifyUrl(genChannelPayNotifyUrl(channel)) // TODO 芋艿:优化下 notifyUrl + .setNotifyUrl(genChannelPayNotifyUrl(channel)) .setReason(reqDTO.getReason()); PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); // TODO 增加一个 channelErrorCode、channelErrorMsg 字段 // 2.3 处理退款返回 @@ -183,7 +183,7 @@ public class PayRefundServiceImpl implements PayRefundService { * @return 支付渠道的回调地址 配置地址 + "/" + channel id */ private String genChannelPayNotifyUrl(PayChannelDO channel) { - return payProperties.getCallbackUrl() + "/" + channel.getId(); + return payProperties.getRefundNotifyUrl() + "/" + channel.getId(); } private String generateRefundNo() { diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 250a29d29..4cdcb3b52 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -194,7 +194,8 @@ yudao: - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 pay: - callback-url: http://yunai.natapp1.cc/admin-api/pay/notify/callback + order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址 + refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址 access-log: # 访问日志的配置项 enable: false error-code: # 错误码相关配置项 diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 8108af64f..d7d278273 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -142,7 +142,7 @@ yudao: - /admin-api/system/captcha/check # 校验图片验证码,和租户无关 - /admin-api/infra/file/*/get/** # 获取图片,和租户无关 - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号 - - /admin-api/pay/notify/callback/* # 支付回调通知,不携带租户编号 + - /admin-api/pay/notify/** # 支付回调通知,不携带租户编号 - /jmreport/* # 积木报表,无法携带租户编号 - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号 ignore-tables: diff --git a/yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue b/yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue index af210e5d9..c3e691f9f 100644 --- a/yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue +++ b/yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue @@ -1,7 +1,7 @@