Browse Source

Merge remote-tracking branch 'yudao/feature/mall_product' into feature/mall_product

puhui999 2 years ago
parent
commit
d80bf5a368
78 changed files with 1633 additions and 1110 deletions
  1. 8 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java
  2. 17 6
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java
  3. 26 16
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
  4. 0 29
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayNotifyReqDTO.java
  5. 0 38
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayOrderNotifyRespDTO.java
  6. 0 58
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayRefundNotifyRespDTO.java
  7. 137 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java
  8. 6 5
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java
  9. 0 38
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedRespDTO.java
  10. 47 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java
  11. 21 25
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java
  12. 0 24
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedRespDTO.java
  13. 39 21
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
  14. 78 76
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
  15. 9 6
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java
  16. 10 6
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java
  17. 9 6
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java
  18. 9 6
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java
  19. 9 7
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java
  20. 202 75
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
  21. 3 10
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java
  22. 14 26
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClient.java
  23. 3 10
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxH5PayClient.java
  24. 18 25
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClient.java
  25. 11 15
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java
  26. 12 20
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java
  27. 0 16
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java
  28. 1 2
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java
  29. 23 1
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java
  30. 0 23
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayNotifyRefundStatusEnum.java
  31. 0 23
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundRespEnum.java
  32. 28 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java
  33. 2 2
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java
  34. 2 4
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java
  35. 123 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java
  36. 83 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java
  37. 1 1
      yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/TradeAfterSaleServiceImpl.java
  38. 2 2
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApi.java
  39. 13 7
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java
  40. 5 3
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java
  41. 10 0
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderStatusEnum.java
  42. 8 4
      yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/refund/PayRefundStatusEnum.java
  43. 1 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApiImpl.java
  44. 4 4
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApiImpl.java
  45. 1 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java
  46. 30 31
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java
  47. 1 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/PayOrderController.java
  48. 0 3
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java
  49. 1 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java
  50. 2 2
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java
  51. 8 17
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/refund/PayRefundConvert.java
  52. 1 1
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/app/PayAppDO.java
  53. 5 4
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java
  54. 19 8
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderExtensionDO.java
  55. 34 59
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java
  56. 32 14
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/refund/PayRefundMapper.java
  57. 3 3
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderRefundStatusEnum.java
  58. 2 2
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/job/notify/PayNotifyJob.java
  59. 14 11
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceImpl.java
  60. 10 5
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java
  61. 1 0
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderExtensionService.java
  62. 25 10
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderService.java
  63. 130 56
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java
  64. 3 5
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundService.java
  65. 140 150
      yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java
  66. 2 6
      yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceTest.java
  67. 9 9
      yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java
  68. 10 23
      yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java
  69. 2 1
      yudao-server/src/main/resources/application-local.yaml
  70. 1 1
      yudao-server/src/main/resources/application.yaml
  71. 5 5
      yudao-ui-admin/src/api/pay/channel.js
  72. 0 0
      yudao-ui-admin/src/assets/images/pay/icon/wx_bar.svg
  73. 1 0
      yudao-ui-admin/src/assets/images/pay/icon/wx_native.svg
  74. 8 0
      yudao-ui-admin/src/utils/constants.js
  75. 71 33
      yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue
  76. 28 4
      yudao-ui-admin/src/views/pay/app/index.vue
  77. 38 2
      yudao-ui-admin/src/views/pay/cashier/index.vue
  78. 2 1
      yudao-ui-admin/src/views/pay/demo/index.vue

+ 8 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/validation/ValidationUtils.java

@@ -1,10 +1,12 @@
 package cn.iocoder.yudao.framework.common.util.validation;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Assert;
 import org.springframework.util.StringUtils;
 
 import javax.validation.ConstraintViolation;
 import javax.validation.ConstraintViolationException;
+import javax.validation.Validation;
 import javax.validation.Validator;
 import java.util.Set;
 import java.util.regex.Pattern;
@@ -37,6 +39,12 @@ public class ValidationUtils {
                 && PATTERN_XML_NCNAME.matcher(str).matches();
     }
 
+    public static void validate(Object object, Class<?>... groups) {
+        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+        Assert.notNull(validator);
+        validate(validator, object, groups);
+    }
+
     public static void validate(Validator validator, Object object, Class<?>... groups) {
         Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
         if (CollUtil.isNotEmpty(constraintViolations)) {

+ 17 - 6
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;
 
 }

+ 26 - 16
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java

@@ -1,12 +1,11 @@
 package cn.iocoder.yudao.framework.pay.core.client;
 
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
+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.dto.refund.PayRefundUnifiedRespDTO;
+
+import java.util.Map;
 
 /**
  * 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能
@@ -22,31 +21,42 @@ public interface PayClient {
      */
     Long getId();
 
+    // ============ 支付相关 ==========
+
     /**
      * 调用支付渠道,统一下单
      *
      * @param reqDTO 下单信息
      * @return 各支付渠道的返回结果
      */
-    PayOrderUnifiedRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
+    PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
+
+    /**
+     * 解析 order 回调数据
+     *
+     * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
+     * @param body HTTP 回调接口的 request body
+     * @return 支付订单信息
+     */
+    PayOrderRespDTO parseOrderNotify(Map<String, String> params, String body);
+
+    // ============ 退款相关 ==========
 
     /**
      * 调用支付渠道,进行退款
+     *
      * @param reqDTO  统一退款请求信息
-     * @return 各支付渠道的统一返回结果
+     * @return 退款信息
      */
-    PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
+    PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO);
 
     /**
-     * 解析回调数据
+     * 解析 refund 回调数据
      *
-     * @param rawNotify 通知内容
-     * @return 回调对象
-     *         1. {@link PayRefundNotifyRespDTO} 退款通知
-     *         2. {@link PayOrderNotifyRespDTO} 支付通知
+     * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
+     * @param body HTTP 回调接口的 request body
+     * @return 支付订单信息
      */
-    default Object parseNotify(PayNotifyReqDTO rawNotify) {
-        throw new UnsupportedOperationException("未实现 parseNotify 方法!");
-    }
+    PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body);
 
 }

+ 0 - 29
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayNotifyReqDTO.java

@@ -1,29 +0,0 @@
-package cn.iocoder.yudao.framework.pay.core.client.dto.notify;
-
-import lombok.Builder;
-import lombok.Data;
-import lombok.ToString;
-
-import java.util.Map;
-
-
-/**
- * 支付订单,退款订单回调,渠道的统一通知请求数据
- */
-@Data
-@ToString
-@Builder
-public class PayNotifyReqDTO {
-
-
-    /**
-     * HTTP 回调接口的 request body
-     */
-    private String body;
-
-    /**
-     * HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数
-     */
-    private Map<String,String> params;
-
-}

+ 0 - 38
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayOrderNotifyRespDTO.java

@@ -1,38 +0,0 @@
-package cn.iocoder.yudao.framework.pay.core.client.dto.notify;
-
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.time.LocalDateTime;
-
-/**
- * 支付通知 Response DTO
- *
- * @author 芋道源码
- */
-@Data
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class PayOrderNotifyRespDTO {
-
-    /**
-     * 支付订单号(支付模块的)
-     */
-    private String orderExtensionNo;
-    /**
-     * 支付渠道编号
-     */
-    private String channelOrderNo;
-    /**
-     * 支付渠道用户编号
-     */
-    private String channelUserId;
-    /**
-     * 支付成功时间
-     */
-    private LocalDateTime successTime;
-
-}

+ 0 - 58
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/notify/PayRefundNotifyRespDTO.java

@@ -1,58 +0,0 @@
-package cn.iocoder.yudao.framework.pay.core.client.dto.notify;
-
-import cn.iocoder.yudao.framework.pay.core.enums.refund.PayNotifyRefundStatusEnum;
-import lombok.Builder;
-import lombok.Data;
-import lombok.ToString;
-
-import java.time.LocalDateTime;
-
-/**
- * 从渠道返回数据中解析得到的支付退款通知的Notify DTO
- *
- * @author jason
- */
-@Data
-@ToString
-@Builder
-public class PayRefundNotifyRespDTO {
-
-    /**
-     * 支付渠道编号
-     */
-    private String channelOrderNo;
-
-    /**
-     * 交易订单号,根据规则生成
-     * 调用支付渠道时,使用该字段作为对接的订单号。
-     * 1. 调用微信支付 https://api.mch.weixin.qq.com/pay/unifiedorder 时,使用该字段作为 out_trade_no
-     * 2. 调用支付宝 https://opendocs.alipay.com/apis 时,使用该字段作为 out_trade_no
-     *  这里对应 pay_extension 里面的 no
-     * 例如说,P202110132239124200055
-     */
-    private String tradeNo;
-
-    /**
-     * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
-     * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
-     * 退款请求号。
-     * 标识一次退款请求,需要保证在交易号下唯一,如需部分退款,则此参数必传。
-     * 注:针对同一次退款请求,如果调用接口失败或异常了,重试时需要保证退款请求号不能变更,
-     * 防止该笔交易重复退款。支付宝会保证同样的退款请求号多次请求只会退一次。
-     * 退款单请求号,根据规则生成
-     *
-     * 例如说,RR202109181134287570000
-     */
-    private String reqNo;
-
-    /**
-     * 退款是否成功
-     */
-    private PayNotifyRefundStatusEnum status;
-
-    /**
-     * 退款成功时间
-     */
-    private LocalDateTime refundSuccessTime;
-
-}

+ 137 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderRespDTO.java

@@ -0,0 +1,137 @@
+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.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 渠道支付订单 Response DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class PayOrderRespDTO {
+
+    /**
+     * 支付状态
+     *
+     * 枚举:{@link PayOrderStatusRespEnum}
+     */
+    private Integer status;
+
+    /**
+     * 外部订单号
+     *
+     * 对应 PayOrderExtensionDO 的 no 字段
+     */
+    private String outTradeNo;
+
+    /**
+     * 支付渠道编号
+     */
+    private String channelOrderNo;
+    /**
+     * 支付渠道用户编号
+     */
+    private String channelUserId;
+
+    /**
+     * 支付成功时间
+     */
+    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;
+    }
+
+}

+ 6 - 5
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java

@@ -28,10 +28,12 @@ public class PayOrderUnifiedReqDTO {
     // ========== 商户相关字段 ==========
 
     /**
-     * 商户订单编号
+     * 外部订单号
+     *
+     * 对应 PayOrderExtensionDO 的 no 字段
      */
-    @NotEmpty(message = "商户订单编号不能为空")
-    private String merchantOrderId;
+    @NotEmpty(message = "外部订单编号不能为空")
+    private String outTradeNo;
     /**
      * 商品标题
      */
@@ -41,7 +43,6 @@ public class PayOrderUnifiedReqDTO {
     /**
      * 商品描述信息
      */
-    @NotEmpty(message = "商品描述信息不能为空")
     @Length(max = 128, message = "商品描述信息长度不能超过128")
     private String body;
     /**
@@ -63,7 +64,7 @@ public class PayOrderUnifiedReqDTO {
      */
     @NotNull(message = "支付金额不能为空")
     @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
-    private Integer amount;
+    private Integer price;
 
     /**
      * 支付过期时间

+ 0 - 38
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/order/PayOrderUnifiedRespDTO.java

@@ -1,38 +0,0 @@
-package cn.iocoder.yudao.framework.pay.core.client.dto.order;
-
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
-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 PayOrderNotifyRespDTO notify;
-
-    public PayOrderUnifiedRespDTO(String displayMode, String displayContent) {
-        this.displayMode = displayMode;
-        this.displayContent = displayContent;
-    }
-
-}

+ 47 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundRespDTO.java

@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
+
+import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 渠道退款订单 Response DTO
+ *
+ * @author jason
+ */
+@Data
+public class PayRefundRespDTO {
+
+    /**
+     * 退款状态
+     *
+     * 枚举 {@link PayRefundStatusRespEnum}
+     */
+    private Integer status;
+
+    /**
+     * 外部退款号
+     *
+     * 对应 PayRefundDO 的 no 字段
+     */
+    private String outRefundNo;
+
+    /**
+     * 渠道退款单号
+     *
+     * 对应 PayRefundDO.channelRefundNo 字段
+     */
+    private String channelRefundNo;
+
+    /**
+     * 退款成功时间
+     */
+    private LocalDateTime successTime;
+
+    /**
+     * 原始的异步通知结果
+     */
+    private Object rawData;
+
+}

+ 21 - 25
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java

@@ -24,33 +24,20 @@ import javax.validation.constraints.NotNull;
 public class PayRefundUnifiedReqDTO {
 
     /**
-     * 用户 IP
+     * 外部订单号
+     *
+     * 对应 PayOrderExtensionDO 的 no 字段
      */
-    private String userIp;
+    @NotEmpty(message = "外部订单编号不能为空")
+    private String outTradeNo;
 
-    // TODO @jason:这个是否为非必传字段呀,只需要传递 payTradeNo 字段即可。尽可能精简
     /**
-     * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 transaction_id
-     * https://opendocs.alipay.com/apis alipay.trade.refund 中的 trade_no
-     * 渠道订单号
+     * 外部退款号
+     *
+     * 对应 PayRefundDO 的 no 字段
      */
-    private String channelOrderNo;
-
-    /**
-     * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_trade_no
-     * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
-     * 支付交易号 {PayOrderExtensionDO no字段} 和 渠道订单号 不能同时为空
-     */
-    private String payTradeNo;
-
-    /**
-     * https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 中的 out_refund_no
-     * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_trade_no
-     * 退款请求单号  同一退款请求单号多次请求只退一笔。
-     * 使用 商户的退款单号。{PayRefundDO 字段 merchantRefundNo}
-     */
-    @NotEmpty(message = "退款请求单号")
-    private String merchantRefundId;
+    @NotEmpty(message = "退款请求单号不能为空")
+    private String outRefundNo;
 
     /**
      * 退款原因
@@ -58,16 +45,25 @@ public class PayRefundUnifiedReqDTO {
     @NotEmpty(message = "退款原因不能为空")
     private String reason;
 
+    /**
+     * 支付金额,单位:分
+     *
+     * 目前微信支付在退款的时候,必须传递该字段
+     */
+    @NotNull(message = "支付金额不能为空")
+    @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
+    private Integer payPrice;
     /**
      * 退款金额,单位:分
      */
     @NotNull(message = "退款金额不能为空")
     @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
-    private Integer amount;
+    private Integer refundPrice;
 
     /**
-     * 退款结果 notify 回调地址, 支付宝退款不需要回调地址, 微信需要
+     * 退款结果 notify 回调地址
      */
+    @NotEmpty(message = "支付结果的回调地址不能为空")
     @URL(message = "支付结果的 notify 回调地址必须是 URL 格式")
     private String notifyUrl;
 

+ 0 - 24
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/refund/PayRefundUnifiedRespDTO.java

@@ -1,24 +0,0 @@
-package cn.iocoder.yudao.framework.pay.core.client.dto.refund;
-
-import lombok.AllArgsConstructor;
-import lombok.Builder;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import lombok.experimental.Accessors;
-/**
- * 统一退款 Response DTO
- *
- * @author jason
- */
-@Accessors(chain = true)
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-@Data
-public class PayRefundUnifiedRespDTO {
-
-    /**
-     * 渠道退款单编号
-     */
-    private String channelRefundId;
-}

+ 39 - 21
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.dto.refund.PayRefundUnifiedRespDTO;
 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<Config extends PayClientConfig> implemen
     /**
      * 渠道编码
      */
+    @SuppressWarnings("FieldCanBeLocal")
     private final String channelCode;
     /**
      * 支付配置
@@ -46,7 +48,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
      */
     public final void init() {
         doInit();
-        log.info("[init][配置({}) 初始化完成]", config);
+        log.info("[init][客户端({}) 初始化完成]", getId());
     }
 
     /**
@@ -59,7 +61,7 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
         if (config.equals(this.config)) {
             return;
         }
-        log.info("[refresh][配置({})发生变化,重新初始化]", config);
+        log.info("[refresh][客户端({})发生变化,重新初始化]", getId());
         this.config = config;
         // 初始化
         this.init();
@@ -70,32 +72,47 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
         return channelId;
     }
 
+    // ============ 支付相关 ==========
+
     @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 PayRefundUnifiedRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
-        Validation.buildDefaultValidatorFactory().getValidator().validate(reqDTO);
+    public PayOrderRespDTO parseOrderNotify(Map<String, String> 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<String, String> params, String body)
+            throws Throwable;
+
+    // ============ 退款相关 ==========
+
+    @Override
+    public PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
+        ValidationUtils.validate(reqDTO);
         // 执行统一退款
-        PayRefundUnifiedRespDTO resp;
+        PayRefundRespDTO resp;
         try {
             resp = doUnifiedRefund(reqDTO);
         } catch (ServiceException ex) {
@@ -103,17 +120,18 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> 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;
     }
 
-    protected abstract PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
+    protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
 
     // ========== 各种工具方法 ==========
 
-    private PayException buildException(Throwable ex) {
+    private PayException buildPayException(Throwable ex) {
         if (ex instanceof PayException) {
             return (PayException) ex;
         }

+ 78 - 76
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java

@@ -2,16 +2,19 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
 
 import cn.hutool.core.bean.BeanUtil;
 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.notify.PayNotifyReqDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
+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.dto.refund.PayRefundUnifiedRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
-import cn.iocoder.yudao.framework.pay.core.enums.refund.PayNotifyRefundStatusEnum;
-import com.alipay.api.*;
+import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.AlipayConfig;
+import com.alipay.api.AlipayResponse;
+import com.alipay.api.DefaultAlipayClient;
 import com.alipay.api.domain.AlipayTradeRefundModel;
 import com.alipay.api.internal.util.AlipaySignature;
 import com.alipay.api.request.AlipayTradeRefundRequest;
@@ -22,11 +25,11 @@ import lombok.extern.slf4j.Slf4j;
 import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
 import java.util.Map;
+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;
 
 /**
  * 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款)
@@ -50,72 +53,88 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
         this.client = new DefaultAlipayClient(alipayConfig);
     }
 
+    // ============ 支付相关 ==========
+
+    /**
+     * 构造支付关闭的 {@link PayOrderRespDTO} 对象
+     *
+     * @return 支付关闭的 {@link PayOrderRespDTO} 对象
+     */
+    protected PayOrderRespDTO buildClosedPayOrderRespDTO(PayOrderUnifiedReqDTO reqDTO, AlipayResponse response) {
+        Assert.isFalse(response.isSuccess());
+        return PayOrderRespDTO.build(response.getSubCode(), response.getSubMsg(),
+                reqDTO.getOutTradeNo(), response);
+    }
+
+    @Override
+    public PayOrderRespDTO doParseOrderNotify(Map<String, String> params, String body) throws Throwable {
+        // 1. 校验回调数据
+        Map<String, String> 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<Throwable>) () -> {
+            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
+     *
      * @param reqDTO 退款请求 request DTO
      * @return 退款请求 Response
      */
     @Override
-    protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO)  {
-        AlipayTradeRefundModel model=new AlipayTradeRefundModel();
-        model.setTradeNo(reqDTO.getChannelOrderNo());
-        model.setOutTradeNo(reqDTO.getPayTradeNo());
-
-        model.setOutRequestNo(reqDTO.getMerchantRefundId());
-        model.setRefundAmount(formatAmount(reqDTO.getAmount()));
+    protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO)  {
+        // 1.1 构建 AlipayTradeRefundModel 请求
+        AlipayTradeRefundModel model = new AlipayTradeRefundModel();
+        model.setOutTradeNo(reqDTO.getOutTradeNo());
+        model.setOutRequestNo(reqDTO.getOutRefundNo());
+        model.setRefundAmount(formatAmount(reqDTO.getRefundPrice()));
         model.setRefundReason(reqDTO.getReason());
-
-        AlipayTradeRefundRequest refundRequest = new AlipayTradeRefundRequest();
-        refundRequest.setBizModel(model);
-        refundRequest.setNotifyUrl(reqDTO.getNotifyUrl());
-        refundRequest.setReturnUrl(reqDTO.getNotifyUrl());
+        // 1.2 构建 AlipayTradePayRequest 请求
+        AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
+        request.setBizModel(model);
         try {
-            AlipayTradeRefundResponse response =  client.execute(refundRequest);
-            log.info("[doUnifiedRefund][response({}) 发起退款 渠道返回", toJsonString(response));
+            // 2.1 执行请求
+            AlipayTradeRefundResponse response =  client.execute(request);
+            // 2.2 创建返回结果
+            PayRefundRespDTO refund = new PayRefundRespDTO()
+                    .setOutRefundNo(reqDTO.getOutRefundNo())
+                    .setRawData(response);
+            // 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。
+            // 另外,支付宝没有退款单号,所以不用设置
             if (response.isSuccess()) {
-                //退款导致触发的异步通知是发送到支付接口中设置的notify_url
-                //支付宝不返回退款单号,设置为空
-                PayRefundUnifiedRespDTO respDTO = new PayRefundUnifiedRespDTO();
-                respDTO.setChannelRefundId("");
-//                return PayCommonResult.build(response.getCode(), response.getMsg(), respDTO, codeMapping); TODO
-                return null;
+                refund.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())
+                        .setSuccessTime(LocalDateTimeUtil.of(response.getGmtRefundPay()));
+                Assert.notNull(refund.getSuccessTime(), "退款成功时间不能为空");
+            } else {
+                refund.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus());
             }
-            // 失败。需要抛出异常
-//            return PayCommonResult.build(response.getCode(), response.getMsg(), null, codeMapping); TODO
-            return null;
+            return refund;
         } catch (AlipayApiException e) {
-            // TODO 记录异常日志
-            log.error("[doUnifiedRefund][request({}) 发起退款失败,网络读超时,退款状态未知]", toJsonString(reqDTO), e);
-//            return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping); TODO
+            log.error("[doUnifiedRefund][request({}) 发起退款异常]", toJsonString(reqDTO), e);
             return null;
         }
     }
 
     @Override
-    @SneakyThrows
-    public Object parseNotify(PayNotifyReqDTO rawNotify) {
-        // 1. 校验回调数据
-        String body = rawNotify.getBody();
-        Map<String, String> params = rawNotify.getParams();
-        Map<String, String> bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8);
-        AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(),
-                StandardCharsets.UTF_8.name(), "RSA2");
-
-        // 2.1 退款的情况
-        if (bodyObj.containsKey("refund_fee")) {
-            return PayRefundNotifyRespDTO.builder().channelOrderNo(bodyObj.get("trade_no"))
-                    .tradeNo(bodyObj.get("out_trade_no")).reqNo(bodyObj.get("out_biz_no"))
-                    .status(PayNotifyRefundStatusEnum.SUCCESS)
-                    .refundSuccessTime(parseTime(params.get("gmt_refund")))
-                    .build();
-        }
-        // 2.2 支付的情况
-        return PayOrderNotifyRespDTO.builder()
-                .orderExtensionNo(bodyObj.get("out_trade_no"))
-                .channelOrderNo(bodyObj.get("trade_no"))
-                .channelUserId(bodyObj.get("seller_id"))
-                .successTime(parseTime(params.get("notify_time")))
-                .build();
+    public PayRefundRespDTO parseRefundNotify(Map<String, String> params, String body) {
+        // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。
+        // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调
+        // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有
+        // 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。
+        // 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。
+        throw new UnsupportedOperationException("支付宝无退款回调");
     }
 
     // ========== 各种工具方法 ==========
