From 6f475f8c85cdad39c733579872b402b700139bb8 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Tue, 18 Jul 2023 07:37:03 +0800 Subject: [PATCH] =?UTF-8?q?mall=20+=20pay=EF=BC=9A=201.=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=20PayClient=20=E6=94=AF=E4=BB=98=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E8=BF=94=E5=9B=9E=E4=B8=9A=E5=8A=A1=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=20errorCode=20+=20errorMsg=20=E9=94=99=E8=AF=AF=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/pay/core/client/PayClient.java | 3 +- .../client/dto/order/PayOrderRespDTO.java | 96 ++++++++++++- .../dto/order/PayOrderUnifiedReqDTO.java | 1 - .../dto/order/PayOrderUnifiedRespDTO.java | 38 ------ .../core/client/impl/AbstractPayClient.java | 44 +++--- .../impl/alipay/AbstractAlipayPayClient.java | 81 +++++------ .../impl/alipay/AlipayAppPayClient.java | 11 +- .../impl/alipay/AlipayBarPayClient.java | 12 +- .../client/impl/alipay/AlipayPcPayClient.java | 11 +- .../client/impl/alipay/AlipayQrPayClient.java | 11 +- .../impl/alipay/AlipayWapPayClient.java | 12 +- .../impl/weixin/AbstractWxPayClient.java | 127 +++++++----------- .../client/impl/weixin/WxAppPayClient.java | 6 +- .../client/impl/weixin/WxBarPayClient.java | 30 ++--- .../client/impl/weixin/WxH5PayClient.java | 6 +- .../client/impl/weixin/WxNativePayClient.java | 14 +- .../client/impl/weixin/WxPubPayClient.java | 16 +-- .../enums/PayFrameworkErrorCodeConstants.java | 16 --- .../enums/order/PayOrderDisplayModeEnum.java | 3 +- .../enums/order/PayOrderStatusRespEnum.java | 10 ++ .../aftersale/TradeAfterSaleServiceImpl.java | 2 +- .../module/pay/api/refund/PayRefundApi.java | 4 +- .../module/pay/enums/ErrorCodeConstants.java | 1 + .../pay/api/refund/PayRefundApiImpl.java | 4 +- .../pay/convert/order/PayOrderConvert.java | 4 +- .../dataobject/order/PayOrderExtensionDO.java | 16 ++- .../dal/dataobject/refund/PayRefundDO.java | 5 +- .../service/demo/PayDemoOrderServiceImpl.java | 4 +- .../service/order/PayOrderServiceImpl.java | 65 +++++++-- 29 files changed, 361 insertions(+), 292 deletions(-) delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedRespDTO.java delete mode 100644 yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java 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 ee5d11832..9362466a6 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 @@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.pay.core.client; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; @@ -30,7 +29,7 @@ public interface PayClient { * @param reqDTO 下单信息 * @return 各支付渠道的返回结果 */ - PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO); + PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO); /** * 解析 order 回调数据 diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java index 3f98cf841..928830f66 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java @@ -1,10 +1,9 @@ package cn.iocoder.yudao.framework.pay.core.client.dto.order; +import cn.iocoder.yudao.framework.pay.core.client.exception.PayException; +import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; -import lombok.NoArgsConstructor; import java.time.LocalDateTime; @@ -14,9 +13,6 @@ import java.time.LocalDateTime; * @author 芋道源码 */ @Data -@Builder -@NoArgsConstructor -@AllArgsConstructor public class PayOrderRespDTO { /** @@ -48,8 +44,94 @@ public class PayOrderRespDTO { private LocalDateTime successTime; /** - * 原始的异步通知结果 + * 原始的同步/异步通知结果 */ private Object rawData; + // ========== 主动发起支付时,会返回的字段 ========== + + /** + * 展示模式 + * + * 枚举 {@link PayOrderDisplayModeEnum} 类 + */ + private String displayMode; + /** + * 展示内容 + */ + private String displayContent; + + /** + * 调用渠道的错误码 + * + * 注意:这里返回的是业务异常,而是不系统异常。 + * 如果是系统异常,则会抛出 {@link PayException} + */ + private String channelErrorCode; + /** + * 调用渠道报错时,错误信息 + */ + private String channelErrorMsg; + + public PayOrderRespDTO() { + } + + /** + * 创建【WAITING】状态的订单返回 + */ + public PayOrderRespDTO(String displayMode, String displayContent, + String outTradeNo, Object rawData) { + this.status = PayOrderStatusRespEnum.WAITING.getStatus(); + this.displayMode = displayMode; + this.displayContent = displayContent; + // 相对通用的字段 + this.outTradeNo = outTradeNo; + this.rawData = rawData; + } + + /** + * 创建【SUCCESS】状态的订单返回 + */ + public PayOrderRespDTO(String channelOrderNo, String channelUserId, LocalDateTime successTime, + String outTradeNo, Object rawData) { + this.status = PayOrderStatusRespEnum.SUCCESS.getStatus(); + this.channelOrderNo = channelOrderNo; + this.channelUserId = channelUserId; + this.successTime = successTime; + // 相对通用的字段 + this.outTradeNo = outTradeNo; + this.rawData = rawData; + } + + /** + * 创建【SUCCESS】或【CLOSED】状态的订单返回,适合支付渠道回调时 + */ + public PayOrderRespDTO(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime, + String outTradeNo, Object rawData) { + this.status = status; + this.channelOrderNo = channelOrderNo; + this.channelUserId = channelUserId; + this.successTime = successTime; + // 相对通用的字段 + this.outTradeNo = outTradeNo; + this.rawData = rawData; + } + + /** + * 创建【CLOSED】状态的订单返回,适合调用支付渠道失败时 + * + * 参数和 {@link #PayOrderRespDTO(String, String, String, Object)} 冲突,所以独立个方法出来 + */ + public static PayOrderRespDTO build(String channelErrorCode, String channelErrorMsg, + String outTradeNo, Object rawData) { + PayOrderRespDTO respDTO = new PayOrderRespDTO(); + respDTO.status = PayOrderStatusRespEnum.CLOSED.getStatus(); + respDTO.channelErrorCode = channelErrorCode; + respDTO.channelErrorMsg = channelErrorMsg; + // 相对通用的字段 + respDTO.outTradeNo = outTradeNo; + respDTO.rawData = rawData; + return respDTO; + } + } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java index bd92df13a..f269d2f8f 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java @@ -43,7 +43,6 @@ public class PayOrderUnifiedReqDTO { /** * 商品描述信息 */ - @NotEmpty(message = "商品描述信息不能为空") @Length(max = 128, message = "商品描述信息长度不能超过128") private String body; /** diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedRespDTO.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedRespDTO.java deleted file mode 100644 index 7a282b4eb..000000000 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedRespDTO.java +++ /dev/null @@ -1,38 +0,0 @@ -package cn.iocoder.yudao.framework.pay.core.client.dto.order; - -import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum; -import lombok.Data; - -/** - * 统一下单 Response DTO - * - * @author 芋道源码 - */ -@Data -public class PayOrderUnifiedRespDTO { - - /** - * 展示模式 - * - * 枚举 {@link PayOrderDisplayModeEnum} 类 - */ - private String displayMode; - /** - * 展示内容 - */ - private String displayContent; - - /** - * 渠道支付订单 - * - * 只有在订单直接支付成功时,才会进行返回。 - * 目前只有 bar 条码支付才会出现,它是支付发起时,直接返回是否支付成功的,而其它支付还是异步通知 - */ - private PayOrderRespDTO order; - - public PayOrderUnifiedRespDTO(String displayMode, String displayContent) { - this.displayMode = displayMode; - this.displayContent = displayContent; - } - -} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java index eb3e74777..8be5aa498 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java @@ -1,16 +1,17 @@ package cn.iocoder.yudao.framework.pay.core.client.impl; import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; import cn.iocoder.yudao.framework.pay.core.client.PayClient; import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO; 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.exception.PayException; import lombok.extern.slf4j.Slf4j; -import javax.validation.Validation; +import java.util.Map; import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; @@ -29,6 +30,7 @@ public abstract class AbstractPayClient implemen /** * 渠道编码 */ + @SuppressWarnings("FieldCanBeLocal") private final String channelCode; /** * 支付配置 @@ -73,31 +75,42 @@ public abstract class AbstractPayClient implemen // ============ 支付相关 ========== @Override - public final PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { - Validation.buildDefaultValidatorFactory().getValidator().validate(reqDTO); + public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + ValidationUtils.validate(reqDTO); // 执行统一下单 - PayOrderUnifiedRespDTO resp; + PayOrderRespDTO resp; try { resp = doUnifiedOrder(reqDTO); - } catch (ServiceException ex) { - // 业务异常,都是实现类已经翻译,所以直接抛出即可 - throw ex; } catch (Throwable ex) { // 系统异常,则包装成 PayException 异常抛出 - log.error("[unifiedRefund][request({}) 发起支付异常]", toJsonString(reqDTO), ex); - throw buildException(ex); + log.error("[unifiedRefund][客户端({}) request({}) 发起支付异常]", + getId(), toJsonString(reqDTO), ex); + throw buildPayException(ex); } return resp; } - protected abstract PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) + protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) + throws Throwable; + + @Override + public PayOrderRespDTO parseOrderNotify(Map params, String body) { + try { + return doParseOrderNotify(params, body); + } catch (Throwable ex) { + log.error("[parseOrderNotify][params({}) body({}) 解析失败]", params, body, ex); + throw buildPayException(ex); + } + } + + protected abstract PayOrderRespDTO doParseOrderNotify(Map params, String body) throws Throwable; // ============ 退款相关 ========== @Override public PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) { - Validation.buildDefaultValidatorFactory().getValidator().validate(reqDTO); + ValidationUtils.validate(reqDTO); // 执行统一退款 PayRefundRespDTO resp; try { @@ -107,8 +120,9 @@ public abstract class AbstractPayClient implemen throw ex; } catch (Throwable ex) { // 系统异常,则包装成 PayException 异常抛出 - log.error("[unifiedRefund][request({}) 发起退款异常]", toJsonString(reqDTO), ex); - throw buildException(ex); + log.error("[unifiedRefund][客户端({}) request({}) 发起退款异常]", + getId(), toJsonString(reqDTO), ex); + throw buildPayException(ex); } return resp; } @@ -117,7 +131,7 @@ public abstract class AbstractPayClient implemen // ========== 各种工具方法 ========== - private PayException buildException(Throwable ex) { + private PayException buildPayException(Throwable ex) { if (ex instanceof PayException) { return (PayException) ex; } 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 6bfb5c886..f11c88ec0 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 @@ -5,8 +5,8 @@ import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; +import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; 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; @@ -29,9 +29,7 @@ import java.util.Objects; import java.util.function.Supplier; import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER; -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception0; import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; -import static cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants.ORDER_UNIFIED_ERROR; /** * 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款) @@ -55,6 +53,40 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient params, String body) throws Throwable { + // 1. 校验回调数据 + Map bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8); + AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(), + StandardCharsets.UTF_8.name(), config.getSignType()); + + // 2. 解析订单的状态 + String tradeStatus = bodyObj.get("trade_status"); + Integer status = Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING.getStatus() + : Objects.equals("TRADE_SUCCESS", tradeStatus) ? PayOrderStatusRespEnum.SUCCESS.getStatus() + : Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED.getStatus() : null; + Assert.notNull(status, (Supplier) () -> { + throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body)); + }); + return new PayOrderRespDTO(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")), + bodyObj.get("out_trade_no"), body); + } + + // ============ 退款相关 ========== + /** * 支付宝统一的退款接口 alipay.trade.refund * @@ -95,32 +127,6 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient params, String body) { - // 1. 校验回调数据 - Map bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8); - AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(), - StandardCharsets.UTF_8.name(), config.getSignType()); - - // 2. 解析订单的状态 - String tradeStatus = bodyObj.get("trade_status"); - PayOrderStatusRespEnum status = Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING - : Objects.equals("TRADE_SUCCESS", tradeStatus) ? PayOrderStatusRespEnum.SUCCESS - : Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED : null; - Assert.notNull(status, (Supplier) () -> { - throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body)); - }); - return PayOrderRespDTO.builder() - .status(Objects.requireNonNull(status).getStatus()) - .outTradeNo(bodyObj.get("out_trade_no")) - .channelOrderNo(bodyObj.get("trade_no")) - .channelUserId(bodyObj.get("seller_id")) - .successTime(parseTime(params.get("gmt_payment"))) - .rawData(body) - .build(); - } - @Override public PayRefundRespDTO parseRefundNotify(Map params, String body) { // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。 @@ -145,21 +151,4 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient params, String body) { - try { - // 微信支付 v2 回调结果处理 - switch (config.getApiVersion()) { - case API_VERSION_V2: - return parseOrderNotifyV2(body); - case WxPayClientConfig.API_VERSION_V3: - return parseOrderNotifyV3(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 芋艿:缺一个异常翻译 + public PayOrderRespDTO doParseOrderNotify(Map params, String body) throws WxPayException { + // 微信支付 v2 回调结果处理 + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doParseOrderNotifyV2(body); + case WxPayClientConfig.API_VERSION_V3: + return doParseOrderNotifyV3(body); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); } } - private PayOrderRespDTO parseOrderNotifyV2(String body) throws WxPayException { + private PayOrderRespDTO doParseOrderNotifyV2(String body) throws WxPayException { // 1. 解析回调 WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body); // 2. 构建结果 - return PayOrderRespDTO.builder() - .outTradeNo(response.getOutTradeNo()) - .channelOrderNo(response.getTransactionId()) - .channelUserId(response.getOpenid()) - .status(Objects.equals(response.getResultCode(), "SUCCESS") ? - PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus()) - .successTime(parseDateV2(response.getTimeEnd())) - .rawData(response) - .build(); + Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ? + PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus(); + return new PayOrderRespDTO(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()), + response.getOutTradeNo(), body); } - private PayOrderRespDTO parseOrderNotifyV3(String body) throws WxPayException { + private PayOrderRespDTO doParseOrderNotifyV3(String body) throws WxPayException { // 1. 解析回调 WxPayOrderNotifyV3Result response = client.parseOrderNotifyV3Result(body, null); - WxPayOrderNotifyV3Result.DecryptNotifyResult responseResult = response.getResult(); + WxPayOrderNotifyV3Result.DecryptNotifyResult result = response.getResult(); // 2. 构建结果 - return PayOrderRespDTO.builder() - .outTradeNo(responseResult.getOutTradeNo()) - .channelOrderNo(responseResult.getTradeState()) - .channelUserId(responseResult.getPayer() != null ? responseResult.getPayer().getOpenid() : null) - .status(Objects.equals(responseResult.getTradeState(), "SUCCESS") ? - PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus()) - .successTime(parseDateV3(responseResult.getSuccessTime())) - .build(); + Integer status = Objects.equals(result.getTradeState(), "SUCCESS") ? + PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus(); + String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null; + return new PayOrderRespDTO(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()), + result.getOutTradeNo(), body); } // ============ 退款相关 ========== @@ -182,7 +164,8 @@ public abstract class AbstractWxPayClient extends AbstractPayClient channelExtras; + /** - * 支付渠道异步通知的内容 + * 调用渠道的错误码 + */ + private String channelErrorCode; + /** + * 调用渠道报错时,错误信息 + */ + private String channelErrorMsg; + + /** + * 支付渠道的同步/异步通知的内容 * - * 在支持成功后,会记录回调的数据 + * 对应 {@link PayOrderRespDTO#getRawData()} */ private String channelNotifyData; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java index 1a13a8115..eaf6cd876 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.pay.dal.dataobject.refund; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO; import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum; import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO; @@ -151,9 +152,9 @@ public class PayRefundDO extends BaseDO { private String channelErrorMsg; /** - * 支付渠道异步通知的内容 + * 支付渠道的同步/异步通知的内容 * - * 在退款成功后,会记录回调的数据 + * 对应 {@link PayRefundRespDTO#getRawData()} */ private String channelNotifyData; diff --git a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java index c9bcbbd56..24e7b7572 100644 --- a/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java +++ b/yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java @@ -189,7 +189,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService { // 这里我们是个简单的 demo,所以没有售后维权表,直接使用订单 id + "-refund" 来演示 String refundId = order.getId() + "-refund"; // 2.2 创建退款单 - Long payRefundId = payRefundApi.createPayRefund(new PayRefundCreateReqDTO() + Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO() .setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用 .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 .setMerchantRefundId(refundId) @@ -239,7 +239,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService { } // 2.1 校验退款订单 - PayRefundRespDTO payRefund = payRefundApi.getPayRefund(payRefundId); + PayRefundRespDTO payRefund = payRefundApi.getRefund(payRefundId); if (payRefund == null) { throw exception(PAY_DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND); } 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 e2148f4db..0e5fee42e 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 @@ -4,6 +4,7 @@ import cn.hutool.core.date.DateUtil; import cn.hutool.core.lang.Pair; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; import cn.iocoder.yudao.framework.pay.config.PayProperties; @@ -11,7 +12,6 @@ import cn.iocoder.yudao.framework.pay.core.client.PayClient; import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO; import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; -import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderUnifiedRespDTO; import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; @@ -134,8 +134,7 @@ public class PayOrderServiceImpl implements PayOrderService { return order.getId(); } - @Override - @Transactional(rollbackFor = Exception.class) + @Override // 注意,这里不能添加事务注解,避免调用支付渠道失败时,将 PayOrderExtensionDO 回滚了 public PayOrderSubmitRespVO submitOrder(PayOrderSubmitReqVO reqVO, String userIp) { // 1. 获得 PayOrderDO ,并校验其是否存在 PayOrderDO order = validateOrderCanSubmit(reqVO.getId()); @@ -159,17 +158,20 @@ public class PayOrderServiceImpl implements PayOrderService { .setReturnUrl(reqVO.getReturnUrl()) // 订单相关字段 .setPrice(order.getPrice()).setExpireTime(order.getExpireTime()); - PayOrderUnifiedRespDTO unifiedOrderRespDTO = client.unifiedOrder(unifiedOrderReqDTO); + PayOrderRespDTO unifiedOrderResp = client.unifiedOrder(unifiedOrderReqDTO); // 4. 如果调用直接支付成功,则直接更新支付单状态为成功。例如说:付款码支付,免密支付时,就直接验证支付成功 - if (unifiedOrderRespDTO.getOrder() != null) { - notifyPayOrder(channel, unifiedOrderRespDTO.getOrder()); + if (unifiedOrderResp != null) { + notifyPayOrder(channel, unifiedOrderResp); + // 如有渠道错误码,则抛出业务异常,提示用户 + if (StrUtil.isNotEmpty(unifiedOrderResp.getChannelErrorCode())) { + throw exception(PAY_ORDER_SUBMIT_CHANNEL_ERROR, unifiedOrderResp.getChannelErrorCode(), + unifiedOrderResp.getChannelErrorMsg()); + } // 此处需要读取最新的状态 order = orderMapper.selectById(order.getId()); } - - // 返回成功 - return PayOrderConvert.INSTANCE.convert(order, unifiedOrderRespDTO); + return PayOrderConvert.INSTANCE.convert(order, unifiedOrderResp); } private PayOrderDO validateOrderCanSubmit(Long id) { @@ -269,8 +271,10 @@ public class PayOrderServiceImpl implements PayOrderService { notifyOrderSuccess(channel, notify); return; } - // 情况二:非支付成功的回调,进行忽略 - log.info("[notifyPayOrder][非支付成功的回调({}),直接忽略]", toJsonString(notify)); + // 情况二:支付失败的回调 + if (PayOrderStatusRespEnum.isClosed(notify.getStatus())) { + notifyOrderClosed(channel, notify); + } } private void notifyOrderSuccess(PayChannelDO channel, PayOrderRespDTO notify) { @@ -300,7 +304,7 @@ public class PayOrderServiceImpl implements PayOrderService { throw exception(PAY_ORDER_EXTENSION_NOT_FOUND); } if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { // 如果已经是成功,直接返回,不用重复更新 - log.info("[updateOrderExtensionSuccess][支付拓展单({}) 已经是已支付,无需更新为已支付]", orderExtension.getId()); + log.info("[updateOrderExtensionSuccess][支付拓展单({}) 已经是已支付,无需更新]", orderExtension.getId()); return orderExtension; } if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付 @@ -327,7 +331,7 @@ public class PayOrderServiceImpl implements PayOrderService { * value:PayOrderDO 对象 */ private Pair updateOrderExtensionSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension, - PayOrderRespDTO notify) { + PayOrderRespDTO notify) { // 1. 判断 PayOrderDO 是否处于待支付 PayOrderDO order = orderMapper.selectById(orderExtension.getOrderId()); if (order == null) { @@ -335,7 +339,7 @@ public class PayOrderServiceImpl implements PayOrderService { } if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功,直接返回,不用重复更新 && Objects.equals(order.getSuccessExtensionId(), orderExtension.getId())) { - log.info("[updateOrderExtensionSuccess][支付订单({}) 已经是已支付,无需更新为已支付]", order.getId()); + log.info("[updateOrderExtensionSuccess][支付订单({}) 已经是已支付,无需更新]", order.getId()); return Pair.of(true, order); } if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付 @@ -356,4 +360,37 @@ public class PayOrderServiceImpl implements PayOrderService { return Pair.of(false, order); } + private void notifyOrderClosed(PayChannelDO channel, PayOrderRespDTO notify) { + updateOrderExtensionClosed(channel, notify); + } + + private void updateOrderExtensionClosed(PayChannelDO channel, PayOrderRespDTO notify) { + // 1. 查询 PayOrderExtensionDO + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo()); + if (orderExtension == null) { + throw exception(PAY_ORDER_EXTENSION_NOT_FOUND); + } + if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { // 如果已经是关闭,直接返回,不用重复更新 + log.info("[updateOrderExtensionClosed][支付拓展单({}) 已经是支付关闭,无需更新]", orderExtension.getId()); + return; + } + // 一般出现先是支付成功,然后支付关闭,都是全部退款导致关闭的场景。这个情况,我们不更新支付拓展单,只通过退款流程,更新支付单 + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { + log.info("[updateOrderExtensionClosed][支付拓展单({}) 是已支付,无需更新为支付关闭]", orderExtension.getId()); + return; + } + if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付 + throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + + // 2. 更新 PayOrderExtensionDO + int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(), + PayOrderExtensionDO.builder().status(PayOrderStatusEnum.CLOSED.getStatus()).channelNotifyData(toJsonString(notify)) + .channelErrorCode(notify.getChannelErrorCode()).channelErrorMsg(notify.getChannelErrorMsg()).build()); + if (updateCounts == 0) { // 校验状态,必须是待支付 + throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + log.info("[updateOrderExtensionClosed][支付拓展单({}) 更新为支付关闭]", orderExtension.getId()); + } + }