@@ -132,21 +151,4 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
         return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER);
     }
 
-    /**
-     * 校验支付宝统一下单的响应
-     *
-     * 如果校验不通过,则抛出 {@link cn.iocoder.yudao.framework.common.exception.ServiceException} 异常
-     *
-     * @param request 请求
-     * @param response 响应
-     */
-    protected void validateUnifiedOrderResponse(Object request, AlipayResponse response) {
-        if (response.isSuccess()) {
-            return;
-        }
-        log.error("[validateUnifiedOrderResponse][发起支付失败,request({}),response({})]",
-                JsonUtils.toJsonString(request), JsonUtils.toJsonString(response));
-        throw exception0(ORDER_UNIFIED_ERROR.getCode(), response.getSubMsg());
-    }
-
 }

+ 9 - 6
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
 
+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.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
 import com.alipay.api.AlipayApiException;
@@ -27,14 +27,14 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
     }
 
     @Override
-    public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
+    public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
         // 1.1 构建 AlipayTradeAppPayModel 请求
         AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
         // ① 通用的参数
-        model.setOutTradeNo(reqDTO.getMerchantOrderId());
+        model.setOutTradeNo(reqDTO.getOutTradeNo());
         model.setSubject(reqDTO.getSubject());
         model.setBody(reqDTO.getBody());
-        model.setTotalAmount(formatAmount(reqDTO.getAmount()));
+        model.setTotalAmount(formatAmount(reqDTO.getPrice()));
         model.setProductCode(" QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品
         // ② 个性化的参数【无】
         // ③ 支付宝扫码支付只有一种展示
@@ -49,8 +49,11 @@ public class AlipayAppPayClient extends AbstractAlipayPayClient {
         // 2.1 执行请求
         AlipayTradeAppPayResponse response = client.execute(request);
         // 2.2 处理结果
-        validateUnifiedOrderResponse(request, response);
-        return new PayOrderUnifiedRespDTO(displayMode, "");
+        if (!response.isSuccess()) {
+            return buildClosedPayOrderRespDTO(reqDTO, response);
+        }
+        return new PayOrderRespDTO(displayMode, "",
+                reqDTO.getOutTradeNo(), response);
     }
 
 }

+ 10 - 6
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java

@@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
 
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.StrUtil;
+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.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
 import com.alipay.api.AlipayApiException;
@@ -30,7 +30,7 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
     }
 
     @Override
-    public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
+    public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
         String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code");
         if (StrUtil.isEmpty(authCode)) {
             throw exception0(BAD_REQUEST.getCode(), "条形码不能为空");
@@ -39,10 +39,10 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
         // 1.1 构建 AlipayTradePayModel 请求
         AlipayTradePayModel model = new AlipayTradePayModel();
         // ① 通用的参数
-        model.setOutTradeNo(reqDTO.getMerchantOrderId());
+        model.setOutTradeNo(reqDTO.getOutTradeNo());
         model.setSubject(reqDTO.getSubject());
         model.setBody(reqDTO.getBody());
-        model.setTotalAmount(formatAmount(reqDTO.getAmount()));
+        model.setTotalAmount(formatAmount(reqDTO.getPrice()));
         model.setScene("bar_code"); // 当面付条码支付场景
         // ② 个性化的参数
         model.setAuthCode(authCode);
@@ -55,11 +55,15 @@ public class AlipayBarPayClient extends AbstractAlipayPayClient {
         request.setNotifyUrl(reqDTO.getNotifyUrl());
         request.setReturnUrl(reqDTO.getReturnUrl());
 
+        // TODO 芋艿:各种边界的处理
         // 2.1 执行请求
         AlipayTradePayResponse response = client.execute(request);
         // 2.2 处理结果
-        validateUnifiedOrderResponse(request, response);
-        return new PayOrderUnifiedRespDTO(displayMode, "");
+        if (!response.isSuccess()) {
+            return buildClosedPayOrderRespDTO(reqDTO, response);
+        }
+        return new PayOrderRespDTO(displayMode, "",
+                reqDTO.getOutTradeNo(), response);
     }
 
 }

+ 9 - 6
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java

@@ -2,8 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
 
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.http.Method;
+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.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
 import com.alipay.api.AlipayApiException;
@@ -29,14 +29,14 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
     }
 
     @Override
-    public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
+    public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
         // 1.1 构建 AlipayTradePagePayModel 请求
         AlipayTradePagePayModel model = new AlipayTradePagePayModel();
         // ① 通用的参数
-        model.setOutTradeNo(reqDTO.getMerchantOrderId());
+        model.setOutTradeNo(reqDTO.getOutTradeNo());
         model.setSubject(reqDTO.getSubject());
         model.setBody(reqDTO.getBody());
-        model.setTotalAmount(formatAmount(reqDTO.getAmount()));
+        model.setTotalAmount(formatAmount(reqDTO.getPrice()));
         model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
         model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY
         // ② 个性化的参数
@@ -60,8 +60,11 @@ public class AlipayPcPayClient extends AbstractAlipayPayClient {
             response = client.pageExecute(request, Method.GET.name());
         }
         // 2.2 处理结果
-        validateUnifiedOrderResponse(request, response);
-        return new PayOrderUnifiedRespDTO(displayMode, response.getBody());
+        if (!response.isSuccess()) {
+            return buildClosedPayOrderRespDTO(reqDTO, response);
+        }
+        return new PayOrderRespDTO(displayMode, response.getBody(),
+                reqDTO.getOutTradeNo(), response);
     }
 
 }

+ 9 - 6
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java

@@ -1,7 +1,7 @@
 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
 
+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.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
 import com.alipay.api.AlipayApiException;
@@ -25,14 +25,14 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient {
     }
 
     @Override
-    public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
+    public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
         // 1.1 构建 AlipayTradePrecreateModel 请求
         AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
         // ① 通用的参数
-        model.setOutTradeNo(reqDTO.getMerchantOrderId());
+        model.setOutTradeNo(reqDTO.getOutTradeNo());
         model.setSubject(reqDTO.getSubject());
         model.setBody(reqDTO.getBody());
-        model.setTotalAmount(formatAmount(reqDTO.getAmount()));
+        model.setTotalAmount(formatAmount(reqDTO.getPrice()));
         model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT
         // ② 个性化的参数【无】
         // ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开
@@ -47,8 +47,11 @@ public class AlipayQrPayClient extends AbstractAlipayPayClient {
         // 2.1 执行请求
         AlipayTradePrecreateResponse response = client.execute(request);
         // 2.2 处理结果
-        validateUnifiedOrderResponse(request, response);
-        return new PayOrderUnifiedRespDTO(displayMode, response.getQrCode());
+        if (!response.isSuccess()) {
+            return buildClosedPayOrderRespDTO(reqDTO, response);
+        }
+        return new PayOrderRespDTO(displayMode, response.getQrCode(),
+                reqDTO.getOutTradeNo(), response);
     }
 
 }

+ 9 - 7
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java

@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
 
 import cn.hutool.http.Method;
+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.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
 import com.alipay.api.AlipayApiException;
@@ -26,14 +26,14 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient {
     }
 
     @Override
-    public PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
+    public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
         // 1.1 构建 AlipayTradeWapPayModel 请求
         AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
         // ① 通用的参数
-        model.setOutTradeNo(reqDTO.getMerchantOrderId());
+        model.setOutTradeNo(reqDTO.getOutTradeNo());
         model.setSubject(reqDTO.getSubject());
         model.setBody(reqDTO.getBody());
-        model.setTotalAmount(formatAmount(reqDTO.getAmount()));
+        model.setTotalAmount(formatAmount(reqDTO.getPrice()));
         model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY
         // ② 个性化的参数【无】
         // ③ 支付宝 Wap 支付只有一种展示:URL
@@ -48,10 +48,12 @@ public class AlipayWapPayClient extends AbstractAlipayPayClient {
 
         // 2.1 执行请求
         AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name());
-
         // 2.2 处理结果
-        validateUnifiedOrderResponse(request, response);
-        return new PayOrderUnifiedRespDTO(displayMode, response.getBody());
+        if (!response.isSuccess()) {
+            return buildClosedPayOrderRespDTO(reqDTO, response);
+        }
+        return new PayOrderRespDTO(displayMode, response.getBody(),
+                reqDTO.getOutTradeNo(), response);
     }
 
 }

+ 202 - 75
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java

@@ -1,19 +1,26 @@
 package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.codec.Base64;
 import cn.hutool.core.date.LocalDateTimeUtil;
 import cn.hutool.core.date.TemporalAccessorUtil;
-import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.io.FileUtils;
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
+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.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.notify.WxPayRefundNotifyV3Result;
+import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
+import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
+import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
+import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
 import com.github.binarywang.wxpay.config.WxPayConfig;
 import com.github.binarywang.wxpay.exception.WxPayException;
 import com.github.binarywang.wxpay.service.WxPayService;
@@ -22,12 +29,11 @@ import lombok.extern.slf4j.Slf4j;
 
 import java.time.LocalDateTime;
 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.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.*;
-import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+import static cn.hutool.core.date.DatePattern.*;
+import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2;
 
 /**
  * 微信支付抽象类,实现微信统一的接口、以及部分实现(退款)
@@ -53,12 +59,14 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         WxPayConfig payConfig = new WxPayConfig();
         BeanUtil.copyProperties(config, payConfig, "keyContent");
         payConfig.setTradeType(tradeType);
+        // weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决
+        if (Base64.isBase64(config.getKeyContent())) {
+            payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath());
+        }
         if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) {
-            // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
             payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath());
         }
         if (StrUtil.isNotEmpty(config.getPrivateCertContent())) {
-            // weixin-pay-java 存在 BUG,无法直接设置内容,所以创建临时文件来解决
             payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath());
         }
 
@@ -67,11 +75,13 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         client.setConfig(payConfig);
     }
 
+    // ============ 支付相关 ==========
+
     @Override
-    protected PayOrderUnifiedRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
+    protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception {
         try {
             switch (config.getApiVersion()) {
-                case WxPayClientConfig.API_VERSION_V2:
+                case API_VERSION_V2:
                     return doUnifiedOrderV2(reqDTO);
                 case WxPayClientConfig.API_VERSION_V3:
                     return doUnifiedOrderV3(reqDTO);
@@ -79,7 +89,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
                     throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
             }
         } catch (WxPayException e) {
-            throw buildUnifiedOrderException(reqDTO, e);
+            String errorCode = getErrorCode(e);
+            String errorMessage = getErrorMessage(e);
+            return PayOrderRespDTO.build(errorCode, errorMessage,
+                    reqDTO.getOutTradeNo(), e.getXmlString());
         }
     }
 
@@ -89,8 +102,8 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
      * @param reqDTO 下单信息
      * @return 各支付渠道的返回结果
      */
-    protected abstract PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)
-            throws WxPayException;
+    protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO)
+            throws Exception;
 
     /**
      * 【V3】调用支付渠道,统一下单
@@ -98,87 +111,177 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
      * @param reqDTO 下单信息
      * @return 各支付渠道的返回结果
      */
-    protected abstract PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
+    protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO)
             throws WxPayException;
 
+    @Override
+    public PayOrderRespDTO doParseOrderNotify(Map<String, String> 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 doParseOrderNotifyV2(String body) throws WxPayException {
+        // 1. 解析回调
+        WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body);
+        // 2. 构建结果
+        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 doParseOrderNotifyV3(String body) throws WxPayException {
+        // 1. 解析回调
+        WxPayOrderNotifyV3Result response = client.parseOrderNotifyV3Result(body, null);
+        WxPayOrderNotifyV3Result.DecryptNotifyResult result = response.getResult();
+        // 2. 构建结果
+        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);
+    }
+
+    // ============ 退款相关 ==========
 
     @Override
-    public Object parseNotify(PayNotifyReqDTO rawNotify) {
-        log.info("[parseNotify][微信支付回调 data 数据: {}]", rawNotify.getBody());
+    protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
         try {
-            // 微信支付 v2 回调结果处理
             switch (config.getApiVersion()) {
-                case WxPayClientConfig.API_VERSION_V2:
-                    return parseOrderNotifyV2(rawNotify);
+                case API_VERSION_V2:
+                    return doUnifiedRefundV2(reqDTO);
                 case WxPayClientConfig.API_VERSION_V3:
-                    return parseOrderNotifyV3(rawNotify);
+                    return doUnifiedRefundV3(reqDTO);
                 default:
                     throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
             }
         } catch (WxPayException e) {
-            log.error("[parseNotify][rawNotify({}) 解析失败]", toJsonString(rawNotify), e);
-//            throw buildPayException(e);
-            throw new RuntimeException(e);
-            // TODO 芋艿:缺一个异常翻译
+            // todo 芋艿:异常的处理;
+//            throw buildUnifiedOrderException(null, e);
+            return null;
         }
     }
 
-    private PayOrderNotifyRespDTO parseOrderNotifyV2(PayNotifyReqDTO data) throws WxPayException {
-        WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data.getBody());
-        Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");
-        // 转换结果
-        return PayOrderNotifyRespDTO
-                .builder()
-                .orderExtensionNo(notifyResult.getOutTradeNo())
-                .channelOrderNo(notifyResult.getTransactionId())
-                .channelUserId(notifyResult.getOpenid())
-                .successTime(parseDateV2(notifyResult.getTimeEnd()))
-                .build();
-    }
-
-    private PayOrderNotifyRespDTO parseOrderNotifyV3(PayNotifyReqDTO data) throws WxPayException {
-        WxPayOrderNotifyV3Result notifyResult = client.parseOrderNotifyV3Result(data.getBody(), null);
-        WxPayOrderNotifyV3Result.DecryptNotifyResult result = notifyResult.getResult();
-        // 转换结果
-        Assert.isTrue(Objects.equals(notifyResult.getResult().getTradeState(), "SUCCESS"),
-                "支付结果非 SUCCESS");
-        return PayOrderNotifyRespDTO.builder()
-                .orderExtensionNo(result.getOutTradeNo())
-                .channelOrderNo(result.getTradeState())
-                .channelUserId(result.getPayer() != null ? result.getPayer().getOpenid() : null)
-                .successTime(parseDateV3(result.getSuccessTime()))
-                .build();
+    private PayRefundRespDTO doUnifiedRefundV2(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
+        // 1. 构建 WxPayRefundRequest 请求
+        WxPayRefundRequest request = new WxPayRefundRequest()
+                .setOutTradeNo(reqDTO.getOutTradeNo())
+                .setOutRefundNo(reqDTO.getOutRefundNo())
+                .setRefundFee(reqDTO.getRefundPrice())
+                .setRefundDesc(reqDTO.getReason())
+                .setTotalFee(reqDTO.getPayPrice())
+                .setNotifyUrl(reqDTO.getNotifyUrl());
+        // 2.1 执行请求
+        WxPayRefundResult response = client.refundV2(request);
+        // 2.2 创建返回结果
+        PayRefundRespDTO refund = new PayRefundRespDTO()
+                .setOutRefundNo(reqDTO.getOutRefundNo())
+                .setRawData(response);
+        if (Objects.equals("SUCCESS", response.getResultCode())) {
+            refund.setStatus(PayRefundStatusRespEnum.WAITING.getStatus())
+                    .setChannelRefundNo(response.getRefundId());
+        } else {
+            refund.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
+        }
+        // TODO 芋艿;异常的处理;
+        return refund;
     }
 
-    // ========== 各种工具方法 ==========
+    private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
+        // 1. 构建 WxPayRefundRequest 请求
+        WxPayRefundV3Request request = new WxPayRefundV3Request()
+                .setOutTradeNo(reqDTO.getOutTradeNo())
+                .setOutRefundNo(reqDTO.getOutRefundNo())
+                .setAmount(new WxPayRefundV3Request.Amount().setRefund(reqDTO.getRefundPrice())
+                        .setTotal(reqDTO.getPayPrice()).setCurrency("CNY"))
+                .setReason(reqDTO.getReason())
+                .setNotifyUrl(reqDTO.getNotifyUrl());
+        // 2.1 执行请求
+        WxPayRefundV3Result response = client.refundV3(request);
+        // 2.2 创建返回结果
+        PayRefundRespDTO refund = new PayRefundRespDTO()
+                .setOutRefundNo(reqDTO.getOutRefundNo())
+                .setRawData(response);
+        if (Objects.equals("SUCCESS", response.getStatus())) {
+            refund.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())
+                    .setChannelRefundNo(response.getRefundId())
+                    .setSuccessTime(parseDateV3(response.getSuccessTime()));
+        } else if (Objects.equals("PROCESSING", response.getStatus())) {
+            refund.setStatus(PayRefundStatusRespEnum.WAITING.getStatus())
+                    .setChannelRefundNo(response.getRefundId());
+        } else {
+            refund.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
+        }
+        // TODO 芋艿;异常的处理;
+        return refund;
+    }
 
-    /**
-     * 构建统一下单的异常
-     *
-     * 目的:将参数不正确等异常,转换成 {@link cn.iocoder.yudao.framework.common.exception.ServiceException} 业务异常
-     *
-     * @param reqDTO 请求
-     * @param e 微信的支付异常
-     * @return 转换后的异常
-     *
-     */
-    static Exception buildUnifiedOrderException(PayOrderUnifiedReqDTO reqDTO, WxPayException e) {
-        // 情况一:业务结果为 FAIL
-        if (Objects.equals(e.getResultCode(), "FAIL")) {
-            log.error("[buildUnifiedOrderException][request({}) 发起支付失败]", toJsonString(reqDTO), e);
-            if (Objects.equals(e.getErrCode(), "PARAM_ERROR")) {
-                throw invalidParamException(e.getErrCodeDes());
+    @Override
+    public PayRefundRespDTO parseRefundNotify(Map<String, String> 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()));
             }
-            throw exception(PayFrameworkErrorCodeConstants.ORDER_UNIFIED_ERROR, e.getErrCodeDes());
+        } catch (WxPayException e) {
+            log.error("[parseNotify][params({}) body({}) 解析失败]", params, body, e);
+            throw new RuntimeException(e);
+            // TODO 芋艿:缺一个异常翻译
+        }
+    }
+
+    @SuppressWarnings("DuplicatedCode")
+    private PayRefundRespDTO parseRefundNotifyV2(String body) throws WxPayException {
+        // 1. 解析回调
+        WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body);
+        WxPayRefundNotifyResult.ReqInfo responseResult = response.getReqInfo();
+        // 2. 构建结果
+        PayRefundRespDTO notify = new PayRefundRespDTO()
+                .setChannelRefundNo(responseResult.getRefundId())
+                .setOutRefundNo(responseResult.getOutRefundNo())
+                .setRawData(response);
+        if (Objects.equals("SUCCESS", responseResult.getRefundStatus())) {
+            notify.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())
+                    .setSuccessTime(parseDateV2B(responseResult.getSuccessTime()));
+        } else {
+            notify.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
         }
-        // 情况二:状态码结果为 FAIL
-        if (Objects.equals(e.getReturnCode(), "FAIL")) {
-            throw exception(PayFrameworkErrorCodeConstants.ORDER_UNIFIED_ERROR, e.getReturnMsg());
+        return notify;
+    }
+
+    @SuppressWarnings("DuplicatedCode")
+    private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException {
+        // 1. 解析回调
+        WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null);
+        WxPayRefundNotifyV3Result.DecryptNotifyResult responseResult = response.getResult();
+        // 2. 构建结果
+        PayRefundRespDTO notify = new PayRefundRespDTO()
+                .setChannelRefundNo(responseResult.getRefundId())
+                .setOutRefundNo(responseResult.getOutRefundNo())
+                .setRawData(response);
+        if (Objects.equals("SUCCESS", responseResult.getRefundStatus())) {
+            notify.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus())
+                    .setSuccessTime(parseDateV3(responseResult.getSuccessTime()));
+        } else {
+            notify.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus());
         }
-        // 情况三:系统异常,这里暂时不打,交给上层的 AbstractPayClient 统一打
-        return e;
+        return notify;
     }
 
+    // ========== 各种工具方法 ==========
+
     static String formatDateV2(LocalDateTime time) {
         return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN);
     }
@@ -187,6 +290,10 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN);
     }
 
+    static LocalDateTime parseDateV2B(String time) {
+        return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN);
+    }
+
     static String formatDateV3(LocalDateTime time) {
         return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN);
     }
@@ -195,4 +302,24 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
         return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN);
     }
 
+    static String getErrorCode(WxPayException e) {
+        if (StrUtil.isNotEmpty(e.getErrCode())) {
+            return e.getErrCode();
+        }
+        if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
+            return "CUSTOM_ERROR";
+        }
+        return e.getReturnCode();
+    }
+
+    static String getErrorMessage(WxPayException e) {
+        if (StrUtil.isNotEmpty(e.getErrCode())) {
+            return e.getErrCodeDes();
+        }
+        if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) {
+            return e.getCustomErrorMsg();
+        }
+        return e.getReturnMsg();
+    }
+
 }

+ 3 - 10
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxAppPayClient.java

@@ -1,9 +1,7 @@
 package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
 
+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.PayRefundUnifiedReqDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
 import com.github.binarywang.wxpay.constant.WxPayConstants;
 import com.github.binarywang.wxpay.exception.WxPayException;
@@ -21,17 +19,12 @@ public class WxAppPayClient extends AbstractWxPayClient {
     }
 
     @Override
-    protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
+    protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
         return null;
     }
 
     @Override
-    protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
-        return null;
-    }
-
-    @Override
-    protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+    protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
         return null;
     }
 

+ 14 - 26
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClient.java

@@ -4,12 +4,8 @@ import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.thread.ThreadUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
+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.PayRefundUnifiedReqDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
 import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
@@ -23,6 +19,7 @@ import java.time.LocalDateTime;
 import java.util.concurrent.TimeUnit;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 
 /**
  * 微信支付【付款码支付】的 PayClient 实现类
@@ -49,7 +46,7 @@ public class WxBarPayClient extends AbstractWxPayClient {
     }
 
     @Override
-    protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+    protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
         // 由于付款码需要不断轮询,所以需要在较短的时间完成支付
         LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE);
         if (expireTime.isAfter(reqDTO.getExpireTime())) {
@@ -57,29 +54,25 @@ public class WxBarPayClient extends AbstractWxPayClient {
         }
         // 构建 WxPayMicropayRequest 对象
         WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
-                .outTradeNo(reqDTO.getMerchantOrderId())
+                .outTradeNo(reqDTO.getOutTradeNo())
                 .body(reqDTO.getSubject())
                 .detail(reqDTO.getBody())
-                .totalFee(reqDTO.getAmount()) // 单位分
+                .totalFee(reqDTO.getPrice()) // 单位分
                 .timeExpire(formatDateV2(expireTime))
                 .spbillCreateIp(reqDTO.getUserIp())
                 .authCode(getAuthCode(reqDTO))
                 .build();
         // 执行请求,重试直到失败(过期),或者成功
+        WxPayException lastWxPayException = null;
         for (int i = 1; i < Byte.MAX_VALUE; i++) {
             try {
                 WxPayMicropayResult response = client.micropay(request);
-                // 支付成功(例如说,用户输入了密码)
-                PayOrderNotifyRespDTO notify = PayOrderNotifyRespDTO.builder()
-                        .orderExtensionNo(response.getOutTradeNo())
-                        .channelOrderNo(response.getTransactionId())
-                        .channelUserId(response.getOpenid())
-                        .successTime(parseDateV2(response.getTimeEnd()))
-                        .build();
-                return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.BAR_CODE.getMode(),
-                        JsonUtils.toJsonString(response))
-                        .setNotify(notify);
+                // 支付成功,例如说:1)用户输入了密码;2)
+                return new PayOrderRespDTO(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()),
+                        response.getOutTradeNo(), response)
+                        .setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode());
             } catch (WxPayException ex) {
+                lastWxPayException = ex;
                 // 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理
                 // 1. SYSTEMERROR:接口返回错误:请立即调用被扫订单结果查询API,查询当前订单状态,并根据订单的状态决定下一步的操作。
                 // 2. USERPAYING:用户支付中,需要输入密码:等待 5 秒,然后调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。
@@ -89,23 +82,18 @@ public class WxBarPayClient extends AbstractWxPayClient {
                 }
                 // 等待 5 秒,继续下一轮重新发起支付
                 log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i,
-                        JsonUtils.toJsonString(request), ex.getMessage());
+                        toJsonString(request), ex.getMessage());
                 ThreadUtil.sleep(5, TimeUnit.SECONDS);
             }
         }
-        throw new IllegalStateException("微信 Bar 支付,重试多次失败");
+        throw lastWxPayException;
     }
 
     @Override
-    protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+    protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
         return doUnifiedOrderV2(reqDTO);
     }
 
-    @Override
-    protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
-        return null;
-    }
-
     // ========== 各种工具方法 ==========
 
     static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) {

+ 3 - 10
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxH5PayClient.java

@@ -1,9 +1,7 @@
 package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
 
+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.PayRefundUnifiedReqDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
 import com.github.binarywang.wxpay.constant.WxPayConstants;
 import com.github.binarywang.wxpay.exception.WxPayException;
@@ -21,17 +19,12 @@ public class WxH5PayClient extends AbstractWxPayClient {
     }
 
     @Override
-    protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
+    protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
         return null;
     }
 
     @Override
-    protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
-        return null;
-    }
-
-    @Override
-    protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+    protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
         return null;
     }
 

+ 18 - 25
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClient.java

@@ -1,9 +1,7 @@
 package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
 
+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.PayRefundUnifiedReqDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
 import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult;
@@ -34,14 +32,14 @@ public class WxNativePayClient extends AbstractWxPayClient {
     }
 
     @Override
-    protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+    protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
         // 构建 WxPayUnifiedOrderRequest 对象
         WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
-                .outTradeNo(reqDTO.getMerchantOrderId())
+                .outTradeNo(reqDTO.getOutTradeNo())
                 .body(reqDTO.getSubject())
                 .detail(reqDTO.getBody())
-                .totalFee(reqDTO.getAmount()) // 单位分
-                .productId(reqDTO.getMerchantOrderId())
+                .totalFee(reqDTO.getPrice()) // 单位分
+                .productId(reqDTO.getOutTradeNo())
                 .timeExpire(formatDateV2(reqDTO.getExpireTime()))
                 .spbillCreateIp(reqDTO.getUserIp())
                 .notifyUrl(reqDTO.getNotifyUrl())
@@ -50,31 +48,26 @@ public class WxNativePayClient extends AbstractWxPayClient {
         WxPayNativeOrderResult response = client.createOrder(request);
 
         // 转换结果
-        return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.QR_CODE_URL.getMode(),
-                response.getCodeUrl());
+        return new PayOrderRespDTO(PayOrderDisplayModeEnum.QR_CODE.getMode(), response.getCodeUrl(),
+                reqDTO.getOutTradeNo(), response);
     }
 
     @Override
-    protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+    protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
         // 构建 WxPayUnifiedOrderRequest 对象
-        WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
-        request.setOutTradeNo(reqDTO.getMerchantOrderId());
-        request.setDescription(reqDTO.getBody());
-        request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分
-        request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
-        request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
-        request.setNotifyUrl(reqDTO.getNotifyUrl());
+        WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request()
+                .setOutTradeNo(reqDTO.getOutTradeNo())
+                .setDescription(reqDTO.getSubject())
+                .setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())) // 单位分
+                .setTimeExpire(formatDateV3(reqDTO.getExpireTime()))
+                .setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()))
+                .setNotifyUrl(reqDTO.getNotifyUrl());
         // 执行请求
-        WxPayNativeOrderResult response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
+        String response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
 
         // 转换结果
-        return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.QR_CODE_URL.getMode(),
-                response.getCodeUrl());
-    }
-
-    @Override
-    protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable {
-        return null;
+        return new PayOrderRespDTO(PayOrderDisplayModeEnum.QR_CODE.getMode(), response,
+                reqDTO.getOutTradeNo(), response);
     }
 
 }

+ 11 - 15
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPayClientConfig.java

@@ -37,16 +37,17 @@ public class WxPayClientConfig implements PayClientConfig {
      *
      * 只有公众号或小程序需要该字段
      */
+    @NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class})
     private String appId;
     /**
      * 商户号
      */
-    @NotBlank(message = "商户号 不能为空", groups = {V2.class, V3.class})
+    @NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class})
     private String mchId;
     /**
      * API 版本
      */
-    @NotBlank(message = "API 版本 不能为空", groups = {V2.class, V3.class})
+    @NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class})
     private String apiVersion;
 
     // ========== V2 版本的参数 ==========
@@ -54,36 +55,31 @@ public class WxPayClientConfig implements PayClientConfig {
     /**
      * 商户密钥
      */
-    @NotBlank(message = "商户密钥 不能为空", groups = V2.class)
+    @NotBlank(message = "商户密钥不能为空", groups = V2.class)
     private String mchKey;
     /**
-     * apiclient_cert.p12 证书文件的绝对路径或者以 classpath: 开头的类路径.
-     * 对应的字符串
+     * apiclient_cert.p12 证书文件的对应字符串【base64 格式】
      *
-     * 注意,可通过 {@link #main(String[])} 读取
+     * 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储
      */
-    /// private String keyContent;
+    @NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class)
+    private String keyContent;
 
     // ========== V3 版本的参数 ==========
     /**
-     * apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
-     * 对应的字符串
-     * 注意,可通过 {@link #main(String[])} 读取
+     * apiclient_key.pem 证书文件的对应字符串
      */
     @NotBlank(message = "apiclient_key 不能为空", groups = V3.class)
     private String privateKeyContent;
     /**
-     * apiclient_cert.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
-     * 对应的字符串
-     * <p>
-     * 注意,可通过 {@link #main(String[])} 读取
+     * apiclient_cert.pem 证书文件的对应的字符串
      */
     @NotBlank(message = "apiclient_cert 不能为空", groups = V3.class)
     private String privateCertContent;
     /**
      * apiV3 密钥值
      */
-    @NotBlank(message = "apiV3 密钥值 不能为空", groups = V3.class)
+    @NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class)
     private String apiV3Key;
 
     /**

+ 12 - 20
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxPubPayClient.java

@@ -2,11 +2,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
 
 import cn.hutool.core.map.MapUtil;
 import cn.hutool.core.util.StrUtil;
-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.order.PayOrderUnifiedRespDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedRespDTO;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderDisplayModeEnum;
 import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
@@ -19,6 +16,7 @@ import com.github.binarywang.wxpay.exception.WxPayException;
 import lombok.extern.slf4j.Slf4j;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 
 /**
  * 微信支付(公众号)的 PayClient 实现类
@@ -44,13 +42,13 @@ public class WxPubPayClient extends AbstractWxPayClient {
     }
 
     @Override
-    protected PayOrderUnifiedRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+    protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
         // 构建 WxPayUnifiedOrderRequest 对象
         WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
-                .outTradeNo(reqDTO.getMerchantOrderId())
+                .outTradeNo(reqDTO.getOutTradeNo())
                 .body(reqDTO.getSubject())
                 .detail(reqDTO.getBody())
-                .totalFee(reqDTO.getAmount()) // 单位分
+                .totalFee(reqDTO.getPrice()) // 单位分
                 .timeExpire(formatDateV2(reqDTO.getExpireTime()))
                 .spbillCreateIp(reqDTO.getUserIp())
                 .openid(getOpenid(reqDTO))
@@ -60,17 +58,17 @@ public class WxPubPayClient extends AbstractWxPayClient {
         WxPayMpOrderResult response = client.createOrder(request);
 
         // 转换结果
-        return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.CUSTOM.getMode(),
-                JsonUtils.toJsonString(response));
+        return new PayOrderRespDTO(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
+                reqDTO.getOutTradeNo(), response);
     }
 
     @Override
-    protected PayOrderUnifiedRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+    protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
         // 构建 WxPayUnifiedOrderRequest 对象
         WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
-        request.setOutTradeNo(reqDTO.getMerchantOrderId());
+        request.setOutTradeNo(reqDTO.getOutTradeNo());
         request.setDescription(reqDTO.getSubject());
-        request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分
+        request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分
         request.setTimeExpire(formatDateV3(reqDTO.getExpireTime()));
         request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
         request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
@@ -79,14 +77,8 @@ public class WxPubPayClient extends AbstractWxPayClient {
         WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request);
 
         // 转换结果
-        return new PayOrderUnifiedRespDTO(PayOrderDisplayModeEnum.CUSTOM.getMode(),
-                JsonUtils.toJsonString(response));
-    }
-
-    @Override
-    protected PayRefundUnifiedRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
-        // TODO 需要实现
-        throw new UnsupportedOperationException();
+        return new PayOrderRespDTO(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response),
+                reqDTO.getOutTradeNo(), response);
     }
 
     // ========== 各种工具方法 ==========

+ 0 - 16
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java

@@ -1,16 +0,0 @@
-package cn.iocoder.yudao.framework.pay.core.enums;
-
-import cn.iocoder.yudao.framework.common.exception.ErrorCode;
-
-/**
- * 支付框架的错误码枚举
- *
- * 支付框架,使用 2-002-000-000 段
- *
- * @author 芋道源码
- */
-public interface PayFrameworkErrorCodeConstants {
-
-    ErrorCode ORDER_UNIFIED_ERROR = new ErrorCode(2002000000, "发起支付失败,原因:{}");
-
-}

+ 1 - 2
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java

@@ -18,8 +18,7 @@ public enum PayOrderDisplayModeEnum {
     QR_CODE("qr_code"), // 二维码的文字内容
     QR_CODE_URL("qr_code_url"), // 二维码的图片链接
     BAR_CODE("bar_code"), // 条形码
-    APP("app"), // 应用【目前暂时用不到】
-    CUSTOM("custom"), // 自定义:每种支付方式,做个性化处理;例如说,微信公众号支付时,调用 JSAPI 接口
+    APP("app"), // 应用:Android、iOS、微信小程序、微信公众号等,需要做自定义处理的
     ;
 
     /**

+ 23 - 1
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/order/PayOrderStatusRespEnum.java

@@ -3,10 +3,12 @@ package cn.iocoder.yudao.framework.pay.core.enums.order;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 
+import java.util.Objects;
+
 /**
  * 渠道的支付状态枚举
  *
- * @author 遇到源码
+ * @author 芋道源码
  */
 @Getter
 @AllArgsConstructor
@@ -20,4 +22,24 @@ public enum PayOrderStatusRespEnum {
     private final Integer status;
     private final String name;
 
+    /**
+     * 判断是否支付成功
+     *
+     * @param status 状态
+     * @return 是否支付成功
+     */
+    public static boolean isSuccess(Integer status) {
+        return Objects.equals(status, SUCCESS.getStatus());
+    }
+
+    /**
+     * 判断是否支付关闭
+     *
+     * @param status 状态
+     * @return 是否支付关闭
+     */
+    public static boolean isClosed(Integer status) {
+        return Objects.equals(status, CLOSED.getStatus());
+    }
+
 }

+ 0 - 23
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayNotifyRefundStatusEnum.java

@@ -1,23 +0,0 @@
-package cn.iocoder.yudao.framework.pay.core.enums.refund;
-
-// TODO 芋艿:看看能不能去掉
-/**
- * 退款通知, 统一的渠道退款状态
- *
- * @author  jason
- */
-public enum PayNotifyRefundStatusEnum {
-
-    /**
-     * 支付宝 中 全额退款 trade_status=TRADE_CLOSED, 部分退款 trade_status=TRADE_SUCCESS
-     * 退款成功
-     */
-    SUCCESS,
-
-    /**
-     * 支付宝退款通知没有这个状态
-     * 退款异常
-     */
-    ABNORMAL;
-
-}

+ 0 - 23
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundRespEnum.java

@@ -1,23 +0,0 @@
-package cn.iocoder.yudao.framework.pay.core.enums.refund;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-
-/**
- * 渠道的退款状态枚举
- *
- * @author jason
- */
-@Getter
-@AllArgsConstructor
-public enum PayRefundRespEnum {
-
-    SUCCESS(1, "退款成功"),
-    FAILURE(2, "退款失败"),
-    PROCESSING(3,"退款处理中"),
-    CLOSED(4, "退款关闭");
-
-    private final Integer status;
-    private final String name;
-
-}

+ 28 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.pay.core.enums.refund;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Objects;
+
+/**
+ * 渠道的退款状态枚举
+ *
+ * @author jason
+ */
+@Getter
+@AllArgsConstructor
+public enum PayRefundStatusRespEnum {
+
+    WAITING(0, "未退款"),
+    SUCCESS(10, "退款成功"),
+    FAILURE(20, "退款失败");
+
+    private final Integer status;
+    private final String name;
+
+    public static boolean isSuccess(Integer status) {
+        return Objects.equals(status, SUCCESS.getStatus());
+    }
+
+}

+ 2 - 2
yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java

@@ -121,10 +121,10 @@ public class PayClientFactoryImplIntegrationTest {
 
     private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() {
         PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO();
-        reqDTO.setAmount(123);
+        reqDTO.setPrice(123);
         reqDTO.setSubject("IPhone 13");
         reqDTO.setBody("biubiubiu");
-        reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
+        reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
         reqDTO.setUserIp("127.0.0.1");
         reqDTO.setNotifyUrl("http://127.0.0.1:8080");
         return reqDTO;

+ 2 - 4
yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java

@@ -1,8 +1,6 @@
 package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
 import cn.hutool.core.util.ReflectUtil;
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
 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.test.core.ut.BaseMockitoUnitTest;
 import com.alipay.api.AlipayApiException;
 import com.alipay.api.DefaultAlipayClient;
@@ -74,8 +72,8 @@ public class AlipayQrPayClientTest extends BaseMockitoUnitTest {
         // 这里,设置可以直接随机整个对象。
         Long shopOrderId = System.currentTimeMillis();
         PayOrderUnifiedReqDTO reqDTO=new PayOrderUnifiedReqDTO();
-        reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
-        reqDTO.setAmount(1);
+        reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis()));
+        reqDTO.setPrice(1);
         reqDTO.setBody("内容:" + shopOrderId);
         reqDTO.setSubject("标题:"+shopOrderId);
         String notify="http://niubi.natapp1.cc/api/pay/order/notify";

+ 123 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java

@@ -0,0 +1,123 @@
+package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
+
+import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
+import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest;
+import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest;
+import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
+import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult;
+import com.github.binarywang.wxpay.bean.result.WxPayRefundResult;
+import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+
+import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV2;
+
+/**
+ * {@link WxBarPayClient} 的集成测试,用于快速调试微信条码支付
+ *
+ * @author 芋道源码
+ */
+@Disabled
+public class WxBarPayClientIntegrationTest {
+
+    @Test
+    public void testPayV2() throws WxPayException {
+        // 创建 config 配置
+        WxPayConfig config = buildWxPayConfigV2();
+        // 创建 WxPayService 客户端
+        WxPayService client = new WxPayServiceImpl();
+        client.setConfig(config);
+
+        // 执行发起支付
+        WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder()
+                .outTradeNo(String.valueOf(System.currentTimeMillis()))
+                .body("测试支付-body")
+                .detail("测试支付-detail")
+                .totalFee(1) // 单位分
+                .timeExpire(formatDateV2(LocalDateTimeUtils.addTime(Duration.ofMinutes(2))))
+                .spbillCreateIp("127.0.0.1")
+                .authCode("134298744426278497")
+                .build();
+        System.out.println("========= request ==========");
+        System.out.println(JsonUtils.toJsonPrettyString(request));
+        WxPayMicropayResult response = client.micropay(request);
+        System.out.println("========= response ==========");
+        System.out.println(JsonUtils.toJsonPrettyString(response));
+    }
+
+    @Test
+    public void testParseRefundNotifyV2() throws WxPayException {
+        // 创建 config 配置
+        WxPayConfig config = buildWxPayConfigV2();
+        // 创建 WxPayService 客户端
+        WxPayService client = new WxPayServiceImpl();
+        client.setConfig(config);
+
+        // 执行解析
+        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>";
+        WxPayRefundNotifyResult response = client.parseRefundNotifyResult(xml);
+        System.out.println(response.getReqInfo());
+    }
+
+    @Test
+    public void testRefundV2() throws WxPayException {
+        // 创建 config 配置
+        WxPayConfig config = buildWxPayConfigV2();
+        // 创建 WxPayService 客户端
+        WxPayService client = new WxPayServiceImpl();
+        client.setConfig(config);
+
+        // 执行发起退款
+        WxPayRefundRequest request = new WxPayRefundRequest()
+                .setOutTradeNo("1689545667276")
+                .setOutRefundNo(String.valueOf(System.currentTimeMillis()))
+                .setRefundFee(1)
+                .setRefundDesc("就是想退了")
+                .setTotalFee(1);
+        System.out.println("========= request ==========");
+        System.out.println(JsonUtils.toJsonPrettyString(request));
+        WxPayRefundResult response = client.refund(request);
+        System.out.println("========= response ==========");
+        System.out.println(JsonUtils.toJsonPrettyString(response));
+    }
+
+    @Test
+    public void testRefundV3() throws WxPayException {
+        // 创建 config 配置
+        WxPayConfig config = buildWxPayConfigV2();
+        // 创建 WxPayService 客户端
+        WxPayService client = new WxPayServiceImpl();
+        client.setConfig(config);
+
+        // 执行发起退款
+        WxPayRefundV3Request request = new WxPayRefundV3Request()
+                .setOutTradeNo("1689506325635")
+                .setOutRefundNo(String.valueOf(System.currentTimeMillis()))
+                .setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY"))
+                .setReason("就是想退了");
+        System.out.println("========= request ==========");
+        System.out.println(JsonUtils.toJsonPrettyString(request));
+        WxPayRefundV3Result response = client.refundV3(request);
+        System.out.println("========= response ==========");
+        System.out.println(JsonUtils.toJsonPrettyString(response));
+    }
+
+    private WxPayConfig buildWxPayConfigV2() {
+        WxPayConfig config = new WxPayConfig();
+        config.setAppId("wx62056c0d5e8db250");
+        config.setMchId("1545083881");
+        config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim");
+//        config.setSignType(WxPayConstants.SignType.MD5);
+        config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12");
+        return config;
+    }
+
+}

+ 83 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/test/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java

@@ -0,0 +1,83 @@
+package cn.iocoder.yudao.framework.pay.core.client.impl.weixin;
+
+import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
+import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result;
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+
+import static cn.iocoder.yudao.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV3;
+
+/**
+ * {@link WxNativePayClient} 的集成测试,用于快速调试微信扫码支付
+ *
+ * @author 芋道源码
+ */
+@Disabled
+public class WxNativePayClientIntegrationTest {
+
+    @Test
+    public void testPayV3() throws WxPayException {
+        // 创建 config 配置
+        WxPayConfig config = buildWxPayConfigV3();
+        // 创建 WxPayService 客户端
+        WxPayService client = new WxPayServiceImpl();
+        client.setConfig(config);
+
+        // 执行发起支付
+        WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request()
+                .setOutTradeNo(String.valueOf(System.currentTimeMillis()))
+                .setDescription("测试支付-body")
+                .setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(1)) // 单位分
+                .setTimeExpire(formatDateV3(LocalDateTimeUtils.addTime(Duration.ofMinutes(2))))
+                .setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp("127.0.0.1"))
+                .setNotifyUrl("http://127.0.0.1:48080");
+        System.out.println("========= request ==========");
+        System.out.println(JsonUtils.toJsonPrettyString(request));
+        String response = client.createOrderV3(TradeTypeEnum.NATIVE, request);
+        System.out.println("========= response ==========");
+        System.out.println(JsonUtils.toJsonPrettyString(response));
+    }
+
+    @Test
+    public void testRefundV3() throws WxPayException {
+        // 创建 config 配置
+        WxPayConfig config = buildWxPayConfigV3();
+        // 创建 WxPayService 客户端
+        WxPayService client = new WxPayServiceImpl();
+        client.setConfig(config);
+
+        // 执行发起退款
+        WxPayRefundV3Request request = new WxPayRefundV3Request()
+                .setOutTradeNo("1689545729695")
+                .setOutRefundNo(String.valueOf(System.currentTimeMillis()))
+                .setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY"))
+                .setReason("就是想退了");
+        System.out.println("========= request ==========");
+        System.out.println(JsonUtils.toJsonPrettyString(request));
+        WxPayRefundV3Result response = client.refundV3(request);
+        System.out.println("========= response ==========");
+        System.out.println(JsonUtils.toJsonPrettyString(response));
+    }
+
+    private WxPayConfig buildWxPayConfigV3() {
+        WxPayConfig config = new WxPayConfig();
+        config.setAppId("wx62056c0d5e8db250");
+        config.setMchId("1545083881");
+        config.setApiV3Key("459arNsYHl1mgkiO6H9ZH5KkhFXSxaA4");
+//        config.setCertSerialNo(serialNo);
+        config.setPrivateCertPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem");
+        config.setPrivateKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_key.pem");
+        return config;
+    }
+
+}

+ 1 - 1
yudao-module-mall/yudao-module-trade-biz/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/TradeAfterSaleServiceImpl.java

@@ -351,7 +351,7 @@ public class TradeAfterSaleServiceImpl implements TradeAfterSaleService, AfterSa
             public void afterCommit() {
                 // 创建退款单
                 PayRefundCreateReqDTO createReqDTO = TradeAfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties);
-                Long payRefundId = payRefundApi.createPayRefund(createReqDTO);
+                Long payRefundId = payRefundApi.createRefund(createReqDTO);
                 // 更新售后单的退款单号
                 tradeAfterSaleMapper.updateById(new TradeAfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId));
             }

+ 2 - 2
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApi.java

@@ -18,7 +18,7 @@ public interface PayRefundApi {
      * @param reqDTO 创建请求
      * @return 退款单编号
      */
-    Long createPayRefund(@Valid PayRefundCreateReqDTO reqDTO);
+    Long createRefund(@Valid PayRefundCreateReqDTO reqDTO);
 
     /**
      * 获得退款单
@@ -26,6 +26,6 @@ public interface PayRefundApi {
      * @param id 退款单编号
      * @return 退款单
      */
-    PayRefundRespDTO getPayRefund(Long id);
+    PayRefundRespDTO getRefund(Long id);
 
 }

+ 13 - 7
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java

@@ -27,26 +27,32 @@ public class PayRefundCreateReqDTO {
     private String userIp;
 
     // ========== 商户相关字段 ==========
+    /**
+     * 商户订单编号
+     */
+    @NotEmpty(message = "商户订单编号不能为空")
+    private String merchantOrderId;
+
+    /**
+     * 商户退款编号
+     */
+    @NotEmpty(message = "商户退款编号不能为空")
+    private String merchantRefundId;
 
     /**
      * 退款描述
      */
     @NotEmpty(message = "退款描述不能为空")
-    @Length(max = 128, message = "退款描述长度不能超过128")
+    @Length(max = 128, message = "退款描述长度不能超过 128")
     private String reason;
 
     // ========== 订单相关字段 ==========
 
-    /**
-     * 支付单号
-     */
-    @NotNull(message = "支付单号不能为空")
-    private Long payOrderId;
-
     /**
      * 退款金额,单位:分
      */
     @NotNull(message = "退款金额不能为空")
     @Min(value = 1, message = "退款金额必须大于零")
     private Integer price;
+
 }

+ 5 - 3
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/ErrorCodeConstants.java

@@ -27,17 +27,19 @@ public interface ErrorCodeConstants {
     ErrorCode PAY_ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1007002001, "支付订单不处于待支付");
     ErrorCode PAY_ORDER_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007002002, "支付订单不处于已支付");
     ErrorCode PAY_ORDER_IS_EXPIRED = new ErrorCode(1007002003, "支付订单已经过期");
+    ErrorCode PAY_ORDER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1007002004, "发起支付报错,错误码:{},错误提示:{}");
 
     // ========== ORDER 模块(拓展单) 1007003000 ==========
     ErrorCode PAY_ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在");
     ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付");
 
     // ========== 支付模块(退款) 1007006000 ==========
-    ErrorCode PAY_PRICE_PRICE_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额");
+    ErrorCode PAY_REFUND_PRICE_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额");
     ErrorCode PAY_REFUND_ALL_REFUNDED = new ErrorCode(1007006001, "订单已经全额退款");
-    ErrorCode PAY_REFUND_CHN_ORDER_NO_IS_NULL = new ErrorCode(1007006002, "该订单的渠道订单为空");
-    ErrorCode PAY_REFUND_SUCCEED = new ErrorCode(1007006003, "已经退款成功");
+    ErrorCode PAY_REFUND_HAS_REFUNDING = new ErrorCode(1007006002, "已经有退款在处理中");
+    ErrorCode PAY_REFUND_EXISTS = new ErrorCode(1007006003, "已经存在退款单");
     ErrorCode PAY_REFUND_NOT_FOUND = new ErrorCode(1007006004, "支付退款单不存在");
+    ErrorCode PAY_REFUND_STATUS_IS_NOT_WAITING = new ErrorCode(1007006005, "支付退款单不处于待退款");
 
     // ========== 示例订单 1007900000 ==========
     ErrorCode PAY_DEMO_ORDER_NOT_FOUND = new ErrorCode(1007900000, "示例订单不存在");

+ 10 - 0
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderStatusEnum.java

@@ -38,4 +38,14 @@ public enum PayOrderStatusEnum implements IntArrayValuable {
         return Objects.equals(status, SUCCESS.getStatus());
     }
 
+    /**
+     * 判断是否支付关闭
+     *
+     * @param status 状态
+     * @return 是否支付关闭
+     */
+    public static boolean isClosed(Integer status) {
+        return Objects.equals(status, CLOSED.getStatus());
+    }
+
 }

+ 8 - 4
yudao-module-pay/yudao-module-pay-api/src/main/java/cn/iocoder/yudao/module/pay/enums/refund/PayRefundStatusEnum.java

@@ -5,14 +5,18 @@ import lombok.Getter;
 
 import java.util.Objects;
 
+/**
+ * 渠道的退款状态枚举
+ *
+ * @author 芋道源码
+ */
 @Getter
 @AllArgsConstructor
 public enum PayRefundStatusEnum {
 
-    CREATE(0, "退款订单生成"),
-    SUCCESS(1, "退款成功"),
-    FAILURE(2, "退款失败"),
-    CLOSE(99, "退款关闭");
+    WAITING(0, "未退款"),
+    SUCCESS(10, "退款成功"),
+    FAILURE(20, "退款失败");
 
     private final Integer status;
     private final String name;

+ 1 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/order/PayOrderApiImpl.java

@@ -22,7 +22,7 @@ public class PayOrderApiImpl implements PayOrderApi {
 
     @Override
     public Long createOrder(PayOrderCreateReqDTO reqDTO) {
-        return payOrderService.createPayOrder(reqDTO);
+        return payOrderService.createOrder(reqDTO);
     }
 
     @Override

+ 4 - 4
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/api/refund/PayRefundApiImpl.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.pay.api.refund;
 
 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
+import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
@@ -21,14 +22,13 @@ public class PayRefundApiImpl implements PayRefundApi {
     private PayRefundService payRefundService;
 
     @Override
-    public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
+    public Long createRefund(PayRefundCreateReqDTO reqDTO) {
         return payRefundService.createPayRefund(reqDTO);
     }
 
     @Override
-    public PayRefundRespDTO getPayRefund(Long id) {
-        // TODO 芋艿:暂未实现
-        return null;
+    public PayRefundRespDTO getRefund(Long id) {
+        return PayRefundConvert.INSTANCE.convert02(payRefundService.getRefund(id));
     }
 
 }

+ 1 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/app/vo/PayAppBaseVO.java

@@ -29,7 +29,7 @@ public class PayAppBaseVO {
     @Schema(description = "支付结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/pay-callback")
     @NotNull(message = "支付结果的回调地址不能为空")
     @URL(message = "支付结果的回调地址必须为 URL 格式")
-    private String payNotifyUrl;
+    private String orderNotifyUrl;
 
     @Schema(description = "退款结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/refund-callback")
     @NotNull(message = "退款结果的回调地址不能为空")

+ 30 - 31
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/notify/PayNotifyController.java

@@ -3,9 +3,8 @@ package cn.iocoder.yudao.module.pay.controller.admin.notify;
 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
 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.notify.PayNotifyReqDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
 import cn.iocoder.yudao.module.pay.service.order.PayOrderService;
 import cn.iocoder.yudao.module.pay.service.refund.PayRefundService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -19,7 +18,6 @@ import javax.annotation.security.PermitAll;
 import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND;
 
 @Tag(name = "管理后台 - 支付通知")
@@ -37,22 +35,14 @@ public class PayNotifyController {
     @Resource
     private PayClientFactory payClientFactory;
 
-    /**
-     * 统一的渠道支付回调,支付宝的退款回调
-     *
-     * @param channelId 渠道编号
-     * @param params form 参数
-     * @param body request body
-     * @return 成功返回 "success"
-     */
-    @PostMapping(value = "/callback/{channelId}")
-    @Operation(summary = "支付渠道的统一回调接口 - 包括支付回调,退款回调")
+    @PostMapping(value = "/order/{channelId}")
+    @Operation(summary = "支付渠道的统一【支付】回调")
     @PermitAll
     @OperateLog(enable = false) // 回调地址,无需记录操作日志
-    public String notifyCallback(@PathVariable("channelId") Long channelId,
-                                 @RequestParam(required = false) Map<String, String> params,
-                                 @RequestBody(required = false) String body) {
-        log.info("[notifyCallback][channelId({}) 回调数据({}/{})]", channelId, params, body);
+    public String notifyOrder(@PathVariable("channelId") Long channelId,
+                              @RequestParam(required = false) Map<String, String> params,
+                              @RequestBody(required = false) String body) {
+        log.info("[notifyOrder][channelId({}) 回调数据({}/{})]", channelId, params, body);
         // 1. 校验支付渠道是否存在
         PayClient payClient = payClientFactory.getPayClient(channelId);
         if (payClient == null) {
@@ -61,21 +51,30 @@ public class PayNotifyController {
         }
 
         // 2. 解析通知数据
-        PayNotifyReqDTO rawNotify = PayNotifyReqDTO.builder().params(params).body(body).build();
-        Object notify = payClient.parseNotify(rawNotify);
+        PayOrderRespDTO notify = payClient.parseOrderNotify(params, body);
+        orderService.notifyOrder(channelId, notify);
+        return "success";
+    }
 
-        // 3. 处理通知
-        // 3.1:退款通知
-        if (notify instanceof PayRefundNotifyRespDTO) {
-            refundService.notifyPayRefund(channelId, (PayRefundNotifyRespDTO) notify, rawNotify);
-            return "success";
-        }
-        // 3.2:支付通知
-        if (notify instanceof PayOrderNotifyRespDTO) {
-            orderService.notifyPayOrder(channelId, (PayOrderNotifyRespDTO) notify, rawNotify);
-            return "success";
+    @PostMapping(value = "/refund/{channelId}")
+    @Operation(summary = "支付渠道的统一【退款】回调")
+    @PermitAll
+    @OperateLog(enable = false) // 回调地址,无需记录操作日志
+    public String notifyRefund(@PathVariable("channelId") Long channelId,
+                              @RequestParam(required = false) Map<String, String> 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);
         }
-        throw new UnsupportedOperationException("未知通知:" + toJsonString(notify));
+
+        // 2. 解析通知数据
+        PayRefundRespDTO notify = payClient.parseRefundNotify(params, body);
+        refundService.notifyRefund(channelId, notify);
+        return "success";
     }
 
 }

+ 1 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/PayOrderController.java

@@ -86,7 +86,7 @@ public class PayOrderController {
     @PostMapping("/submit")
     @Operation(summary = "提交支付订单")
     public CommonResult<PayOrderSubmitRespVO> submitPayOrder(@RequestBody PayOrderSubmitReqVO reqVO) {
-        PayOrderSubmitRespVO respVO = payOrderService.submitPayOrder(reqVO, getClientIP());
+        PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP());
         return success(respVO);
     }
 

+ 0 - 3
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java

@@ -76,9 +76,6 @@ public class PayRefundExcelVO {
     @ExcelProperty("退款成功时间")
     private LocalDateTime successTime;
 
-    @ExcelProperty("退款通知时间")
-    private LocalDateTime notifyTime;
-
     @ExcelProperty("退款失效时间")
     private LocalDateTime expireTime;
 

+ 1 - 1
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java

@@ -40,7 +40,7 @@ public class AppPayOrderController {
     @PostMapping("/submit")
     @Operation(summary = "提交支付订单")
     public CommonResult<AppPayOrderSubmitRespVO> submitPayOrder(@RequestBody AppPayOrderSubmitReqVO reqVO) {
-        PayOrderSubmitRespVO respVO = payOrderService.submitPayOrder(reqVO, getClientIP());
+        PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP());
         return success(PayOrderConvert.INSTANCE.convert3(respVO));
     }
 

+ 2 - 2
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java

@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.pay.convert.order;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 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.module.pay.api.order.dto.PayOrderCreateReqDTO;
 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.*;
@@ -93,7 +92,8 @@ public interface PayOrderConvert {
 
     PayOrderUnifiedReqDTO convert2(PayOrderSubmitReqVO reqVO, String userIp);
 
-    PayOrderSubmitRespVO convert(PayOrderDO order, PayOrderUnifiedRespDTO unifiedRespDTO);
+    @Mapping(source = "order.status", target = "status")
+    PayOrderSubmitRespVO convert(PayOrderDO order, cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO respDTO);
 
     AppPayOrderSubmitRespVO convert3(PayOrderSubmitRespVO bean);
 

+ 8 - 17
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/convert/refund/PayRefundConvert.java

@@ -1,12 +1,11 @@
 package cn.iocoder.yudao.module.pay.convert.refund;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
+import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundRespDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.*;
-import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
 import org.mapstruct.Mapper;
-import org.mapstruct.Mapping;
-import org.mapstruct.Mappings;
 import org.mapstruct.factory.Mappers;
 
 import java.math.BigDecimal;
@@ -44,8 +43,6 @@ public interface PayRefundConvert {
 
     PageResult<PayRefundRespVO> convertPage(PageResult<PayRefundDO> page);
 
-    List<PayRefundExcelVO> convertList02(List<PayRefundDO> list);
-
     /**
      * 退款订单DO 转 导出excel VO
      *
@@ -60,20 +57,18 @@ public interface PayRefundConvert {
         PayRefundExcelVO payRefundExcelVO = new PayRefundExcelVO();
 
         payRefundExcelVO.setId(bean.getId());
-        payRefundExcelVO.setTradeNo(bean.getTradeNo());
+        payRefundExcelVO.setTradeNo(bean.getNo());
         payRefundExcelVO.setMerchantOrderId(bean.getMerchantOrderId());
-        payRefundExcelVO.setMerchantRefundNo(bean.getMerchantRefundNo());
+        // TODO 芋艿:晚点在改
+//        payRefundExcelVO.setMerchantRefundNo(bean.getMerchantRefundNo());
         payRefundExcelVO.setNotifyUrl(bean.getNotifyUrl());
         payRefundExcelVO.setNotifyStatus(bean.getNotifyStatus());
         payRefundExcelVO.setStatus(bean.getStatus());
-        payRefundExcelVO.setType(bean.getType());
         payRefundExcelVO.setReason(bean.getReason());
         payRefundExcelVO.setUserIp(bean.getUserIp());
         payRefundExcelVO.setChannelOrderNo(bean.getChannelOrderNo());
         payRefundExcelVO.setChannelRefundNo(bean.getChannelRefundNo());
-        payRefundExcelVO.setExpireTime(bean.getExpireTime());
         payRefundExcelVO.setSuccessTime(bean.getSuccessTime());
-        payRefundExcelVO.setNotifyTime(bean.getNotifyTime());
         payRefundExcelVO.setCreateTime(bean.getCreateTime());
 
         BigDecimal multiple = new BigDecimal(100);
@@ -85,12 +80,8 @@ public interface PayRefundConvert {
         return payRefundExcelVO;
     }
 
-    //TODO 太多需要处理了, 暂时不用
-    @Mappings(value = {
-            @Mapping(source = "price", target = "payPrice"),
-            @Mapping(source = "id", target = "orderId"),
-            @Mapping(target = "status",ignore = true)
-    })
-    PayRefundDO convert(PayOrderDO orderDO);
+    PayRefundDO convert(PayRefundCreateReqDTO bean);
+
+    PayRefundRespDTO convert02(PayRefundDO bean);
 
 }

+ 1 - 1
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;
     /**
      * 退款结果的回调地址
      */

+ 5 - 4
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java

@@ -6,7 +6,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
-import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
+import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.*;
@@ -55,7 +55,8 @@ public class PayOrderDO extends BaseDO {
 
     /**
      * 商户订单编号
-     * 例如说,内部系统 A 的订单号。需要保证每个 PayMerchantDO 唯一
+     *
+     * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一
      */
     private String merchantOrderId;
     /**
@@ -126,7 +127,7 @@ public class PayOrderDO extends BaseDO {
     /**
      * 退款状态
      *
-     * 枚举 {@link PayRefundTypeEnum}
+     * 枚举 {@link PayOrderRefundStatusEnum}
      */
     private Integer refundStatus;
     /**
@@ -136,7 +137,7 @@ public class PayOrderDO extends BaseDO {
     /**
      * 退款总金额,单位:分
      */
-    private Long refundPrice;
+    private Integer refundPrice;
 
     // ========== 渠道相关字段 ==========
     /**

+ 19 - 8
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderExtensionDO.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.pay.dal.dataobject.order;
 
+import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
@@ -32,10 +33,11 @@ public class PayOrderExtensionDO extends BaseDO {
      */
     private Long id;
     /**
-     * 支付订单号,根据规则生成
-     * 调用支付渠道时,使用该字段作为对接的订单号。
-     * 1. 调用微信支付 https://api.mch.weixin.qq.com/pay/unifiedorder 时,使用该字段作为 out_trade_no
-     * 2. 调用支付宝 https://opendocs.alipay.com/apis 时,使用该字段作为 out_trade_no
+     * 外部订单号,根据规则生成
+     *
+     * 调用支付渠道时,使用该字段作为对接的订单号:
+     * 1. 微信支付:对应 <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml">JSAPI 支付</a> 的 out_trade_no 字段
+     * 2. 支付宝支付:对应 <a href="https://opendocs.alipay.com/open/270/105898">电脑网站支付</a> 的 out_trade_no 字段
      *
      * 例如说,P202110132239124200055
      */
@@ -64,20 +66,29 @@ public class PayOrderExtensionDO extends BaseDO {
      * 支付状态
      *
      * 枚举 {@link PayOrderStatusEnum}
-     * 注意,只包含上述枚举的 WAITING 和 SUCCESS
      */
     private Integer status;
     /**
      * 支付渠道的额外参数
      *
-     * 参见 https://www.pingxx.com/api/支付渠道%20extra%20参数说明.html
+     * 参见 <a href="https://www.pingxx.com/api/支付渠道%20extra%20参数说明.html">参数说明</>
      */
     @TableField(typeHandler = JacksonTypeHandler.class)
     private Map<String, String> channelExtras;
+
+    /**
+     * 调用渠道的错误码
+     */
+    private String channelErrorCode;
+    /**
+     * 调用渠道报错时,错误信息
+     */
+    private String channelErrorMsg;
+
     /**
-     * 支付渠道异步通知的内容
+     * 支付渠道的同步/异步通知的内容
      *
-     * 在支持成功后,会记录回调的数据
+     * 对应 {@link PayOrderRespDTO#getRawData()}
      */
     private String channelNotifyData;
 

+ 34 - 59
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java

@@ -1,12 +1,13 @@
 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;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
-import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
 import com.baomidou.mybatisplus.annotation.KeySequence;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
@@ -37,6 +38,14 @@ public class PayRefundDO extends BaseDO {
      */
     @TableId
     private Long id;
+    /**
+     * 外部退款号,根据规则生成
+     *
+     * 调用支付渠道时,使用该字段作为对接的退款号:
+     * 1. 微信退款:对应 <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_4">申请退款</a> 的 out_refund_no 字段
+     * 2. 支付宝退款:对应 <a href="https://opendocs.alipay.com/open/02e7go"统一收单交易退款接口></a> 的 out_request_no 字段
+     */
+    private String no;
 
     /**
      * 应用编号
@@ -63,47 +72,27 @@ public class PayRefundDO extends BaseDO {
      */
     private Long orderId;
 
-    /**
-     * 交易订单号,根据规则生成
-     * 调用支付渠道时,使用该字段作为对接的订单号。
-     * 1. 调用微信支付 https://api.mch.weixin.qq.com/v3/refund/domestic/refunds 时,使用该字段作为 out_trade_no
-     * 2. 调用支付宝 https://opendocs.alipay.com/apis 时,使用该字段作为 out_trade_no
-     *  这里对应 pay_extension 里面的 no
-     * 例如说,P202110132239124200055
-     */
-    private String tradeNo;
-
     // ========== 商户相关字段 ==========
     /**
      * 商户订单编号
+     *
+     * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一
      */
     private String merchantOrderId;
-
     /**
-     * 商户退款订单号, 由商户系统产生, 由他们保证唯一,不能为空,通知商户时会传该字段。
-     * 例如说,内部系统 A 的退款订单号。需要保证每个 PayMerchantDO 唯一
-     * 个商户退款订单,对应一条退款请求记录。可多次提交。 渠道保持幂等
-     * 使用商户退款单,作为退款请求号
-     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml 中的 out_refund_no
-     * https://opendocs.alipay.com/apis alipay.trade.refund 中的 out_request_no
-     * 退款请求号。
-     * 标识一次退款请求,需要保证在交易号下唯一,如需部分退款,则此参数必传。
-     * 注:针对同一次退款请求,如果调用接口失败或异常了,重试时需要保证退款请求号不能变更,
-     * 防止该笔交易重复退款。支付宝会保证同样的退款请求号多次请求只会退一次。
-     * 退款单请求号,根据规则生成
-     * 例如说,R202109181134287570000
-     */
-    // TODO @jason:merchantRefundNo =》merchantRefundOId
-    private String merchantRefundNo;
-
+     * 商户退款订单号
+     *
+     * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一
+     */
+    private String merchantRefundId;
     /**
      * 异步通知地址
      */
     private String notifyUrl;
-
     /**
      * 通知商户退款结果的回调状态
-     * TODO 0 未发送 1 已发送
+     *
+     * 枚举 {@link PayNotifyStatusEnum}
      */
     private Integer notifyStatus;
 
@@ -115,12 +104,6 @@ public class PayRefundDO extends BaseDO {
      */
     private Integer status;
 
-    /**
-     * 退款类型(部分退款,全部退款)
-     *
-     * 枚举 {@link PayRefundTypeEnum}
-     */
-    private Integer type;
     /**
      * 支付金额,单位:分
      */
@@ -142,45 +125,37 @@ public class PayRefundDO extends BaseDO {
 
     // ========== 渠道相关字段 ==========
     /**
-     * 渠道订单号,pay_order 中的channel_order_no 对应
+     * 渠道订单号
+     *
+     * 冗余 {@link PayOrderDO#getChannelOrderNo()}
      */
     private String channelOrderNo;
     /**
-     * 微信中的 refund_id
-     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml
-     * 支付宝没有
-     * 渠道退款单号,渠道返回
+     * 渠道退款单号
+     *
+     * 1. 微信退款:对应 <a href="https://pay.weixin.qq.com/wiki/doc/api/micropay.php?chapter=9_4">申请退款</a> 的 refund_id 字段
+     * 2. 支付宝退款:没有字段
      */
     private String channelRefundNo;
+    /**
+     * 退款成功时间
+     */
+    private LocalDateTime successTime;
 
     /**
      * 调用渠道的错误码
      */
     private String channelErrorCode;
-
     /**
      * 调用渠道报错时,错误信息
      */
     private String channelErrorMsg;
 
     /**
-     * 支付渠道的额外参数
-     * 参见 https://www.pingxx.com/api/Refunds%20退款概述.html
-     */
-    private String channelExtras;
-
-    /**
-     * TODO
-     * 退款失效时间
-     */
-    private LocalDateTime expireTime;
-    /**
-     * 退款成功时间
-     */
-    private LocalDateTime successTime;
-    /**
-     * 退款通知时间
+     * 支付渠道的同步/异步通知的内容
+     *
+     * 对应 {@link PayRefundRespDTO#getRawData()}
      */
-    private LocalDateTime notifyTime;
+    private String channelNotifyData;
 
 }

+ 32 - 14
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/dal/mysql/refund/PayRefundMapper.java

@@ -1,11 +1,13 @@
 package cn.iocoder.yudao.module.pay.dal.mysql.refund;
 
-import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
-import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
+import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
+import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import org.apache.ibatis.annotations.Mapper;
 
 import java.util.List;
@@ -13,6 +15,34 @@ import java.util.List;
 @Mapper
 public interface PayRefundMapper extends BaseMapperX<PayRefundDO> {
 
+    default Long selectCountByAppId(Long appId) {
+        return selectCount(PayRefundDO::getAppId, appId);
+    }
+
+    default  PayRefundDO selectByAppIdAndMerchantRefundId(Long appId, String merchantRefundId) {
+        return selectOne(new LambdaQueryWrapperX<PayRefundDO>()
+                .eq(PayRefundDO::getAppId, appId)
+                .eq(PayRefundDO::getMerchantRefundId, merchantRefundId));
+    }
+
+    default Long selectCountByAppIdAndOrderId(Long appId, Long orderId, Integer status) {
+        return selectCount(new LambdaQueryWrapperX<PayRefundDO>()
+                .eq(PayRefundDO::getAppId, appId)
+                .eq(PayRefundDO::getOrderId, orderId)
+                .eq(PayRefundDO::getStatus, status));
+    }
+
+    default  PayRefundDO selectByAppIdAndNo(Long appId, String no) {
+        return selectOne(new LambdaQueryWrapperX<PayRefundDO>()
+                .eq(PayRefundDO::getAppId, appId)
+                .eq(PayRefundDO::getNo, no));
+    }
+
+    default int updateByIdAndStatus(Long id, Integer status, PayRefundDO update) {
+        return update(update, new LambdaQueryWrapper<PayRefundDO>()
+                .eq(PayRefundDO::getId, id).eq(PayRefundDO::getStatus, status));
+    }
+
     default PageResult<PayRefundDO> selectPage(PayRefundPageReqVO reqVO) {
         return selectPage(reqVO, new QueryWrapperX<PayRefundDO>()
                 .eqIfPresent("app_id", reqVO.getAppId())
@@ -37,16 +67,4 @@ public interface PayRefundMapper extends BaseMapperX<PayRefundDO> {
                 .orderByDesc("id"));
     }
 
-    default Long selectCountByApp(Long appId) {
-         return selectCount(PayRefundDO::getAppId, appId);
-    }
-
-    default PayRefundDO selectByReqNo(String reqNo) {
-        return selectOne("req_no", reqNo);
-    }
-
-    default  PayRefundDO selectByTradeNoAndMerchantRefundNo(String tradeNo, String merchantRefundNo){
-        return selectOne("trade_no", tradeNo, "merchant_refund_no", merchantRefundNo);
-    }
-
 }

+ 3 - 3
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/enums/refund/PayRefundTypeEnum.java → yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/enums/order/PayOrderRefundStatusEnum.java

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.pay.enums.refund;
+package cn.iocoder.yudao.module.pay.enums.order;
 
 import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
 import lombok.AllArgsConstructor;
@@ -11,10 +11,10 @@ import lombok.Getter;
  */
 @Getter
 @AllArgsConstructor
-public enum PayRefundTypeEnum implements IntArrayValuable {
+public enum PayOrderRefundStatusEnum implements IntArrayValuable {
 
     NO(0, "未退款"),
-    SOME(10, "部分退款"),
+    PART(10, "部分退款"),
     ALL(20, "全部退款")
     ;
 

+ 2 - 2
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);
     }
 

+ 14 - 11
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/channel/PayChannelServiceImpl.java

@@ -83,17 +83,20 @@ public class PayChannelServiceImpl implements PayChannelService {
      */
     @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS)
     public void refreshLocalCache() {
-        // 情况一:如果缓存里没有数据,则直接刷新缓存
-        if (CollUtil.isEmpty(channelCache)) {
-            initLocalCache();
-            return;
-        }
-
-        // 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存
-        LocalDateTime maxTime = CollectionUtils.getMaxValue(channelCache, PayChannelDO::getUpdateTime);
-        if (channelMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
-            initLocalCache();
-        }
+        // 注意:忽略自动多租户,因为要全局初始化缓存
+        TenantUtils.executeIgnore(() -> {
+            // 情况一:如果缓存里没有数据,则直接刷新缓存
+            if (CollUtil.isEmpty(channelCache)) {
+                initLocalCache();
+                return;
+            }
+
+            // 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存
+            LocalDateTime maxTime = CollectionUtils.getMaxValue(channelCache, PayChannelDO::getUpdateTime);
+            if (channelMapper.selectCountByUpdateTimeGt(maxTime) > 0) {
+                initLocalCache();
+            }
+        });
     }
 
     @Override

+ 10 - 5
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java

@@ -184,12 +184,17 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService {
         // 1. 校验订单是否可以退款
         PayDemoOrderDO order = validateDemoOrderCanRefund(id);
 
-        // 2.1 创建退款单
-        Long payRefundId = payRefundApi.createPayRefund(new PayRefundCreateReqDTO()
+        // 2.1 生成退款单号
+        // 一般来说,用户发起退款的时候,都会单独插入一个售后维权表,然后使用该表的 id 作为 refundId
+        // 这里我们是个简单的 demo,所以没有售后维权表,直接使用订单 id + "-refund" 来演示
+        String refundId = order.getId() + "-refund";
+        // 2.2 创建退款单
+        Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO()
                 .setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用
-                .setPayOrderId(order.getPayOrderId()) // 支付单号
+                .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号
+                .setMerchantRefundId(refundId)
                 .setReason("想退钱").setPrice(order.getPrice()));// 价格信息
-        // 2.2 更新退款单到 demo 订单
+        // 2.3 更新退款单到 demo 订单
         payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id)
                 .setPayRefundId(payRefundId).setRefundPrice(order.getPrice()));
     }
@@ -234,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);
         }

+ 1 - 0
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderExtensionService.java

@@ -7,6 +7,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
+// TODO 芋艿:合并到 PayOrder 里;
 /**
  * 支付订单 Service 接口
  *

+ 25 - 10
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderService.java

@@ -2,8 +2,7 @@ package cn.iocoder.yudao.module.pay.service.order;
 
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.order.PayOrderRespDTO;
 import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderPageReqVO;
@@ -32,6 +31,15 @@ public interface PayOrderService {
      */
     PayOrderDO getOrder(Long id);
 
+    /**
+     * 获得支付订单
+     *
+     * @param appId 应用编号
+     * @param merchantOrderId 商户订单编号
+     * @return 支付订单
+     */
+    PayOrderDO getOrder(Long appId, String merchantOrderId);
+
     /**
      * 获得指定应用的订单数量
      *
@@ -71,11 +79,11 @@ public interface PayOrderService {
     /**
      * 根据订单 ID 集合获取订单商品名称Map集合
      *
-     * @param idList 订单 ID 集合
+     * @param ids 订单 ID 集合
      * @return 订单商品 map 集合
      */
-    default Map<Long, PayOrderDO> getOrderSubjectMap(Collection<Long> idList) {
-        List<PayOrderDO> list = getOrderSubjectList(idList);
+    default Map<Long, PayOrderDO> getOrderSubjectMap(Collection<Long> ids) {
+        List<PayOrderDO> list = getOrderSubjectList(ids);
         return CollectionUtils.convertMap(list, PayOrderDO::getId);
     }
 
@@ -85,7 +93,7 @@ public interface PayOrderService {
      * @param reqDTO 创建请求
      * @return 支付单编号
      */
-    Long createPayOrder(@Valid PayOrderCreateReqDTO reqDTO);
+    Long createOrder(@Valid PayOrderCreateReqDTO reqDTO);
 
     /**
      * 提交支付
@@ -95,16 +103,23 @@ public interface PayOrderService {
      * @param userIp 提交 IP
      * @return 提交结果
      */
-    PayOrderSubmitRespVO submitPayOrder(@Valid PayOrderSubmitReqVO reqVO,
-                                        @NotEmpty(message = "提交 IP 不能为空") String userIp);
+    PayOrderSubmitRespVO submitOrder(@Valid PayOrderSubmitReqVO reqVO,
+                                     @NotEmpty(message = "提交 IP 不能为空") String userIp);
 
     /**
      * 通知支付单成功
      *
      * @param channelId 渠道编号
      * @param notify    通知
-     * @param rawNotify 通知数据
      */
-    void notifyPayOrder(Long channelId, PayOrderNotifyRespDTO notify, PayNotifyReqDTO rawNotify);
+    void notifyOrder(Long channelId, PayOrderRespDTO notify);
+
+    /**
+     * 更新支付订单的退款金额
+     *
+     * @param id 编号
+     * @param incrRefundPrice 增加的退款金额
+     */
+    void updateOrderRefundPrice(Long id, Integer incrRefundPrice);
 
 }

+ 130 - 56
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceImpl.java

@@ -4,15 +4,15 @@ 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;
 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.notify.PayNotifyReqDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayOrderNotifyRespDTO;
+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;
 import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderExportReqVO;
@@ -26,11 +26,10 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderExtensionMapper;
 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
-import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
+import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
-import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@@ -48,6 +47,7 @@ import java.util.Objects;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+import static cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants.*;
 
 /**
  * 支付订单 Service 实现类
@@ -82,6 +82,11 @@ public class PayOrderServiceImpl implements PayOrderService {
         return orderMapper.selectById(id);
     }
 
+    @Override
+    public PayOrderDO getOrder(Long appId, String merchantOrderId) {
+        return orderMapper.selectByAppIdAndMerchantOrderId(appId, merchantOrderId);
+    }
+
     @Override
     public Long getOrderCountByAppId(Long appId) {
         return orderMapper.selectCountByAppId(appId);
@@ -104,7 +109,7 @@ public class PayOrderServiceImpl implements PayOrderService {
     }
 
     @Override
-    public Long createPayOrder(PayOrderCreateReqDTO reqDTO) {
+    public Long createOrder(PayOrderCreateReqDTO reqDTO) {
         // 校验 App
         PayAppDO app = appService.validPayApp(reqDTO.getAppId());
 
@@ -112,7 +117,7 @@ public class PayOrderServiceImpl implements PayOrderService {
         PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId(
                 reqDTO.getAppId(), reqDTO.getMerchantOrderId());
         if (order != null) {
-            log.warn("[createPayOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(),
+            log.warn("[createOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(),
                     order.getMerchantOrderId(), toJsonString(order)); // 理论来说,不会出现这个情况
             return order.getId();
         }
@@ -120,20 +125,19 @@ 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())
                 // 退款相关字段
-                .setRefundStatus(PayRefundTypeEnum.NO.getStatus()).setRefundTimes(0).setRefundPrice(0L);
+                .setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus()).setRefundTimes(0).setRefundPrice(0);
         orderMapper.insert(order);
         return order.getId();
     }
 
-    @Override
-    @Transactional(rollbackFor = Exception.class)
-    public PayOrderSubmitRespVO submitPayOrder(PayOrderSubmitReqVO reqVO, String userIp) {
+    @Override // 注意,这里不能添加事务注解,避免调用支付渠道失败时,将 PayOrderExtensionDO 回滚了
+    public PayOrderSubmitRespVO submitOrder(PayOrderSubmitReqVO reqVO, String userIp) {
         // 1. 获得 PayOrderDO ,并校验其是否存在
-        PayOrderDO order = validatePayOrderCanSubmit(reqVO.getId());
+        PayOrderDO order = validateOrderCanSubmit(reqVO.getId());
         // 1.2 校验支付渠道是否有效
         PayChannelDO channel = validatePayChannelCanSubmit(order.getAppId(), reqVO.getChannelCode());
         PayClient client = payClientFactory.getPayClient(channel.getId());
@@ -148,35 +152,38 @@ public class PayOrderServiceImpl implements PayOrderService {
         // 3. 调用三方接口
         PayOrderUnifiedReqDTO unifiedOrderReqDTO = PayOrderConvert.INSTANCE.convert2(reqVO, userIp)
                 // 商户相关的字段
-                .setMerchantOrderId(orderExtension.getNo()) // 注意,此处使用的是 PayOrderExtensionDO.no 属性!
+                .setOutTradeNo(orderExtension.getNo()) // 注意,此处使用的是 PayOrderExtensionDO.no 属性!
                 .setSubject(order.getSubject()).setBody(order.getBody())
-                .setNotifyUrl(genChannelPayNotifyUrl(channel))
+                .setNotifyUrl(genChannelOrderNotifyUrl(channel))
                 .setReturnUrl(reqVO.getReturnUrl())
                 // 订单相关字段
-                .setAmount(order.getPrice()).setExpireTime(order.getExpireTime());
-        PayOrderUnifiedRespDTO unifiedOrderRespDTO = client.unifiedOrder(unifiedOrderReqDTO);
+                .setPrice(order.getPrice()).setExpireTime(order.getExpireTime());
+        PayOrderRespDTO unifiedOrderResp = client.unifiedOrder(unifiedOrderReqDTO);
 
         // 4. 如果调用直接支付成功,则直接更新支付单状态为成功。例如说:付款码支付,免密支付时,就直接验证支付成功
-        if (unifiedOrderRespDTO.getNotify() != null) {
-            notifyPayOrderSuccess(channel, unifiedOrderRespDTO.getNotify(), null);
+        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 validatePayOrderCanSubmit(Long id) {
+    private PayOrderDO validateOrderCanSubmit(Long id) {
         PayOrderDO order = orderMapper.selectById(id);
         if (order == null) { // 是否存在
-            throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
+            throw exception(PAY_ORDER_NOT_FOUND);
         }
         if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
-            throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING);
+            throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
         }
         if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期
-            throw exception(ErrorCodeConstants.PAY_ORDER_IS_EXPIRED);
+            throw exception(PAY_ORDER_IS_EXPIRED);
         }
         return order;
     }
@@ -184,14 +191,12 @@ public class PayOrderServiceImpl implements PayOrderService {
     private PayChannelDO validatePayChannelCanSubmit(Long appId, String channelCode) {
         // 校验 App
         appService.validPayApp(appId);
-
         // 校验支付渠道是否有效
         PayChannelDO channel = channelService.validPayChannel(appId, channelCode);
-        // 校验支付客户端是否正确初始化
         PayClient client = payClientFactory.getPayClient(channel.getId());
         if (client == null) {
             log.error("[validatePayChannelCanSubmit][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
-            throw exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND);
+            throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
         }
         return channel;
     }
@@ -202,8 +207,8 @@ public class PayOrderServiceImpl implements PayOrderService {
      * @param channel 支付渠道
      * @return 支付渠道的回调地址  配置地址 + "/" + channel id
      */
-    private String genChannelPayNotifyUrl(PayChannelDO channel) {
-        return payProperties.getCallbackUrl() + "/" + channel.getId();
+    private String genChannelOrderNotifyUrl(PayChannelDO channel) {
+        return payProperties.getOrderNotifyUrl() + "/" + channel.getId();
     }
 
     private String generateOrderExtensionNo() {
@@ -226,18 +231,57 @@ public class PayOrderServiceImpl implements PayOrderService {
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void notifyPayOrder(Long channelId, PayOrderNotifyRespDTO notify, PayNotifyReqDTO rawNotify) {
+    public void notifyOrder(Long channelId, PayOrderRespDTO notify) {
         // 校验支付渠道是否有效
         PayChannelDO channel = channelService.validPayChannel(channelId);
         // 更新支付订单为已支付
-        TenantUtils.execute(channel.getTenantId(), () -> notifyPayOrderSuccess(channel, notify, rawNotify));
+        TenantUtils.execute(channel.getTenantId(), () -> notifyPayOrder(channel, notify));
+    }
+
+    @Override
+    public void updateOrderRefundPrice(Long id, Integer incrRefundPrice) {
+        PayOrderDO order = orderMapper.selectById(id);
+        if (order == null) {
+            throw exception(PAY_ORDER_NOT_FOUND);
+        }
+        if (!PayOrderStatusEnum.isSuccess(order.getStatus())) {
+            throw exception(PAY_REFUND_PRICE_EXCEED);
+        }
+        if (order.getRefundPrice() + incrRefundPrice > order.getPrice()) {
+            throw exception(PAY_REFUND_PRICE_EXCEED);
+        }
+
+        // 更新订单
+        PayOrderDO updateObj = new PayOrderDO()
+                .setRefundPrice(order.getRefundPrice() + incrRefundPrice)
+                .setRefundTimes(order.getRefundTimes() + 1);
+        if (Objects.equals(updateObj.getRefundPrice(), order.getPrice())) {
+            updateObj.setStatus(PayOrderStatusEnum.CLOSED.getStatus())
+                    .setRefundStatus(PayOrderRefundStatusEnum.ALL.getStatus());
+        } else {
+            updateObj.setStatus(PayOrderStatusEnum.CLOSED.getStatus())
+                    .setRefundStatus(PayOrderRefundStatusEnum.PART.getStatus());
+        }
+        orderMapper.updateByIdAndStatus(id, PayOrderStatusEnum.SUCCESS.getStatus(), updateObj);
     }
 
-    private void notifyPayOrderSuccess(PayChannelDO channel, PayOrderNotifyRespDTO notify, PayNotifyReqDTO rawNotify) {
+    private void notifyPayOrder(PayChannelDO channel, PayOrderRespDTO notify) {
+        // 情况一:支付成功的回调
+        if (PayOrderStatusRespEnum.isSuccess(notify.getStatus())) {
+            notifyOrderSuccess(channel, notify);
+            return;
+        }
+        // 情况二:支付失败的回调
+        if (PayOrderStatusRespEnum.isClosed(notify.getStatus())) {
+            notifyOrderClosed(channel, notify);
+        }
+    }
+
+    private void notifyOrderSuccess(PayChannelDO channel, PayOrderRespDTO notify) {
         // 1. 更新 PayOrderExtensionDO 支付成功
-        PayOrderExtensionDO orderExtension = updatePayOrderExtensionSuccess(notify.getOrderExtensionNo(), rawNotify);
+        PayOrderExtensionDO orderExtension = updateOrderExtensionSuccess(notify);
         // 2. 更新 PayOrderDO 支付成功
-        Pair<Boolean, PayOrderDO> order = updatePayOrderSuccess(channel, orderExtension, notify);
+        Pair<Boolean, PayOrderDO> order = updateOrderExtensionSuccess(channel, orderExtension, notify);
         if (order.getKey()) { // 如果之前已经成功回调,则直接返回,不用重复记录支付通知记录;例如说:支付平台重复回调
             return;
         }
@@ -250,33 +294,30 @@ public class PayOrderServiceImpl implements PayOrderService {
     /**
      * 更新 PayOrderExtensionDO 支付成功
      *
-     * @param no 支付订单号(支付模块)
-     * @param rawNotify 通知数据
+     * @param notify 通知
      * @return PayOrderExtensionDO 对象
      */
-    private PayOrderExtensionDO updatePayOrderExtensionSuccess(String no, PayNotifyReqDTO rawNotify) {
+    private PayOrderExtensionDO updateOrderExtensionSuccess(PayOrderRespDTO notify) {
         // 1. 查询 PayOrderExtensionDO
-        PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(no);
+        PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo());
         if (orderExtension == null) {
-            throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_NOT_FOUND);
+            throw exception(PAY_ORDER_EXTENSION_NOT_FOUND);
         }
         if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { // 如果已经是成功,直接返回,不用重复更新
-            log.info("[updatePayOrderSuccess][支付拓展单({}) 已经是已支付,无需更新为已支付]", orderExtension.getId());
+            log.info("[updateOrderExtensionSuccess][支付拓展单({}) 已经是已支付,无需更新]", orderExtension.getId());
             return orderExtension;
         }
         if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付
-            throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
+            throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
         }
 
         // 2. 更新 PayOrderExtensionDO
-        int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(),
-                PayOrderStatusEnum.WAITING.getStatus(), PayOrderExtensionDO.builder().id(orderExtension.getId())
-                        .status(PayOrderStatusEnum.SUCCESS.getStatus())
-                        .channelNotifyData(toJsonString(rawNotify)).build());
+        int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(),
+                PayOrderExtensionDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(toJsonString(notify)).build());
         if (updateCounts == 0) { // 校验状态,必须是待支付
-            throw exception(ErrorCodeConstants.PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
+            throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
         }
-        log.info("[updatePayOrderSuccess][支付拓展单({}) 更新为已支付]", orderExtension.getId());
+        log.info("[updateOrderExtensionSuccess][支付拓展单({}) 更新为已支付]", orderExtension.getId());
         return orderExtension;
     }
 
@@ -289,20 +330,20 @@ public class PayOrderServiceImpl implements PayOrderService {
      * @return key:是否之前已经成功回调
      *         value:PayOrderDO 对象
      */
-    private Pair<Boolean, PayOrderDO> updatePayOrderSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension,
-                                                            PayOrderNotifyRespDTO notify) {
+    private Pair<Boolean, PayOrderDO> updateOrderExtensionSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension,
+                                                                  PayOrderRespDTO notify) {
         // 1. 判断 PayOrderDO 是否处于待支付
         PayOrderDO order = orderMapper.selectById(orderExtension.getOrderId());
         if (order == null) {
-            throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
+            throw exception(PAY_ORDER_NOT_FOUND);
         }
         if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功,直接返回,不用重复更新
                 && Objects.equals(order.getSuccessExtensionId(), orderExtension.getId())) {
-            log.info("[updatePayOrderSuccess][支付订单({}) 已经是已支付,无需更新为已支付]", order.getId());
+            log.info("[updateOrderExtensionSuccess][支付订单({}) 已经是已支付,无需更新]", order.getId());
             return Pair.of(true, order);
         }
         if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
-            throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING);
+            throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
         }
 
         // 2. 更新 PayOrderDO
@@ -313,10 +354,43 @@ public class PayOrderServiceImpl implements PayOrderService {
                         .channelOrderNo(notify.getChannelOrderNo()).channelUserId(notify.getChannelUserId())
                         .notifyTime(LocalDateTime.now()).build());
         if (updateCounts == 0) { // 校验状态,必须是待支付
-            throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_WAITING);
+            throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
         }
-        log.info("[updatePayOrderSuccess][支付订单({}) 更新为已支付]", order.getId());
+        log.info("[updateOrderExtensionSuccess][支付订单({}) 更新为已支付]", order.getId());
         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());
+    }
+
 }

+ 3 - 5
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundService.java

@@ -1,11 +1,10 @@
 package cn.iocoder.yudao.module.pay.service.refund;
 
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayNotifyReqDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
 
 import java.util.List;
@@ -62,8 +61,7 @@ public interface PayRefundService {
      *
      * @param channelId  渠道编号
      * @param notify     通知
-     * @param rawNotify  通知数据
      */
-    void notifyPayRefund(Long channelId, PayRefundNotifyRespDTO notify, PayNotifyReqDTO rawNotify);
+    void notifyRefund(Long channelId, PayRefundRespDTO notify);
 
 }

+ 140 - 150
yudao-module-pay/yudao-module-pay-biz/src/main/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceImpl.java

@@ -1,32 +1,31 @@
 package cn.iocoder.yudao.module.pay.service.refund;
 
+import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.util.RandomUtil;
-import cn.hutool.core.util.StrUtil;
-import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.pay.config.PayProperties;
 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.notify.PayNotifyReqDTO;
-import cn.iocoder.yudao.framework.pay.core.client.dto.notify.PayRefundNotifyRespDTO;
+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.enums.refund.PayNotifyRefundStatusEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.refund.PayRefundStatusRespEnum;
+import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
 import cn.iocoder.yudao.module.pay.api.refund.dto.PayRefundCreateReqDTO;
 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundExportReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.refund.vo.PayRefundPageReqVO;
+import cn.iocoder.yudao.module.pay.convert.refund.PayRefundConvert;
 import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.channel.PayChannelDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO;
 import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
-import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
 import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper;
 import cn.iocoder.yudao.module.pay.enums.ErrorCodeConstants;
+import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.notify.PayNotifyTypeEnum;
-import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
+import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
-import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@@ -41,12 +40,14 @@ import org.springframework.validation.annotation.Validated;
 import javax.annotation.Resource;
 import java.time.LocalDateTime;
 import java.util.List;
-import java.util.Objects;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 
 /**
  * 退款订单 Service 实现类
  *
- * @author aquan
+ * @author jason
  */
 @Service
 @Slf4j
@@ -61,8 +62,6 @@ public class PayRefundServiceImpl implements PayRefundService {
 
     @Resource
     private PayRefundMapper refundMapper;
-    @Resource
-    private PayOrderMapper orderMapper; // TODO @jason:需要改成不直接操作 db;
 
     @Resource
     private PayOrderService orderService;
@@ -82,7 +81,7 @@ public class PayRefundServiceImpl implements PayRefundService {
 
     @Override
     public Long getRefundCountByAppId(Long appId) {
-        return refundMapper.selectCountByApp(appId);
+        return refundMapper.selectCountByAppId(appId);
     }
 
     @Override
@@ -98,85 +97,83 @@ public class PayRefundServiceImpl implements PayRefundService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createPayRefund(PayRefundCreateReqDTO reqDTO) {
-        // 获得 PayOrderDO
-        PayOrderDO order = orderService.getOrder(reqDTO.getPayOrderId());
-        // 校验订单是否存在
-        if (Objects.isNull(order) ) {
-            throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
-        }
-        // 校验 App
-        PayAppDO app = appService.validPayApp(order.getAppId());
-        // 校验支付渠道是否有效
+        // 1.1 校验 App
+        PayAppDO app = appService.validPayApp(reqDTO.getAppId());
+        // 1.2 校验支付订单
+        PayOrderDO order = validatePayOrderCanRefund(reqDTO);
+        // 1.3 校验支付渠道是否有效
         PayChannelDO channel = channelService.validPayChannel(order.getChannelId());
-        // 校验支付客户端是否正确初始化
         PayClient client = payClientFactory.getPayClient(channel.getId());
         if (client == null) {
             log.error("[refund][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
-            throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND);
+            throw exception(ErrorCodeConstants.PAY_CHANNEL_CLIENT_NOT_FOUND);
         }
-
-        // TODO 芋艿:待实现
-        String merchantRefundId = RandomUtil.randomNumbers(16);
-
-        // 校验退款的条件
-        validatePayRefund(reqDTO, order);
-        // 退款类型
-        PayRefundTypeEnum refundType = PayRefundTypeEnum.SOME;
-        if (Objects.equals(reqDTO.getPrice(), order.getPrice())) {
-            refundType = PayRefundTypeEnum.ALL;
+        // 1.4 校验退款订单是否已经存在
+        PayRefundDO refund = refundMapper.selectByAppIdAndMerchantRefundId(
+                app.getId(), reqDTO.getMerchantRefundId());
+        if (refund != null) {
+            throw exception(ErrorCodeConstants.PAY_REFUND_EXISTS);
         }
-        PayOrderExtensionDO orderExtensionDO = orderExtensionService.getOrderExtension(order.getSuccessExtensionId());
-        PayRefundDO payRefundDO = refundMapper.selectByTradeNoAndMerchantRefundNo(orderExtensionDO.getNo(),
-                merchantRefundId);  // TODO 芋艿:需要优化
-        if(Objects.nonNull(payRefundDO)){
-            // 退款订单已经提交过。
-            //TODO 校验相同退款单的金额
-            // TODO @jason:咱要不封装一个 ObjectUtils.equalsAny
-            if (Objects.equals(PayRefundStatusEnum.SUCCESS.getStatus(), payRefundDO.getStatus())
-                    || Objects.equals(PayRefundStatusEnum.CLOSE.getStatus(), payRefundDO.getStatus())) {
-                //已成功退款
-                throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_SUCCEED);
-            }
-            //可以重复提交,保证 退款请求号 一致,由渠道保证幂等
-        } else {
-            // 成功,插入退款单 状态为生成.没有和渠道交互
-            // TODO @jason:搞到 convert 里。一些额外的自动,手动 set 下;
-            payRefundDO = PayRefundDO.builder()
-                    .appId(order.getAppId())
-                    .channelOrderNo(order.getChannelOrderNo())
-                    .channelCode(order.getChannelCode())
-                    .channelId(order.getChannelId())
-                    .orderId(order.getId())
-                    .merchantRefundNo(merchantRefundId) // TODO 芋艿:需要优化
-                    .notifyUrl(app.getRefundNotifyUrl())
-                    .payPrice(order.getPrice())
-                    .refundPrice(reqDTO.getPrice())
-                    .userIp(reqDTO.getUserIp())
-                    .merchantOrderId(order.getMerchantOrderId())
-                    .tradeNo(orderExtensionDO.getNo())
-                    .status(PayRefundStatusEnum.CREATE.getStatus())
-                    .reason(reqDTO.getReason())
-                    .notifyStatus(PayOrderNotifyStatusEnum.NO.getStatus())
-                    .type(refundType.getStatus())
-                    .build();
-            refundMapper.insert(payRefundDO);
-        }
-        // TODO @jason:搞到 convert 里。一些额外的自动,手动 set 下;
-        PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO();
-        unifiedReqDTO.setUserIp(reqDTO.getUserIp())
-                .setAmount(reqDTO.getPrice())
+
+        // 2.1 插入退款单
+        refund = PayRefundConvert.INSTANCE.convert(reqDTO)
+                .setNo(generateRefundNo()).setOrderId(order.getId())
+                .setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode())
+                // 商户相关的字段
+                .setNotifyUrl(app.getRefundNotifyUrl()).setNotifyStatus(PayNotifyStatusEnum.WAITING.getStatus())
+                // 渠道相关字段
                 .setChannelOrderNo(order.getChannelOrderNo())
-                .setPayTradeNo(orderExtensionDO.getNo())
-                .setMerchantRefundId(merchantRefundId)  // TODO 芋艿:需要优化
-                .setNotifyUrl(genChannelPayNotifyUrl(channel)) // TODO 芋艿:优化下 notifyUrl
+                // 退款相关字段
+                .setStatus(PayRefundStatusEnum.WAITING.getStatus())
+                .setPayPrice(order.getPrice()).setRefundPrice(reqDTO.getPrice());
+        refundMapper.insert(refund);
+        // 2.2 向渠道发起退款申请
+        PayOrderExtensionDO orderExtension = orderExtensionService.getOrderExtension(order.getSuccessExtensionId());
+        PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO()
+                .setPayPrice(order.getPrice())
+                .setRefundPrice(reqDTO.getPrice())
+                .setOutTradeNo(orderExtension.getNo())
+                .setOutRefundNo(refund.getNo())
+                .setNotifyUrl(genChannelRefundNotifyUrl(channel))
                 .setReason(reqDTO.getReason());
-        // 向渠道发起退款申请
-        client.unifiedRefund(unifiedReqDTO);
-        // 检查是否失败,失败抛出业务异常。
-        // TODO 渠道的异常记录。
-        // TODO @jason:可以先打个 warn log 哈;
+        PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); // TODO 增加一个 channelErrorCode、channelErrorMsg 字段
+        // 2.3 处理退款返回
+        notifyRefund(channel, refundRespDTO);
+
         // 成功在 退款回调中处理
-        return payRefundDO.getId();
+        return refund.getId();
+    }
+
+    /**
+     * 校验支付订单是否可以退款
+     *
+     * @param reqDTO 退款申请信息
+     * @return 支付订单
+     */
+    private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO) {
+        PayOrderDO order = orderService.getOrder(reqDTO.getAppId(), reqDTO.getMerchantOrderId());
+        if (order == null) {
+            throw exception(ErrorCodeConstants.PAY_ORDER_NOT_FOUND);
+        }
+        // 校验状态,必须是支付状态
+        if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
+            throw exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_SUCCESS);
+        }
+
+        // 是否已经全额退款
+        if (PayOrderRefundStatusEnum.ALL.getStatus().equals(order.getRefundStatus())) {
+            throw exception(ErrorCodeConstants.PAY_REFUND_ALL_REFUNDED);
+        }
+        // 校验金额 退款金额不能大于原定的金额
+        if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){
+            throw exception(ErrorCodeConstants.PAY_REFUND_PRICE_EXCEED);
+        }
+        // 是否有退款中的订单
+        if (refundMapper.selectCountByAppIdAndOrderId(reqDTO.getAppId(), order.getId(),
+                PayRefundStatusEnum.WAITING.getStatus()) > 0) {
+            throw exception(ErrorCodeConstants.PAY_REFUND_HAS_REFUNDING);
+        }
+        return order;
     }
 
     /**
@@ -185,91 +182,84 @@ public class PayRefundServiceImpl implements PayRefundService {
      * @param channel 支付渠道
      * @return 支付渠道的回调地址  配置地址 + "/" + channel id
      */
-    private String genChannelPayNotifyUrl(PayChannelDO channel) {
-        return payProperties.getCallbackUrl() + "/" + channel.getId();
+    private String genChannelRefundNotifyUrl(PayChannelDO channel) {
+        return payProperties.getRefundNotifyUrl() + "/" + channel.getId();
+    }
+
+    private String generateRefundNo() {
+//    wx
+//    2014
+//    10
+//    27
+//    20
+//    09
+//    39
+//    5522657
+//    a690389285100
+        // 目前的算法
+        // 时间序列,年月日时分秒 14 位
+        // 纯随机,6 位 TODO 芋艿:此处估计是会有问题的,后续在调整
+        return DateUtil.format(LocalDateTime.now(), "yyyyMMddHHmmss") + // 时间序列
+                RandomUtil.randomInt(100000, 999999) // 随机。为什么是这个范围,因为偷懒
+                ;
     }
 
     @Override
     @Transactional(rollbackFor = Exception.class)
-    public void notifyPayRefund(Long channelId, PayRefundNotifyRespDTO notify, PayNotifyReqDTO rawNotify) {
+    public void notifyRefund(Long channelId, PayRefundRespDTO notify) {
+        // 校验支付渠道是否有效
+        channelService.validPayChannel(channelId);
+        // 通知结果
+
         // 校验支付渠道是否有效
-        // TODO 芋艿:需要重构下这块的逻辑
         PayChannelDO channel = channelService.validPayChannel(channelId);
-        if (Objects.equals(PayNotifyRefundStatusEnum.SUCCESS, notify.getStatus())){
-            payRefundSuccess(notify);
+        // 更新退款订单
+        TenantUtils.execute(channel.getTenantId(), () -> notifyRefund(channel, notify));
+    }
+
+    private void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) {
+        if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) {
+            notifyRefundSuccess(channel, notify);
         } else {
-            //TODO 支付异常, 支付宝似乎没有支付异常的通知。
-            // TODO @jason:那这里可以考虑打个 error logger @芋艿 微信是否存在支付异常通知
+            notifyRefundFailure(channel, notify);
         }
     }
 
-    private void payRefundSuccess(PayRefundNotifyRespDTO refundNotify) {
-        // 校验退款单存在
-        PayRefundDO refundDO = refundMapper.selectByTradeNoAndMerchantRefundNo(refundNotify.getTradeNo(),
-                refundNotify.getReqNo());
-        if (refundDO == null) {
-            log.error("[payRefundSuccess][不存在 seqNo 为{} 的支付退款单]", refundNotify.getReqNo());
-            throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_NOT_FOUND);
+    private void notifyRefundSuccess(PayChannelDO channel, PayRefundRespDTO notify) {
+        // 1.1 查询 PayRefundDO
+        PayRefundDO refund = refundMapper.selectByAppIdAndNo(
+                channel.getAppId(), notify.getOutRefundNo());
+        if (refund == null) {
+            throw exception(ErrorCodeConstants.PAY_REFUND_NOT_FOUND);
+        }
+        if (PayRefundStatusEnum.isSuccess(refund.getStatus())) { // 如果已经是成功,直接返回,不用重复更新
+            return;
+        }
+        if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) {
+            throw exception(ErrorCodeConstants.PAY_REFUND_STATUS_IS_NOT_WAITING);
         }
 
-        // 得到已退金额
-        PayOrderDO payOrderDO = orderService.getOrder(refundDO.getOrderId());
-        Long refundedAmount = payOrderDO.getRefundPrice();
-
-        PayOrderStatusEnum orderStatus = PayOrderStatusEnum.SUCCESS;
-        if(Objects.equals(payOrderDO.getPrice(), refundedAmount+ refundDO.getRefundPrice())){
-            //支付金额  = 已退金额 + 本次退款金额。
-            orderStatus = PayOrderStatusEnum.CLOSED;
+        // 1.2 更新 PayRefundDO
+        PayRefundDO updateRefundObj = new PayRefundDO()
+                .setSuccessTime(notify.getSuccessTime())
+                .setChannelRefundNo(notify.getChannelRefundNo())
+                .setStatus(PayRefundStatusEnum.SUCCESS.getStatus())
+                .setChannelNotifyData(toJsonString(notify));
+        int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj);
+        if (updateCounts == 0) { // 校验状态,必须是等待状态
+            throw exception(ErrorCodeConstants.PAY_REFUND_STATUS_IS_NOT_WAITING);
         }
-        // 更新支付订单
-        PayOrderDO updateOrderDO = new PayOrderDO();
-        updateOrderDO.setId(refundDO.getOrderId())
-                .setRefundPrice(refundedAmount + refundDO.getRefundPrice())
-                .setStatus(orderStatus.getStatus())
-                .setRefundTimes(payOrderDO.getRefundTimes() + 1)
-                .setRefundStatus(refundDO.getType());
-        orderMapper.updateById(updateOrderDO);
 
-        // 更新退款订单
-        PayRefundDO updateRefundDO = new PayRefundDO();
-        updateRefundDO.setId(refundDO.getId())
-                .setSuccessTime(refundNotify.getRefundSuccessTime())
-                .setChannelRefundNo(refundNotify.getChannelOrderNo())
-                .setTradeNo(refundNotify.getTradeNo())
-                .setNotifyTime(LocalDateTime.now())
-                .setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
-        refundMapper.updateById(updateRefundDO);
+        // 2. 更新订单
+        orderService.updateOrderRefundPrice(refund.getOrderId(), refund.getRefundPrice());
 
-        // 插入退款通知记录
-        // TODO 通知商户成功或者失败. 现在通知似乎没有实现, 只是回调
+        // 3. 插入退款通知记录
         notifyService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
-                .type(PayNotifyTypeEnum.REFUND.getType()).dataId(refundDO.getId()).build());
+                .type(PayNotifyTypeEnum.REFUND.getType()).dataId(refund.getId()).build());
     }
 
-    /**
-     * 校验是否进行退款
-     *
-     * @param reqDTO 退款申请信息
-     * @param order 原始支付订单信息
-     */
-    private void validatePayRefund(PayRefundCreateReqDTO reqDTO, PayOrderDO order) {
-        // 校验状态,必须是支付状态
-        if (!PayOrderStatusEnum.SUCCESS.getStatus().equals(order.getStatus())) {
-            throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_ORDER_STATUS_IS_NOT_SUCCESS);
-        }
-        // 是否已经全额退款
-        if (PayRefundTypeEnum.ALL.getStatus().equals(order.getRefundStatus())) {
-            throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_ALL_REFUNDED);
-        }
-        // 校验金额 退款金额不能大于 原定的金额
-        if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){
-            throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_PRICE_PRICE_EXCEED);
-        }
-        // 校验渠道订单号
-        if (StrUtil.isEmpty(order.getChannelOrderNo())) {
-            throw ServiceExceptionUtil.exception(ErrorCodeConstants.PAY_REFUND_CHN_ORDER_NO_IS_NULL);
-        }
-        //TODO  退款的期限  退款次数的控制
+    private void notifyRefundFailure(PayChannelDO channel, PayRefundRespDTO notify) {
+        // TODO 芋艿:未实现
     }
 
 }

+ 2 - 6
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/app/PayAppServiceTest.java

@@ -3,9 +3,7 @@ package cn.iocoder.yudao.module.pay.service.app;
 import cn.hutool.core.util.RandomUtil;
 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
-import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppCreateReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppPageReqVO;
 import cn.iocoder.yudao.module.pay.controller.admin.app.vo.PayAppUpdateReqVO;
@@ -18,8 +16,6 @@ import org.springframework.boot.test.mock.mockito.MockBean;
 import org.springframework.context.annotation.Import;
 
 import javax.annotation.Resource;
-import java.time.LocalDateTime;
-import java.util.Collections;
 import java.util.Map;
 
 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
@@ -58,7 +54,7 @@ public class PayAppServiceTest extends BaseDbUnitTest {
         // 准备参数
         PayAppCreateReqVO reqVO = randomPojo(PayAppCreateReqVO.class, o ->
                 o.setStatus((RandomUtil.randomEle(CommonStatusEnum.values()).getStatus()))
-                        .setPayNotifyUrl(randomURL())
+                        .setOrderNotifyUrl(randomURL())
                         .setRefundNotifyUrl(randomURL()));
 
         // 调用
@@ -77,7 +73,7 @@ public class PayAppServiceTest extends BaseDbUnitTest {
         // 准备参数
         PayAppUpdateReqVO reqVO = randomPojo(PayAppUpdateReqVO.class, o -> {
             o.setStatus(CommonStatusEnum.ENABLE.getStatus());
-            o.setPayNotifyUrl(randomURL()).setRefundNotifyUrl(randomURL());
+            o.setOrderNotifyUrl(randomURL()).setRefundNotifyUrl(randomURL());
             o.setId(dbApp.getId()); // 设置更新的 ID
         });
 

+ 9 - 9
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/order/PayOrderServiceTest.java

@@ -13,7 +13,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.order.PayOrderMapper;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
-import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
+import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@@ -85,9 +85,9 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
             o.setSuccessTime(LocalDateTime.of(2018, 1, 1, 10, 10, 2));
             o.setNotifyTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
             o.setSuccessExtensionId(1L);
-            o.setRefundStatus(PayRefundTypeEnum.NO.getStatus());
+            o.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
             o.setRefundTimes(0);
-            o.setRefundPrice(0L);
+            o.setRefundPrice(0);
             o.setChannelUserId("1008611");
             o.setChannelOrderNo(channelOrderId);
             o.setUpdateTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
@@ -106,7 +106,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
         // 测试 status 不匹配
         orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())));
         // 测试 refundStatus 不匹配
-        orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayRefundTypeEnum.ALL.getStatus())));
+        orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayOrderRefundStatusEnum.ALL.getStatus())));
         // 测试 createTime 不匹配
         orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(LocalDateTime.of(2019, 1, 1, 10, 10,
                 1))));
@@ -118,7 +118,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
         reqVO.setMerchantOrderId(merchantOrderId);
         reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
         reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
-        reqVO.setRefundStatus(PayRefundTypeEnum.NO.getStatus());
+        reqVO.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
         reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2018, 1, 1, 10, 1, 0), LocalDateTime.of(2018, 1, 1, 10, 1, 0)}));
         // 调用
         PageResult<PayOrderDO> pageResult = orderService.getOrderPage(reqVO);
@@ -153,9 +153,9 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
             o.setSuccessTime(LocalDateTime.of(2018, 1, 1, 10, 10, 2));
             o.setNotifyTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
             o.setSuccessExtensionId(1L);
-            o.setRefundStatus(PayRefundTypeEnum.NO.getStatus());
+            o.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
             o.setRefundTimes(0);
-            o.setRefundPrice(0L);
+            o.setRefundPrice(0);
             o.setChannelUserId("1008611");
             o.setChannelOrderNo(channelOrderId);
             o.setUpdateTime(LocalDateTime.of(2018, 1, 1, 10, 10, 15));
@@ -175,7 +175,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
         // 测试 status 不匹配
         orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())));
         // 测试 refundStatus 不匹配
-        orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayRefundTypeEnum.ALL.getStatus())));
+        orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setRefundStatus(PayOrderRefundStatusEnum.ALL.getStatus())));
         // 测试 createTime 不匹配
         orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(LocalDateTime.of(2019, 1, 1, 10, 10,
                 1))));
@@ -187,7 +187,7 @@ public class PayOrderServiceTest extends BaseDbUnitTest {
         reqVO.setMerchantOrderId(merchantOrderId);
         reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
         reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus());
-        reqVO.setRefundStatus(PayRefundTypeEnum.NO.getStatus());
+        reqVO.setRefundStatus(PayOrderRefundStatusEnum.NO.getStatus());
         reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2018, 1, 1, 10, 1, 0), LocalDateTime.of(2018, 1, 1, 10, 1, 0)}));
 
         // 调用

+ 10 - 23
yudao-module-pay/yudao-module-pay-biz/src/test/java/cn/iocoder/yudao/module/pay/service/refund/PayRefundServiceTest.java

@@ -11,7 +11,7 @@ import cn.iocoder.yudao.module.pay.dal.dataobject.refund.PayRefundDO;
 import cn.iocoder.yudao.module.pay.dal.mysql.refund.PayRefundMapper;
 import cn.iocoder.yudao.module.pay.enums.order.PayOrderNotifyStatusEnum;
 import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
-import cn.iocoder.yudao.module.pay.enums.refund.PayRefundTypeEnum;
+import cn.iocoder.yudao.module.pay.enums.order.PayOrderRefundStatusEnum;
 import cn.iocoder.yudao.module.pay.service.app.PayAppService;
 import cn.iocoder.yudao.module.pay.service.channel.PayChannelService;
 import cn.iocoder.yudao.module.pay.service.notify.PayNotifyService;
@@ -62,13 +62,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
             o.setChannelId(1L);
             o.setChannelCode(PayChannelEnum.WX_PUB.getCode());
             o.setOrderId(1L);
-            o.setTradeNo("OT0000001");
+            o.setNo("OT0000001");
             o.setMerchantOrderId("MOT0000001");
-            o.setMerchantRefundNo("MRF0000001");
+            o.setMerchantRefundId("MRF0000001");
             o.setNotifyUrl("https://www.cancanzi.com");
             o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
             o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
-            o.setType(PayRefundTypeEnum.SOME.getStatus());
             o.setPayPrice(100);
             o.setRefundPrice(500);
             o.setReason("就是想退款了,你有意见吗");
@@ -77,10 +76,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
             o.setChannelRefundNo("CHR0000001");
             o.setChannelErrorCode("");
             o.setChannelErrorMsg("");
-            o.setChannelExtras("");
-            o.setExpireTime(LocalDateTime.of(2021, 1, 1, 10, 10, 30));
             o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15));
-            o.setNotifyTime(LocalDateTime.of(2021, 1, 1, 10, 10, 20));
             o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10));
             o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35));
         });
@@ -90,14 +86,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
         // 测试 channelCode 不匹配
         refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())));
         // 测试 merchantRefundNo 不匹配
-        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundNo("MRF1111112")));
+        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId("MRF1111112")));
         // 测试 notifyStatus 不匹配
         refundMapper.insert(
                 cloneIgnoreId(dbRefund, o -> o.setNotifyStatus(PayOrderNotifyStatusEnum.FAILURE.getStatus())));
         // 测试 status 不匹配
-        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.CLOSE.getStatus())));
-        // 测试 type 不匹配
-        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setType(PayRefundTypeEnum.ALL.getStatus())));
+        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus())));
         // 测试 createTime 不匹配
         refundMapper.insert(cloneIgnoreId(dbRefund, o ->
                 o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10))));
@@ -108,7 +102,6 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
         reqVO.setMerchantRefundNo("MRF0000001");
         reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
         reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
-        reqVO.setType(PayRefundTypeEnum.SOME.getStatus());
         reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2021, 1, 1, 10, 10, 10), LocalDateTime.of(2021, 1, 1, 10, 10, 12)}));
 
         // 调用
@@ -127,13 +120,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
             o.setChannelId(1L);
             o.setChannelCode(PayChannelEnum.WX_PUB.getCode());
             o.setOrderId(1L);
-            o.setTradeNo("OT0000001");
+            o.setNo("OT0000001");
             o.setMerchantOrderId("MOT0000001");
-            o.setMerchantRefundNo("MRF0000001");
+            o.setMerchantRefundId("MRF0000001");
             o.setNotifyUrl("https://www.cancanzi.com");
             o.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
             o.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
-            o.setType(PayRefundTypeEnum.SOME.getStatus());
             o.setPayPrice(100);
             o.setRefundPrice(500);
             o.setReason("就是想退款了,你有意见吗");
@@ -142,10 +134,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
             o.setChannelRefundNo("CHR0000001");
             o.setChannelErrorCode("");
             o.setChannelErrorMsg("");
-            o.setChannelExtras("");
-            o.setExpireTime(LocalDateTime.of(2021, 1, 1, 10, 10, 30));
             o.setSuccessTime(LocalDateTime.of(2021, 1, 1, 10, 10, 15));
-            o.setNotifyTime(LocalDateTime.of(2021, 1, 1, 10, 10, 20));
             o.setCreateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 10));
             o.setUpdateTime(LocalDateTime.of(2021, 1, 1, 10, 10, 35));
         });
@@ -155,14 +144,12 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
         // 测试 channelCode 不匹配
         refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())));
         // 测试 merchantRefundNo 不匹配
-        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundNo("MRF1111112")));
+        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId("MRF1111112")));
         // 测试 notifyStatus 不匹配
         refundMapper.insert(
                 cloneIgnoreId(dbRefund, o -> o.setNotifyStatus(PayOrderNotifyStatusEnum.FAILURE.getStatus())));
         // 测试 status 不匹配
-        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.CLOSE.getStatus())));
-        // 测试 type 不匹配
-        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setType(PayRefundTypeEnum.ALL.getStatus())));
+        refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayRefundStatusEnum.FAILURE.getStatus())));
         // 测试 createTime 不匹配
         refundMapper.insert(cloneIgnoreId(dbRefund, o ->
                 o.setCreateTime(LocalDateTime.of(2022, 1, 1, 10, 10, 10))));
@@ -174,7 +161,7 @@ public class PayRefundServiceTest extends BaseDbUnitTest {
         reqVO.setMerchantRefundNo("MRF0000001");
         reqVO.setNotifyStatus(PayOrderNotifyStatusEnum.SUCCESS.getStatus());
         reqVO.setStatus(PayRefundStatusEnum.SUCCESS.getStatus());
-        reqVO.setType(PayRefundTypeEnum.SOME.getStatus());
+        reqVO.setType(PayOrderRefundStatusEnum.PART.getStatus());
         reqVO.setCreateTime((new LocalDateTime[]{LocalDateTime.of(2021, 1, 1, 10, 10, 10), LocalDateTime.of(2021, 1, 1, 10, 10, 12)}));
 
         // 调用

+ 2 - 1
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: # 错误码相关配置项

+ 1 - 1
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:

+ 5 - 5
yudao-ui-admin/src/api/pay/channel.js

@@ -28,14 +28,14 @@ export function deleteChannel(id) {
 }
 
 // 获得支付渠道
-export function getChannel(appId,code) {
+export function getChannel(appId, code) {
   return request({
-    url: '/pay/channel/get-channel',
+    url: '/pay/channel/get',
+    method: 'get',
     params:{
-      appId:appId,
-      code:code
+      appId,
+      code
     },
-    method: 'get'
   })
 }
 

File diff suppressed because it is too large
+ 0 - 0
yudao-ui-admin/src/assets/images/pay/icon/wx_bar.svg


+ 1 - 0
yudao-ui-admin/src/assets/images/pay/icon/wx_native.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1627279375144" class="icon" viewBox="0 0 1115 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4399" width="43.5546875" height="40" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.eot?#iefix&quot;) format(&quot;embedded-opentype&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff2&quot;) format(&quot;woff2&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.woff&quot;) format(&quot;woff&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.ttf&quot;) format(&quot;truetype&quot;), url(&quot;//at.alicdn.com/t/font_1031158_1uhr8ri0pk5.svg#iconfont&quot;) format(&quot;svg&quot;); }</style></defs><path d="M751.388 68.267a34.133 34.133 0 0 1 0-68.267h227.556a91.022 91.022 0 0 1 91.022 91.022v227.556a34.133 34.133 0 1 1-68.266 0V91.022a22.756 22.756 0 0 0-22.756-22.755H751.388M1001.7 705.422a34.133 34.133 0 0 1 68.266 0v227.556A91.022 91.022 0 0 1 978.944 1024H748.885a34.133 34.133 0 0 1 0-68.267H978.49a22.756 22.756 0 0 0 22.755-22.755V705.422M364.09 955.733a34.133 34.133 0 1 1 0 68.267H136.533a91.022 91.022 0 0 1-91.022-91.022V705.422a34.133 34.133 0 0 1 68.267 0v227.556a22.756 22.756 0 0 0 22.755 22.755H364.09M113.778 318.578a34.133 34.133 0 1 1-68.267 0V91.022A91.022 91.022 0 0 1 136.533 0H364.09a34.133 34.133 0 0 1 0 68.267H136.533a22.756 22.756 0 0 0-22.755 22.755v227.556M34.133 477.867a34.133 34.133 0 0 0 0 68.266h168.619v-68.266z m1046.756 0H912.27v68.266h168.619a34.133 34.133 0 0 0 0-68.266zM202.752 157.24h709.746v320.627H202.752z m0 388.893h709.746V866.76H202.752z" fill="#04C361" p-id="4400"/></svg>

+ 8 - 0
yudao-ui-admin/src/utils/constants.js

@@ -132,6 +132,14 @@ export const PayChannelEnum = {
     "code": "wx_app",
     "name": "微信 APP 支付"
   },
+  WX_NATIVE: {
+    "code": "wx_native",
+    "name": "微信扫码支付"
+  },
+  WX_BAR: {
+    "code": "wx_bar",
+    "name": "微信条码支付"
+  },
   ALIPAY_PC: {
     "code": "alipay_pc",
     "name": "支付宝 PC 网站支付"

+ 71 - 33
yudao-ui-admin/src/views/pay/app/components/wechatChannelForm.vue

@@ -1,15 +1,15 @@
 <template>
   <div>
     <el-dialog :visible.sync="transferParam.wechatOpen" :title="title" @close="close" append-to-body width="800px">
-      <el-form ref="wechatJsApiForm" :model="form" :rules="rules" size="medium" label-width="100px"
+      <el-form ref="wechatJsApiForm" :model="form" :rules="rules" size="medium" label-width="120px"
                v-loading="transferParam.loading">
         <el-form-item label-width="180px" label="渠道费率" prop="feeRate">
           <el-input v-model="form.feeRate" placeholder="请输入渠道费率" clearable :style="{width: '100%'}">
             <template slot="append">%</template>
           </el-input>
         </el-form-item>
-        <el-form-item label-width="180px" label="公众号APPID" prop="weChatConfig.appId">
-          <el-input v-model="form.weChatConfig.appId" placeholder="请输入公众号APPID" clearable :style="{width: '100%'}">
+        <el-form-item label-width="180px" label="公众号 APPID" prop="weChatConfig.appId">
+          <el-input v-model="form.weChatConfig.appId" placeholder="请输入公众号 APPID" clearable :style="{width: '100%'}">
           </el-input>
         </el-form-item>
         <el-form-item label-width="180px" label="商户号" prop="weChatConfig.mchId">
@@ -29,29 +29,41 @@
             </el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item label-width="180px" label="商户密钥" prop="weChatConfig.mchKey"
-                      v-if="form.weChatConfig.apiVersion === 'v2'">
-          <el-input v-model="form.weChatConfig.mchKey" placeholder="请输入商户密钥" clearable
-                    :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
-        </el-form-item>
+        <div v-if="form.weChatConfig.apiVersion === 'v2'">
+          <el-form-item label-width="180px" label="商户密钥" prop="weChatConfig.mchKey">
+            <el-input v-model="form.weChatConfig.mchKey" placeholder="请输入商户密钥" clearable
+                      :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
+          </el-form-item>
+          <el-form-item label-width="180px" label="apiclient_cert.p12 证书" prop="weChatConfig.keyContent">
+            <el-input v-model="form.weChatConfig.keyContent" type="textarea"
+                      placeholder="请上传 apiclient_cert.p12 证书"
+                      readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
+          </el-form-item>
+          <el-form-item label-width="180px" label="">
+            <el-upload :limit="1" accept=".p12" action=""
+                       :before-upload="p12FileBeforeUpload"
+                       :http-request="keyContentUpload">
+              <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
+            </el-upload>
+          </el-form-item>
+        </div>
         <div v-if="form.weChatConfig.apiVersion === 'v3'">
-          <el-form-item label-width="180px" label="API V3密钥" prop="weChatConfig.apiV3Key">
-            <el-input v-model="form.weChatConfig.apiV3Key" placeholder="请输入API V3密钥" clearable
+          <el-form-item label-width="180px" label="API V3 密钥" prop="weChatConfig.apiV3Key">
+            <el-input v-model="form.weChatConfig.apiV3Key" placeholder="请输入 API V3 密钥" clearable
                       :style="{width: '100%'}" type="textarea" :autosize="{minRows: 8, maxRows: 8}"></el-input>
           </el-form-item>
-          <el-form-item label-width="180px" label="apiclient_key.perm证书" prop="weChatConfig.privateKeyContent">
+          <el-form-item label-width="180px" label="apiclient_key.perm 证书" prop="weChatConfig.privateKeyContent">
             <el-input v-model="form.weChatConfig.privateKeyContent" type="textarea"
-                      placeholder="请上传apiclient_key.perm证书"
+                      placeholder="请上传 apiclient_key.perm 证书"
                       readonly :autosize="{minRows: 8, maxRows: 8}" :style="{width: '100%'}"></el-input>
           </el-form-item>
           <el-form-item label-width="180px" label="" prop="privateKeyContentFile">
             <el-upload ref="privateKeyContentFile"
                        :limit="1"
-                       :accept="fileAccept"
-                       :headers="header"
+                       accept=".pem"
                        action=""
                        :before-upload="pemFileBeforeUpload"
-                       :http-request="privateKeyUpload"
+                       :http-request="privateKeyContentUpload"
             >
               <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
             </el-upload>
@@ -64,18 +76,17 @@
           <el-form-item label-width="180px" label="" prop="privateCertContentFile">
             <el-upload ref="privateCertContentFile"
                        :limit="1"
-                       :accept="fileAccept"
-                       :headers="header"
+                       accept=".pem"
                        action=""
                        :before-upload="pemFileBeforeUpload"
-                       :http-request="privateCertUpload"
+                       :http-request="privateCertContentUpload"
             >
               <el-button size="small" type="primary" icon="el-icon-upload">点击上传</el-button>
             </el-upload>
           </el-form-item>
         </div>
         <el-form-item label-width="180px" label="备注" prop="remark">
-          <el-input v-model="form.remark" :style="{width: '100%'}"></el-input>
+          <el-input v-model="form.remark" :style="{width: '100%'}" />
         </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
@@ -100,6 +111,7 @@ const defaultForm = {
     mchId: '',
     apiVersion: '',
     mchKey: '',
+    keyContent: '',
     privateKeyContent: '',
     privateCertContent: '',
     apiV3Key:'',
@@ -159,27 +171,27 @@ export default {
           message: '请输入商户密钥',
           trigger: 'blur'
         }],
+        'weChatConfig.keyContent': [{
+          required: true,
+          message: '请上传 apiclient_cert.p12 证书',
+          trigger: 'blur'
+        }],
         'weChatConfig.privateKeyContent': [{
           required: true,
-          message: '请上传apiclient_key.perm证书',
+          message: '请上传 apiclient_key.perm 证书',
           trigger: 'blur'
         }],
         'weChatConfig.privateCertContent': [{
           required: true,
-          message: '请上传apiclient_cert.perm证书',
+          message: '请上传 apiclient_cert.perm证 书',
           trigger: 'blur'
         }],
         'weChatConfig.apiV3Key': [{
           required: true,
-          message: '请上传apiV3密钥值',
+          message: '请上传 api V3 密钥值',
           trigger: 'blur'
         }],
       },
-      // 文件上传的header
-      header: {
-        "Authorization": null
-      },
-      fileAccept: ".pem",
       // 渠道状态 数据字典
       statusDictDatas: getDictDatas(DICT_TYPE.COMMON_STATUS),
       versionDictDatas: getDictDatas(DICT_TYPE.PAY_CHANNEL_WECHAT_VERSION),
@@ -218,6 +230,7 @@ export default {
           this.form.weChatConfig.apiVersion = config.apiVersion;
           this.form.weChatConfig.mchId = config.mchId;
           this.form.weChatConfig.mchKey = config.mchKey;
+          this.form.weChatConfig.keyContent = config.keyContent;
           this.form.weChatConfig.privateKeyContent = config.privateKeyContent;
           this.form.weChatConfig.privateCertContent = config.privateCertContent;
           this.form.weChatConfig.apiV3Key = config.apiV3Key;
@@ -241,7 +254,6 @@ export default {
               this.$modal.msgSuccess("修改成功");
               this.close();
             }
-
           })
         } else {
 
@@ -255,10 +267,14 @@ export default {
         }
       });
     },
-    pemFileBeforeUpload(file) {
+    /**
+     * apiclient_cert.p12、apiclient_cert.pem、apiclient_key.pem 上传前的校验
+     */
+    fileBeforeUpload(file, fileAccept) {
       let format = '.' + file.name.split(".")[1];
-      if (format !== this.fileAccept) {
-        this.$message.error('请上传指定格式"' + this.fileAccept + '"文件');
+      if (format !== fileAccept) {
+        debugger
+        this.$message.error('请上传指定格式"' + fileAccept + '"文件');
         return false;
       }
       let isRightSize = file.size / 1024 / 1024 < 2
@@ -267,19 +283,41 @@ export default {
       }
       return isRightSize
     },
-    privateKeyUpload(event) {
+    p12FileBeforeUpload(file) {
+      this.fileBeforeUpload(file, '.p12')
+    },
+    pemFileBeforeUpload(file) {
+      this.fileBeforeUpload(file, '.pem')
+    },
+    /**
+     * 读取 apiclient_key.pem 到 privateKeyContent 字段
+     */
+    privateKeyContentUpload(event) {
       const readFile = new FileReader()
       readFile.onload = (e) => {
         this.form.weChatConfig.privateKeyContent = e.target.result
       }
       readFile.readAsText(event.file);
     },
-    privateCertUpload(event) {
+    /**
+     * 读取 apiclient_cert.pem 到 privateCertContent 字段
+     */
+    privateCertContentUpload(event) {
       const readFile = new FileReader()
       readFile.onload = (e) => {
         this.form.weChatConfig.privateCertContent = e.target.result
       }
       readFile.readAsText(event.file);
+    },
+    /**
+     * 读取 apiclient_cert.p12 到 keyContent 字段
+     */
+    keyContentUpload(event) {
+      const readFile = new FileReader()
+      readFile.onload = (e) => {
+        this.form.weChatConfig.keyContent = e.target.result.split(',')[1]
+      }
+      readFile.readAsDataURL(event.file); // 读成 base64
     }
   }
 }

+ 28 - 4
yudao-ui-admin/src/views/pay/app/index.vue

@@ -143,6 +143,30 @@
             </el-button>
           </template>
         </el-table-column>
+        <el-table-column :label="payChannelEnum.WX_NATIVE.name" align="center">
+          <template v-slot="scope">
+            <el-button type="success" icon="el-icon-check" circle
+                       v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.WX_NATIVE.code)"
+                       @click="handleUpdateChannel(scope.row,payChannelEnum.WX_NATIVE.code,payType.WECHAT)">
+            </el-button>
+            <el-button v-else
+                       type="danger" icon="el-icon-close" circle
+                       @click="handleCreateChannel(scope.row,payChannelEnum.WX_NATIVE.code,payType.WECHAT)">
+            </el-button>
+          </template>
+        </el-table-column>
+        <el-table-column :label="payChannelEnum.WX_BAR.name" align="center">
+          <template v-slot="scope">
+            <el-button type="success" icon="el-icon-check" circle
+                       v-if="judgeChannelExist(scope.row.channelCodes,payChannelEnum.WX_BAR.code)"
+                       @click="handleUpdateChannel(scope.row,payChannelEnum.WX_BAR.code,payType.WECHAT)">
+            </el-button>
+            <el-button v-else
+                       type="danger" icon="el-icon-close" circle
+                       @click="handleCreateChannel(scope.row,payChannelEnum.WX_BAR.code,payType.WECHAT)">
+            </el-button>
+          </template>
+        </el-table-column>
       </el-table-column>
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template v-slot="scope">
@@ -173,8 +197,8 @@
           </el-radio-group>
         </el-form-item>
 
-        <el-form-item label="支付结果的回调地址" prop="payNotifyUrl">
-          <el-input v-model="form.payNotifyUrl" placeholder="请输入支付结果的回调地址"/>
+        <el-form-item label="支付结果的回调地址" prop="orderNotifyUrl">
+          <el-input v-model="form.orderNotifyUrl" placeholder="请输入支付结果的回调地址"/>
         </el-form-item>
         <el-form-item label="退款结果的回调地址" prop="refundNotifyUrl">
           <el-input v-model="form.refundNotifyUrl" placeholder="请输入退款结果的回调地址"/>
@@ -235,7 +259,7 @@ export default {
       rules: {
         name: [{required: true, message: "应用名不能为空", trigger: "blur"}],
         status: [{required: true, message: "开启状态不能为空", trigger: "blur"}],
-        payNotifyUrl: [{required: true, message: "支付结果的回调地址不能为空", trigger: "blur"}],
+        orderNotifyUrl: [{required: true, message: "支付结果的回调地址不能为空", trigger: "blur"}],
         refundNotifyUrl: [{required: true, message: "退款结果的回调地址不能为空", trigger: "blur"}],
       },
       // 数据字典
@@ -295,7 +319,7 @@ export default {
         name: undefined,
         status: undefined,
         remark: undefined,
-        payNotifyUrl: undefined,
+        orderNotifyUrl: undefined,
         refundNotifyUrl: undefined,
       };
       this.resetForm("form");

+ 38 - 2
yudao-ui-admin/src/views/pay/cashier/index.vue

@@ -26,7 +26,7 @@
       <!-- 微信支付 -->
       <el-descriptions title="选择微信支付" style="margin-top: 20px;" />
       <div class="pay-channel-container">
-        <div class="box" v-for="channel in channels" v-if="channel.code.indexOf('wx_') === 0" :key="channel.code">
+        <div class="box" v-for="channel in channels" v-if="channel.code.indexOf('wx_') === 0" :key="channel.code" @click="submit(channel.code)">
           <img :src="channel.icon">
           <div class="title">{{ channel.name }}</div>
         </div>
@@ -125,6 +125,14 @@ export default {
         name: '微信 App 支付',
         icon: require("@/assets/images/pay/icon/wx_app.svg"),
         code: "wx_app"
+      }, {
+        name: '微信扫码支付',
+        icon: require("@/assets/images/pay/icon/wx_native.svg"),
+        code: "wx_native"
+      }, {
+        name: '微信条码支付',
+        icon: require("@/assets/images/pay/icon/wx_bar.svg"),
+        code: "wx_bar"
       }, {
         name: '模拟支付',
         icon: require("@/assets/images/pay/icon/mock.svg"),
@@ -195,6 +203,15 @@ export default {
         }
         return;
       }
+      if (channelCode === PayChannelEnum.WX_BAR.code) {
+        this.barCode = {
+          channelCode: channelCode,
+          value: '',
+          title: '“微信”条码支付',
+          visible: true
+        }
+        return;
+      }
 
       // 默认的提交处理
       this.submit0(channelCode)
@@ -207,7 +224,16 @@ export default {
         returnUrl: location.href, // 支付成功后,支付渠道跳转回当前页;再由当前页,跳转回 {@link returnUrl} 对应的地址
         ...this.buildSubmitParam(channelCode)
       }).then(response => {
+        // 直接返回已支付的情况,例如说扫码支付
         const data = response.data
+        if (data.status === PayOrderStatusEnum.SUCCESS.status) {
+          this.clearQueryInterval();
+          this.$message.success('支付成功!');
+          this.goReturnUrl();
+          return
+        }
+
+        // 展示对应的界面
         if (data.displayMode === PayDisplayModeEnum.URL.mode) {
           this.displayUrl(channelCode, data)
         } else if (data.displayMode === PayDisplayModeEnum.QR_CODE.mode) {
@@ -230,11 +256,18 @@ export default {
           }
         }
       }
+      // ② 微信 BarCode 支付时,需要传递 authCode 条形码
+      if (channelCode === PayChannelEnum.WX_BAR.code) {
+        return {
+          "channelExtras": {
+            "authCode": this.barCode.value
+          }
+        }
+      }
       return {}
     },
     /** 提交支付后,URL 的展示形式 */
     displayUrl(channelCode, data) {
-      // window.open(data.displayContent)
       location.href = data.displayContent
       this.submitLoading = false
     },
@@ -298,6 +331,9 @@ export default {
      *                  ③ close:支付已关闭
      */
     goReturnUrl(payResult) {
+      // 清理任务
+      this.clearQueryInterval();
+
       // 未配置的情况下,只能关闭
       if (!this.returnUrl) {
         this.$tab.closePage();

+ 2 - 1
yudao-ui-admin/src/views/pay/demo/index.vue

@@ -41,7 +41,8 @@
       </el-table-column>
       <el-table-column label="退款时间" align="center" prop="refundTime" width="180">
         <template v-slot="scope">
-          <span>{{ parseTime(scope.row.refundTime) }}</span>
+          <span v-if="scope.row.refundTime">{{ parseTime(scope.row.refundTime) }}</span>
+          <span v-else-if="scope.row.payRefundId">退款中,等待退款结果</span>
         </template>
       </el-table-column>
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">

Some files were not shown because too many files changed in this diff