Browse Source

!50 支付系统的部分实现
Merge pull request !50 from 芋道源码/pay_extension

芋道源码 3 years ago
parent
commit
b336655a51
100 changed files with 4437 additions and 34 deletions
  1. 605 3
      sql/ruoyi-vue-pro.sql
  2. 2 2
      yudao-admin-server/pom.xml
  3. 0 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/infra/convert/logger/InfApiAccessLogConvert.java
  4. 6 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/pay/convert/package-info.java
  5. 1 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md
  6. 29 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/pay/job/notify/PayNotifyJob.java
  7. 1 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/pay/job/package-info.java
  8. 7 0
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/pay/package-info.java
  9. 1 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/sms/SysSmsChannelService.java
  10. 1 1
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/sms/impl/SysSmsTemplateServiceImpl.java
  11. 2 2
      yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/social/impl/SysSocialServiceImpl.java
  12. 3 0
      yudao-admin-server/src/main/resources/application-local.yaml
  13. 0 3
      yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/modules/infra/service/logger/InfApiAccessLogServiceImplTest.java
  14. 0 3
      yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/modules/infra/service/logger/InfApiErrorLogServiceImplTest.java
  15. 10 0
      yudao-core-service/pom.xml
  16. 2 2
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/infra/convert/logger/InfApiAccessLogCoreConvert.java
  17. 2 2
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/infra/convert/logger/InfApiErrorLogCoreConvert.java
  18. 1 1
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/infra/dal/dataobject/config/InfConfigDO.java
  19. 2 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/infra/dal/dataobject/logger/InfApiErrorLogDO.java
  20. 2 4
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/infra/service/logger/impl/InfApiAccessLogCoreServiceImpl.java
  21. 2 4
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/infra/service/logger/impl/InfApiErrorLogCoreServiceImpl.java
  22. 22 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/convert/order/PayOrderCoreConvert.java
  23. 6 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/convert/package-info.java
  24. 1 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md
  25. 62 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/merchant/PayAppDO.java
  26. 68 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/merchant/PayChannelDO.java
  27. 53 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/merchant/PayMerchantDO.java
  28. 49 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/notify/PayNotifyLogDO.java
  29. 99 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/notify/PayNotifyTaskDO.java
  30. 162 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/order/PayOrderDO.java
  31. 82 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/order/PayOrderExtensionDO.java
  32. 128 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/order/PayRefundDO.java
  33. 9 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/merchant/PayAppCoreMapper.java
  34. 20 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/merchant/PayChannelCoreMapper.java
  35. 9 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/notify/PayNotifyLogCoreMapper.java
  36. 30 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/notify/PayNotifyTaskCoreMapper.java
  37. 22 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/order/PayOrderCoreMapper.java
  38. 20 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/order/PayOrderExtensionCoreMapper.java
  39. 19 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/redis/PayRedisKeyCoreConstants.java
  40. 39 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/redis/notify/PayNotifyLockCoreRedisDAO.java
  41. 31 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/PayErrorCodeCoreConstants.java
  42. 32 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/notify/PayNotifyStatusEnum.java
  43. 28 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/notify/PayNotifyTypeEnum.java
  44. 29 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/order/PayOrderNotifyStatusEnum.java
  45. 29 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/order/PayOrderRefundStatusEnum.java
  46. 29 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/order/PayOrderStatusEnum.java
  47. 7 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/package-info.java
  48. 23 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/merchant/PayAppCoreService.java
  49. 39 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/merchant/PayChannelCoreService.java
  50. 43 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/merchant/impl/PayAppCoreServiceImpl.java
  51. 121 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/merchant/impl/PayChannelCoreServiceImpl.java
  52. 29 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/PayNotifyCoreService.java
  53. 32 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/dto/PayNotifyTaskCreateReqDTO.java
  54. 256 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/impl/PayNotifyCoreServiceImpl.java
  55. 28 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/vo/PayNotifyOrderReqVO.java
  56. 28 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/vo/PayRefundOrderReqVO.java
  57. 6 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/vo/package-info.java
  58. 51 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayOrderCoreService.java
  59. 64 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/dto/PayOrderCreateReqDTO.java
  60. 47 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/dto/PayOrderSubmitReqDTO.java
  61. 23 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/dto/PayOrderSubmitRespDTO.java
  62. 242 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayOrderCoreServiceImpl.java
  63. 1 0
      yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/package-info.java
  64. 38 0
      yudao-core-service/src/test-integration/java/cn/iocoder/yudao/coreservice/BaseDbAndRedisIntegrationTest.java
  65. 30 0
      yudao-core-service/src/test-integration/java/cn/iocoder/yudao/coreservice/BaseDbIntegrationTest.java
  66. 23 0
      yudao-core-service/src/test-integration/java/cn/iocoder/yudao/coreservice/BaseRedisIntegrationTest.java
  67. 29 0
      yudao-core-service/src/test-integration/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/merchant/PayChannelDOTest.java
  68. 56 0
      yudao-core-service/src/test-integration/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/merchant/PayChannelCoreMapperTest.java
  69. 52 0
      yudao-core-service/src/test-integration/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayOrderCoreServiceTest.java
  70. 1 0
      yudao-core-service/src/test-integration/java/cn/iocoder/yudao/coreservice/modules/pay/service/package-info.java
  71. 92 0
      yudao-core-service/src/test-integration/resources/application-integration-test.yaml
  72. 2 3
      yudao-core-service/src/test/java/cn/iocoder/yudao/coreservice/modules/infra/service/logger/InfApiAccessLogCoreServiceTest.java
  73. 2 2
      yudao-core-service/src/test/java/cn/iocoder/yudao/coreservice/modules/infra/service/logger/InfApiErrorLogCoreServiceTest.java
  74. 18 0
      yudao-dependencies/pom.xml
  75. 3 0
      yudao-framework/pom.xml
  76. 46 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java
  77. 34 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java
  78. 9 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/ObjectUtils.java
  79. 76 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml
  80. 32 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java
  81. 22 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java
  82. 33 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/AbstractPayCodeMapping.java
  83. 37 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
  84. 16 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientConfig.java
  85. 28 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientFactory.java
  86. 57 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCommonResult.java
  87. 45 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayOrderNotifyRespDTO.java
  88. 76 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/PayOrderUnifiedReqDTO.java
  89. 97 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
  90. 71 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java
  91. 89 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java
  92. 24 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayCodeMapping.java
  93. 74 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java
  94. 70 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java
  95. 56 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXCodeMapping.java
  96. 88 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPayClientConfig.java
  97. 144 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java
  98. 41 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayChannelEnum.java
  99. 27 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/PayFrameworkErrorCodeConstants.java
  100. 2 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring.factories

File diff suppressed because it is too large
+ 605 - 3
sql/ruoyi-vue-pro.sql


+ 2 - 2
yudao-admin-server/pom.xml

@@ -122,11 +122,11 @@
             <artifactId>screw-core</artifactId> <!-- 实现数据库文档 -->
         </dependency>
 
-        <!-- TODO 后续看情况,进行调整 -->
+        <!-- 三方云服务相关 -->
+
         <dependency>
             <groupId>com.xkcoding.justauth</groupId>
             <artifactId>justauth-spring-boot-starter</artifactId>
-            <version>1.4.0</version>
         </dependency>
     </dependencies>
 

+ 0 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/infra/convert/logger/InfApiAccessLogConvert.java

@@ -2,7 +2,6 @@ package cn.iocoder.yudao.adminserver.modules.infra.convert.logger;
 
 import cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger.InfApiAccessLogDO;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateDTO;
 import cn.iocoder.yudao.adminserver.modules.infra.controller.logger.vo.apiaccesslog.InfApiAccessLogExcelVO;
 import cn.iocoder.yudao.adminserver.modules.infra.controller.logger.vo.apiaccesslog.InfApiAccessLogRespVO;
 import org.mapstruct.Mapper;

+ 6 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/pay/convert/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * 提供 POJO 类的实体转换
+ *
+ * 目前使用 MapStruct 框架
+ */
+package cn.iocoder.yudao.adminserver.modules.pay.convert;

+ 1 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md

@@ -0,0 +1 @@
+<http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao>

+ 29 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/pay/job/notify/PayNotifyJob.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.adminserver.modules.pay.job.notify;
+
+import cn.iocoder.yudao.coreservice.modules.pay.service.notify.PayNotifyCoreService;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 支付通知 Job
+ * 通过不断扫描待通知的 PayNotifyTaskDO 记录,回调业务线的回调接口
+ *
+ * @author 芋道源码
+ */
+@Component
+@Slf4j
+public class PayNotifyJob implements JobHandler {
+
+    @Resource
+    private PayNotifyCoreService payNotifyCoreService;
+
+    @Override
+    public String execute(String param) throws Exception {
+        int notifyCount = payNotifyCoreService.executeNotify();
+        return String.format("执行支付通知 %s 个", notifyCount);
+    }
+
+}

+ 1 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/pay/job/package-info.java

@@ -0,0 +1 @@
+package cn.iocoder.yudao.adminserver.modules.pay.job;

+ 7 - 0
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/pay/package-info.java

@@ -0,0 +1,7 @@
+/**
+ * pay 包下,我们放支付业务,提供业务的支付能力。
+ * 例如说:商户、应用、支付、退款等等
+ *
+ * 缩写:pay
+ */
+package cn.iocoder.yudao.adminserver.modules.pay;

+ 1 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/sms/SysSmsChannelService.java

@@ -11,7 +11,7 @@ import java.util.Collection;
 import java.util.List;
 
 /**
- * 短信渠道Service接口
+ * 短信渠道 Service 接口
  *
  * @author zzf
  * @date 2021/1/25 9:24

+ 1 - 1
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/sms/impl/SysSmsTemplateServiceImpl.java

@@ -33,7 +33,7 @@ import static cn.iocoder.yudao.adminserver.modules.system.enums.SysErrorCodeCons
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 
 /**
- * 短信模板Service实现类
+ * 短信模板 Service 实现类
  *
  * @author zzf
  * @date 2021/1/25 9:25

+ 2 - 2
yudao-admin-server/src/main/java/cn/iocoder/yudao/adminserver/modules/system/service/social/impl/SysSocialServiceImpl.java

@@ -19,9 +19,9 @@ import me.zhyd.oauth.request.AuthRequest;
 import me.zhyd.oauth.utils.AuthStateUtils;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
-import javax.validation.Valid;
 import java.util.List;
 import java.util.Objects;
 
@@ -36,7 +36,7 @@ import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString
  * @author 芋道源码
  */
 @Service
-@Valid
+@Validated
 @Slf4j
 public class SysSocialServiceImpl implements SysSocialService {
 

+ 3 - 0
yudao-admin-server/src/main/resources/application-local.yaml

@@ -166,6 +166,9 @@ yudao:
     exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系
       - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求
       - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
+  pay:
+    pay-notify-url: http://niubi.natapp1.cc/api/pay/order/notify
+    refund-notify-url: http://niubi.natapp1.cc/api/pay/refund/notify
   demo: false # 关闭演示模式
 
 justauth:

+ 0 - 3
yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/modules/infra/service/logger/InfApiAccessLogServiceImplTest.java

@@ -1,12 +1,10 @@
 package cn.iocoder.yudao.adminserver.modules.infra.service.logger;
 
-import cn.hutool.core.util.RandomUtil;
 import cn.iocoder.yudao.adminserver.BaseDbUnitTest;
 import cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger.InfApiAccessLogDO;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateDTO;
 import cn.iocoder.yudao.adminserver.modules.infra.controller.logger.vo.apiaccesslog.InfApiAccessLogExportReqVO;
 import cn.iocoder.yudao.adminserver.modules.infra.controller.logger.vo.apiaccesslog.InfApiAccessLogPageReqVO;
 import cn.iocoder.yudao.adminserver.modules.infra.dal.mysql.logger.InfApiAccessLogMapper;
@@ -19,7 +17,6 @@ import org.springframework.context.annotation.Import;
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
-import java.util.concurrent.Future;
 
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.buildTime;

+ 0 - 3
yudao-admin-server/src/test/java/cn/iocoder/yudao/adminserver/modules/infra/service/logger/InfApiErrorLogServiceImplTest.java

@@ -1,11 +1,9 @@
 package cn.iocoder.yudao.adminserver.modules.infra.service.logger;
 
-import cn.hutool.core.util.RandomUtil;
 import cn.iocoder.yudao.adminserver.BaseDbUnitTest;
 import cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger.InfApiErrorLogDO;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateDTO;
 import cn.iocoder.yudao.adminserver.modules.infra.controller.logger.vo.apierrorlog.InfApiErrorLogExportReqVO;
 import cn.iocoder.yudao.adminserver.modules.infra.controller.logger.vo.apierrorlog.InfApiErrorLogPageReqVO;
 import cn.iocoder.yudao.adminserver.modules.infra.dal.mysql.logger.InfApiErrorLogMapper;
@@ -19,7 +17,6 @@ import org.springframework.context.annotation.Import;
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.List;
-import java.util.concurrent.Future;
 
 import static cn.iocoder.yudao.adminserver.modules.infra.enums.InfErrorCodeConstants.API_ERROR_LOG_NOT_FOUND;
 import static cn.iocoder.yudao.adminserver.modules.infra.enums.InfErrorCodeConstants.API_ERROR_LOG_PROCESSED;

+ 10 - 0
yudao-core-service/pom.xml

@@ -32,6 +32,10 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-biz-sms</artifactId>
         </dependency>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
+        </dependency>
 
         <!-- Web 相关 -->
         <dependency>
@@ -73,6 +77,12 @@
             <artifactId>yudao-spring-boot-starter-mq</artifactId>
         </dependency>
 
+        <!-- 服务保障相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-protection</artifactId>
+        </dependency>
+
         <!-- Test 测试相关 -->
         <dependency>
             <groupId>cn.iocoder.boot</groupId>

+ 2 - 2
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/infra/convert/logger/InfApiAccessLogCoreConvert.java

@@ -1,6 +1,6 @@
 package cn.iocoder.yudao.coreservice.modules.infra.convert.logger;
 
-import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateDTO;
+import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateReqDTO;
 import cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger.InfApiAccessLogDO;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
@@ -10,6 +10,6 @@ public interface InfApiAccessLogCoreConvert {
 
     InfApiAccessLogCoreConvert INSTANCE = Mappers.getMapper(InfApiAccessLogCoreConvert.class);
 
-    InfApiAccessLogDO convert(ApiAccessLogCreateDTO bean);
+    InfApiAccessLogDO convert(ApiAccessLogCreateReqDTO bean);
 
 }

+ 2 - 2
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/infra/convert/logger/InfApiErrorLogCoreConvert.java

@@ -1,6 +1,6 @@
 package cn.iocoder.yudao.coreservice.modules.infra.convert.logger;
 
-import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateDTO;
+import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateReqDTO;
 import cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger.InfApiErrorLogDO;
 import org.mapstruct.Mapper;
 import org.mapstruct.factory.Mappers;
@@ -10,6 +10,6 @@ public interface InfApiErrorLogCoreConvert {
 
     InfApiErrorLogCoreConvert INSTANCE = Mappers.getMapper(InfApiErrorLogCoreConvert.class);
 
-    InfApiErrorLogDO convert(ApiErrorLogCreateDTO bean);
+    InfApiErrorLogDO convert(ApiErrorLogCreateReqDTO bean);
 
 }

+ 1 - 1
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/infra/dal/dataobject/config/InfConfigDO.java

@@ -11,7 +11,7 @@ import lombok.ToString;
 /**
  * 参数配置表
  *
- * @author ruoyi
+ * @author 芋道源码
  */
 @TableName("inf_config")
 @Data

+ 2 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/infra/dal/dataobject/logger/InfApiErrorLogDO.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 import cn.iocoder.yudao.coreservice.modules.infra.enums.logger.InfApiErrorLogProcessStatusEnum;
+import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.*;
 
@@ -25,6 +26,7 @@ public class InfApiErrorLogDO extends BaseDO {
     /**
      * 编号
      */
+    @TableId
     private Long id;
     /**
      * 用户编号

+ 2 - 4
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/infra/service/logger/impl/InfApiAccessLogCoreServiceImpl.java

@@ -1,18 +1,16 @@
 package cn.iocoder.yudao.coreservice.modules.infra.service.logger.impl;
 
 import cn.iocoder.yudao.coreservice.modules.infra.convert.logger.InfApiAccessLogCoreConvert;
-import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateDTO;
+import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateReqDTO;
 import cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger.InfApiAccessLogDO;
 import cn.iocoder.yudao.coreservice.modules.infra.dal.mysql.logger.InfApiAccessLogCoreMapper;
 import cn.iocoder.yudao.coreservice.modules.infra.service.logger.InfApiAccessLogCoreService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.scheduling.annotation.Async;
-import org.springframework.scheduling.annotation.AsyncResult;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
-import java.util.concurrent.Future;
 
 /**
  * API 访问日志 Service 实现类
@@ -29,7 +27,7 @@ public class InfApiAccessLogCoreServiceImpl implements InfApiAccessLogCoreServic
 
     @Override
     @Async
-    public void createApiAccessLogAsync(ApiAccessLogCreateDTO createDTO) {
+    public void createApiAccessLogAsync(ApiAccessLogCreateReqDTO createDTO) {
         InfApiAccessLogDO apiAccessLog = InfApiAccessLogCoreConvert.INSTANCE.convert(createDTO);
         apiAccessLogMapper.insert(apiAccessLog);
     }

+ 2 - 4
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/infra/service/logger/impl/InfApiErrorLogCoreServiceImpl.java

@@ -5,15 +5,13 @@ import cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger.InfApiEr
 import cn.iocoder.yudao.coreservice.modules.infra.dal.mysql.logger.InfApiErrorLogCoreMapper;
 import cn.iocoder.yudao.coreservice.modules.infra.enums.logger.InfApiErrorLogProcessStatusEnum;
 import cn.iocoder.yudao.coreservice.modules.infra.service.logger.InfApiErrorLogCoreService;
-import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateDTO;
+import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateReqDTO;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.scheduling.annotation.Async;
-import org.springframework.scheduling.annotation.AsyncResult;
 import org.springframework.stereotype.Service;
 import org.springframework.validation.annotation.Validated;
 
 import javax.annotation.Resource;
-import java.util.concurrent.Future;
 
 /**
  * API 错误日志 Service 实现类
@@ -30,7 +28,7 @@ public class InfApiErrorLogCoreServiceImpl implements InfApiErrorLogCoreService
 
     @Override
     @Async
-    public void createApiErrorLogAsync(ApiErrorLogCreateDTO createDTO) {
+    public void createApiErrorLogAsync(ApiErrorLogCreateReqDTO createDTO) {
         InfApiErrorLogDO apiErrorLog = InfApiErrorLogCoreConvert.INSTANCE.convert(createDTO);
         apiErrorLog.setProcessStatus(InfApiErrorLogProcessStatusEnum.INIT.getStatus());
         apiErrorLogMapper.insert(apiErrorLog);

+ 22 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/convert/order/PayOrderCoreConvert.java

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.coreservice.modules.pay.convert.order;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderExtensionDO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderCreateReqDTO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface PayOrderCoreConvert {
+
+    PayOrderCoreConvert INSTANCE = Mappers.getMapper(PayOrderCoreConvert.class);
+
+    PayOrderDO convert(PayOrderCreateReqDTO bean);
+
+    PayOrderExtensionDO convert(PayOrderSubmitReqDTO bean);
+
+    PayOrderUnifiedReqDTO convert2(PayOrderSubmitReqDTO bean);
+
+}

+ 6 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/convert/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * 提供 POJO 类的实体转换
+ *
+ * 目前使用 MapStruct 框架
+ */
+package cn.iocoder.yudao.coreservice.modules.pay.convert;

+ 1 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md

@@ -0,0 +1 @@
+<http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao>

+ 62 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/merchant/PayAppDO.java

@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 支付应用 DO
+ * 一个商户下,可能会有多个支付应用。例如说,京东有京东商城、京东到家等等
+ * 不过一般来说,一个商户,只有一个应用哈~
+ *
+ * 即 PayMerchantDO : PayAppDO = 1 : n
+ *
+ * @author 芋道源码
+ */
+@TableName("pay_app")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PayAppDO extends BaseDO {
+
+    /**
+     * 应用编号,数据库自增
+     */
+    @TableId
+    private Long id;
+    /**
+     * 应用名
+     */
+    private String name;
+    /**
+     * 状态
+     *
+     * 枚举 {@link CommonStatusEnum}
+     */
+    private Integer status;
+    /**
+     * 备注
+     */
+    private String remark;
+    /**
+     * 支付结果的回调地址
+     */
+    private String payNotifyUrl;
+    /**
+     * 退款结果的回调地址
+     */
+    private String refundNotifyUrl;
+
+    /**
+     * 商户编号
+     *
+     * 关联 {@link PayMerchantDO#getId()}
+     */
+    private Long merchantId;
+
+}

+ 68 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/merchant/PayChannelDO.java

@@ -0,0 +1,68 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import lombok.*;
+
+/**
+ * 支付渠道 DO
+ * 一个应用下,会有多种支付渠道,例如说微信支付、支付宝支付等等
+ *
+ * 即 PayAppDO : PayChannelDO = 1 : n
+ *
+ * @author 芋道源码
+ */
+@Data
+@TableName(value = "pay_channel", autoResultMap = true)
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PayChannelDO extends BaseDO {
+
+    /**
+     * 渠道编号,数据库自增
+     */
+    private Long id;
+    /**
+     * 渠道编码
+     *
+     * 枚举 {@link PayChannelEnum}
+     */
+    private String code;
+    /**
+     * 状态
+     *
+     * 枚举 {@link CommonStatusEnum}
+     */
+    private Integer status;
+    /**
+     * 渠道费率,单位:百分比
+     */
+    private Double feeRate;
+
+    /**
+     * 商户编号
+     *
+     * 关联 {@link PayMerchantDO#getId()}
+     */
+    private Long merchantId;
+    /**
+     * 应用编号
+     *
+     * 关联 {@link PayAppDO#getId()}
+     */
+    private Long appId;
+    /**
+     * 支付渠道配置
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private PayClientConfig config;
+
+}

+ 53 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/merchant/PayMerchantDO.java

@@ -0,0 +1,53 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 支付商户信息 DO
+ * 目前暂时没有特别的用途,主要为未来多商户提供基础。
+ *
+ * @author 芋道源码
+ */
+@Data
+@TableName("pay_merchant")
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PayMerchantDO extends BaseDO {
+
+    /**
+     * 商户编号,数据库自增
+     */
+    @TableId
+    private Long id;
+    /**
+     * 商户号
+     * 例如说,M233666999
+     */
+    private String no;
+    /**
+     * 商户全称
+     */
+    private String name;
+    /**
+     * 商户简称
+     */
+    private String shortName;
+    /**
+     * 状态
+     *
+     * 枚举 {@link CommonStatusEnum}
+     */
+    private Integer status;
+    /**
+     * 备注
+     */
+    private String remark;
+
+}

+ 49 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/notify/PayNotifyLogDO.java

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify;
+
+import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyStatusEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+/**
+ * 商户支付、退款等的通知 Log
+ * 每次通知时,都会在该表中,记录一次 Log,方便排查问题
+ *
+ * @author 芋道源码
+ */
+@TableName("pay_notify_log")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PayNotifyLogDO extends BaseDO {
+
+    /**
+     * 日志编号,自增
+     */
+    private Long id;
+    /**
+     * 通知任务编号
+     *
+     * 关联 {@link PayNotifyTaskDO#getId()}
+     */
+    private Long taskId;
+    /**
+     * 第几次被通知
+     *
+     * 对应到 {@link PayNotifyTaskDO#getNotifyTimes()}
+     */
+    private Integer notifyTimes;
+    /**
+     * HTTP 响应结果
+     */
+    private String response;
+    /**
+     * 支付通知状态
+     *
+     * 外键 {@link PayNotifyStatusEnum}
+     */
+    private Integer status;
+
+}

+ 99 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/notify/PayNotifyTaskDO.java

@@ -0,0 +1,99 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayMerchantDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayRefundDO;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyStatusEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+import java.util.Date;
+
+/**
+ * 商户支付、退款等的通知
+ * 在支付系统收到支付渠道的支付、退款的结果后,需要不断的通知到业务系统,直到成功。
+ *
+ * @author 芋道源码
+ */
+@TableName("pay_notify_task")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+public class PayNotifyTaskDO extends BaseDO {
+
+    /**
+     * 通知频率,单位为秒。
+     *
+     * 算上首次的通知,实际是一共 1 + 8 = 9 次。
+     */
+    public static final Integer[] NOTIFY_FREQUENCY = new Integer[]{
+            15, 15, 30, 180,
+            1800, 1800, 1800, 3600
+    };
+
+    /**
+     * 编号,自增
+     */
+    private Long id;
+    /**
+     * 商户编号
+     *
+     * 关联 {@link PayMerchantDO#getId()}
+     */
+    private Long merchantId;
+    /**
+     * 应用编号
+     *
+     * 关联 {@link PayAppDO#getId()}
+     */
+    private Long appId;
+    /**
+     * 通知类型
+     *
+     * 外键 {@link PayNotifyTypeEnum}
+     */
+    private Integer type;
+    /**
+     * 数据编号,根据不同 type 进行关联:
+     *
+     * 1. {@link PayNotifyTypeEnum#ORDER} 时,关联 {@link PayOrderDO#getId()}
+     * 2. {@link PayNotifyTypeEnum#REFUND} 时,关联 {@link PayRefundDO#getId()}
+     */
+    private Long dataId;
+    /**
+     * 商户订单编号
+     */
+    private String merchantOrderId;
+    /**
+     * 通知状态
+     *
+     * 外键 {@link PayNotifyStatusEnum}
+     */
+    private Integer status;
+    /**
+     * 下一次通知时间
+     */
+    private Date nextNotifyTime;
+    /**
+     * 最后一次执行时间
+     */
+    private Date lastExecuteTime;
+    /**
+     * 当前通知次数
+     */
+    private Integer notifyTimes;
+    /**
+     * 最大可通知次数
+     */
+    private Integer maxNotifyTimes;
+    /**
+     * 通知地址
+     */
+    private String notifyUrl;
+
+}

+ 162 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/order/PayOrderDO.java

@@ -0,0 +1,162 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayMerchantDO;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderNotifyStatusEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderRefundStatusEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderStatusEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.util.Date;
+
+/**
+ * 支付订单 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("pay_order")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PayOrderDO extends BaseDO {
+
+    /**
+     * 订单编号,数据库自增
+     */
+    private Long id;
+    /**
+     * 商户编号
+     *
+     * 关联 {@link PayMerchantDO#getId()}
+     */
+    private Long merchantId;
+    /**
+     * 应用编号
+     *
+     * 关联 {@link PayAppDO#getId()}
+     */
+    private Long appId;
+    /**
+     * 渠道编号
+     *
+     * 关联 {@link PayChannelDO#getId()}
+     */
+    private Long channelId;
+    /**
+     * 渠道编码
+     *
+     * 枚举 {@link PayChannelEnum}
+     */
+    private String channelCode;
+
+    // ========== 商户相关字段 ==========
+
+    /**
+     * 商户订单编号
+     * 例如说,内部系统 A 的订单号。需要保证每个 PayMerchantDO 唯一
+     */
+    private String merchantOrderId;
+    /**
+     * 商品标题
+     */
+    private String subject;
+    /**
+     * 商品描述信息
+     */
+    private String body;
+    /**
+     * 异步通知地址
+     */
+    private String notifyUrl;
+    /**
+     * 通知商户支付结果的回调状态
+     *
+     * 枚举 {@link PayOrderNotifyStatusEnum}
+     */
+    private Integer notifyStatus;
+//    /**
+//     * 商户拓展参数
+//     */
+//    private Map<String, String> merchantExtras;
+
+    // ========== 订单相关字段 ==========
+
+    /**
+     * 支付金额,单位:分
+     */
+    private Long amount;
+    /**
+     * 渠道手续费,单位:百分比
+     *
+     * 冗余 {@link PayChannelDO#getFeeRate()}
+     */
+    private Double channelFeeRate;
+    /**
+     * 渠道手续金额,单位:分
+     */
+    private Long channelFeeAmount;
+    /**
+     * 支付状态
+     *
+     * 枚举 {@link PayOrderStatusEnum}
+     */
+    private Integer status;
+    /**
+     * 用户 IP
+     */
+    private String userIp;
+    /**
+     * 订单失效时间
+     */
+    private Date expireTime;
+    /**
+     * 订单支付成功时间
+     */
+    private Date successTime;
+    /**
+     * 订单支付通知时间,即支付渠道的通知时间
+     */
+    private Date notifyTime;
+    /**
+     * 支付成功的订单拓展单编号
+     *
+     * 关联 {@link PayOrderDO#getId()}
+     */
+    private Long successExtensionId;
+
+    // ========== 退款相关字段 ==========
+    /**
+     * 退款状态
+     *
+     * 枚举 {@link PayOrderRefundStatusEnum}
+     */
+    private Integer refundStatus;
+    /**
+     * 退款次数
+     */
+    private Integer refundTimes;
+    /**
+     * 退款总金额,单位:分
+     */
+    private Long refundAmount;
+
+    // ========== 渠道相关字段 ==========
+    /**
+     * 渠道用户编号
+     *
+     * 例如说,微信 openid、支付宝账号
+     */
+    private String channelUserId;
+    /**
+     * 渠道订单号
+     */
+    private String channelOrderNo;
+
+}

+ 82 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/order/PayOrderExtensionDO.java

@@ -0,0 +1,82 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderStatusEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
+import lombok.*;
+
+import java.util.Map;
+
+/**
+ * 支付订单拓展 DO
+ *
+ *
+ * @author 芋道源码
+ */
+@TableName("pay_order_extension")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+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
+     *
+     * 例如说,P202110132239124200055
+     */
+    private String no;
+    /**
+     * 订单号
+     *
+     * 关联 {@link PayOrderDO#getId()}
+     */
+    private Long orderId;
+    /**
+     * 渠道编号
+     *
+     * 关联 {@link PayChannelDO#getId()}
+     */
+    private Long channelId;
+    /**
+     * 渠道编码
+     */
+    private String channelCode;
+    /**
+     * 用户 IP
+     */
+    private String userIp;
+    /**
+     * 支付状态
+     *
+     * 枚举 {@link PayOrderStatusEnum}
+     * 注意,只包含上述枚举的 WAITING 和 SUCCESS
+     */
+    private Integer status;
+    /**
+     * 支付渠道的额外参数
+     *
+     * 参见 https://www.pingxx.com/api/支付渠道%20extra%20参数说明.html
+     */
+    @TableField(typeHandler = JacksonTypeHandler.class)
+    private Map<String, String> channelExtras;
+    /**
+     * 支付渠道异步通知的内容
+     *
+     * 在支持成功后,会记录回调的数据
+     */
+    private String channelNotifyData;
+
+}

+ 128 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/order/PayRefundDO.java

@@ -0,0 +1,128 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayMerchantDO;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 支付退款单 DO
+ * 一个支付订单,可以拥有多个支付退款单
+ *
+ * 即 PayOrderDO : PayRefundDO = 1 : n
+ *
+ * @author 芋道源码
+ */
+@Data
+public class PayRefundDO extends BaseDO {
+
+    /**
+     * 退款单编号,数据库自增
+     */
+    private Long id;
+    /**
+     * 退款单号,根据规则生成
+     *
+     * 例如说,R202109181134287570000
+     */
+    private String no;
+    /**
+     * 商户编号
+     *
+     * 关联 {@link PayMerchantDO#getId()}
+     */
+    private Long merchantId;
+    /**
+     * 应用编号
+     *
+     * 关联 {@link PayAppDO#getId()}
+     */
+    private Long appId;
+    /**
+     * 渠道编号
+     *
+     * 关联 {@link PayChannelDO#getId()}
+     */
+    private Long channelId;
+    /**
+     * 商户编码
+     *
+     * 枚举 {@link PayChannelEnum}
+     */
+    private String channelCode;
+    /**
+     * 订单编号
+     *
+     * 关联 {@link PayOrderDO#getId()}
+     */
+    private Long orderId;
+
+    // ========== 商户相关字段 ==========
+    /**
+     * 商户退款订单号
+     * 例如说,内部系统 A 的退款订单号。需要保证每个 PayMerchantDO 唯一 TODO 芋艿:需要在测试下
+     */
+    private String merchantRefundNo;
+//    /**
+//     * 商户拓展参数
+//     */
+//    private String merchantExtra;
+    /**
+     * 异步通知地址
+     */
+    private String notifyUrl;
+    /**
+     * 通知商户退款结果的回调状态
+     * TODO 芋艿:0 未发送 1 已发送
+     */
+    private Integer notifyStatus;
+
+    // ========== 退款相关字段 ==========
+    /**
+     * 退款状态
+     *
+     * TODO 芋艿:状态枚举
+     */
+    private Integer status;
+    /**
+     * 用户 IP
+     */
+    private String userIp;
+    /**
+     * 退款金额,单位:分
+     */
+    private Long amount;
+    /**
+     * 退款原因
+     */
+    private String reason;
+    /**
+     * 订单退款成功时间
+     */
+    private Date successTime;
+    /**
+     * 退款失效时间
+     */
+    private Date expireTime;
+    /**
+     * 支付渠道的额外参数
+     *
+     * 参见 https://www.pingxx.com/api/Refunds%20退款概述.html
+     */
+    private String channelExtra;
+
+    // ========== 渠道相关字段 ==========
+    /**
+     * 渠道订单号
+     */
+    private String channelOrderNo;
+    /**
+     * 渠道退款号
+     */
+    private String channelRefundNo;
+
+}

+ 9 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/merchant/PayAppCoreMapper.java

@@ -0,0 +1,9 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.merchant;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface PayAppCoreMapper extends BaseMapperX<PayAppDO> {
+}

+ 20 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/merchant/PayChannelCoreMapper.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.merchant;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.Date;
+
+@Mapper
+public interface PayChannelCoreMapper extends BaseMapperX<PayChannelDO> {
+
+    default PayChannelDO selectByAppIdAndCode(Long appId, String code) {
+        return selectOne("app_id", appId, "code", code);
+    }
+
+    @Select("SELECT id FROM pay_channel WHERE update_time > #{maxUpdateTime} LIMIT 1")
+    Long selectExistsByUpdateTimeAfter(Date maxUpdateTime);
+
+}

+ 9 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/notify/PayNotifyLogCoreMapper.java

@@ -0,0 +1,9 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyLogDO;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface PayNotifyLogCoreMapper extends BaseMapperX<PayNotifyLogDO> {
+}

+ 30 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/notify/PayNotifyTaskCoreMapper.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyTaskDO;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyStatusEnum;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.Date;
+import java.util.List;
+
+@Mapper
+public interface PayNotifyTaskCoreMapper extends BaseMapperX<PayNotifyTaskDO> {
+
+    /**
+     * 获得需要通知的 PayNotifyTaskDO 记录。需要满足如下条件:
+     *
+     * 1. status 非成功
+     * 2. nextNotifyTime 小于当前时间
+     *
+     * @return PayTransactionNotifyTaskDO 数组
+     */
+    default List<PayNotifyTaskDO> selectListByNotify() {
+        return selectList(new QueryWrapper<PayNotifyTaskDO>()
+                .in("status", PayNotifyStatusEnum.WAITING.getStatus(), PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus(),
+                        PayNotifyStatusEnum.REQUEST_FAILURE.getStatus())
+                .le("next_notify_time", new Date()));
+    }
+
+}

+ 22 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/order/PayOrderCoreMapper.java

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderExtensionDO;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface PayOrderCoreMapper extends BaseMapperX<PayOrderDO> {
+
+    default PayOrderDO selectByAppIdAndMerchantOrderId(Long appId, String merchantOrderId) {
+        return selectOne(new QueryWrapper<PayOrderDO>().eq("app_id", appId)
+                .eq("merchant_order_id", merchantOrderId));
+    }
+
+    default int updateByIdAndStatus(Long id, Integer status, PayOrderDO update) {
+        return update(update, new QueryWrapper<PayOrderDO>()
+                .eq("id", id).eq("status", status));
+    }
+
+}

+ 20 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/order/PayOrderExtensionCoreMapper.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderExtensionDO;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface PayOrderExtensionCoreMapper extends BaseMapperX<PayOrderExtensionDO> {
+
+    default PayOrderExtensionDO selectByNo(String no) {
+        return selectOne("no", no);
+    }
+
+    default int updateByIdAndStatus(Long id, Integer status, PayOrderExtensionDO update) {
+        return update(update, new QueryWrapper<PayOrderExtensionDO>()
+                .eq("id", id).eq("status", status));
+    }
+
+}

+ 19 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/redis/PayRedisKeyCoreConstants.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.redis;
+
+import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
+import org.redisson.api.RLock;
+
+import static cn.iocoder.yudao.framework.redis.core.RedisKeyDefine.KeyTypeEnum.HASH;
+
+/**
+ * Lock4j Redis Key 枚举类
+ *
+ * @author 芋道源码
+ */
+public interface PayRedisKeyCoreConstants {
+
+    RedisKeyDefine PAY_NOTIFY_LOCK = new RedisKeyDefine("通知任务的分布式锁",
+            "pay_notify:lock:", // 参数来自 DefaultLockKeyBuilder 类
+            HASH, RLock.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); // Redisson 的 Lock 锁,使用 Hash 数据结构
+
+}

+ 39 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/dal/redis/notify/PayNotifyLockCoreRedisDAO.java

@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.redis.notify;
+
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.stereotype.Repository;
+
+import javax.annotation.Resource;
+import java.util.concurrent.TimeUnit;
+
+import static cn.iocoder.yudao.coreservice.modules.pay.dal.redis.PayRedisKeyCoreConstants.PAY_NOTIFY_LOCK;
+
+/**
+ * 支付通知的锁 Redis DAO
+ *
+ * @author 芋道源码
+ */
+@Repository
+public class PayNotifyLockCoreRedisDAO {
+
+    @Resource
+    private RedissonClient redissonClient;
+
+    public void lock(Long id, Long timeoutMillis, Runnable runnable) {
+        String lockKey = formatKey(id);
+        RLock lock = redissonClient.getLock(lockKey);
+        try {
+            lock.lock(timeoutMillis, TimeUnit.MILLISECONDS);
+            // 执行逻辑
+            runnable.run();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    private static String formatKey(Long id) {
+        return String.format(PAY_NOTIFY_LOCK.getKeyTemplate(), id);
+    }
+
+}

+ 31 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/PayErrorCodeCoreConstants.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.coreservice.modules.pay.enums;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+
+/**
+ * Pay 错误码 Core 枚举类
+ *
+ * pay 系统,使用 1-007-000-000 段
+ */
+public interface PayErrorCodeCoreConstants {
+
+    // ========== APP 模块 1-007-000-000 ==========
+    ErrorCode PAY_APP_NOT_FOUND = new ErrorCode(1007000000, "App 不存在");
+    ErrorCode PAY_APP_IS_DISABLE = new ErrorCode(1007000002, "App 已经被禁用");
+
+    // ========== CHANNEL 模块 1-007-001-000 ==========
+    ErrorCode PAY_CHANNEL_NOT_FOUND = new ErrorCode(1007001000, "支付渠道的配置不存在");
+    ErrorCode PAY_CHANNEL_IS_DISABLE = new ErrorCode(1007001001, "支付渠道已经禁用");
+    ErrorCode PAY_CHANNEL_CLIENT_NOT_FOUND = new ErrorCode(1007001002, "支付渠道的客户端不存在");
+
+    // ========== ORDER 模块 1-007-002-000 ==========
+    ErrorCode PAY_ORDER_NOT_FOUND = new ErrorCode(1007002000, "支付订单不存在");
+    ErrorCode PAY_ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1007002001, "支付订单不处于待支付");
+    ErrorCode PAY_ORDER_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007002002, "支付订单不处于已支付");
+    ErrorCode PAY_ORDER_ERROR_USER = new ErrorCode(1007002003, "支付订单用户不正确");
+    // ========== ORDER 模块(拓展单) 1-007-003-000 ==========
+    ErrorCode PAY_ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在");
+    ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付");
+    ErrorCode PAY_ORDER_EXTENSION_STATUS_IS_NOT_SUCCESS = new ErrorCode(1007003002, "支付订单不处于已支付");
+
+}

+ 32 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/notify/PayNotifyStatusEnum.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.coreservice.modules.pay.enums.notify;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 支付通知状态枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum PayNotifyStatusEnum {
+
+    WAITING(1, "等待通知"),
+    SUCCESS(2, "通知成功"),
+    FAILURE(3, "通知失败"), // 多次尝试,彻底失败
+    REQUEST_SUCCESS(4, "请求成功,但是结果失败"),
+    REQUEST_FAILURE(5, "请求失败"),
+
+    ;
+
+    /**
+     * 状态
+     */
+    private final Integer status;
+    /**
+     * 名字
+     */
+    private final String name;
+
+}

+ 28 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/notify/PayNotifyTypeEnum.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.coreservice.modules.pay.enums.notify;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 支付通知类型
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum PayNotifyTypeEnum {
+
+    ORDER(1, "支付单"),
+    REFUND(2, "退款单"),
+    ;
+
+    /**
+     * 类型
+     */
+    private final Integer type;
+    /**
+     * 名字
+     */
+    private final String name;
+
+}

+ 29 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/order/PayOrderNotifyStatusEnum.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.coreservice.modules.pay.enums.order;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 支付订单的通知状态枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum PayOrderNotifyStatusEnum implements IntArrayValuable {
+
+    NO(0, "未通知"),
+    SUCCESS(10, "通知成功"),
+    FAILURE(20, "通知失败")
+    ;
+
+    private final Integer status;
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return new int[0];
+    }
+
+}

+ 29 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/order/PayOrderRefundStatusEnum.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.coreservice.modules.pay.enums.order;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 支付订单的退款状态枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum PayOrderRefundStatusEnum implements IntArrayValuable {
+
+    NO(0, "未退款"),
+    SOME(10, "部分退款"),
+    ALL(20, "全部退款")
+    ;
+
+    private final Integer status;
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return new int[0];
+    }
+
+}

+ 29 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/enums/order/PayOrderStatusEnum.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.coreservice.modules.pay.enums.order;
+
+import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 支付订单的状态枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum PayOrderStatusEnum implements IntArrayValuable {
+
+    WAITING(0, "未支付"),
+    SUCCESS(10, "支付成功"),
+    CLOSED(20, "支付关闭"), // 未付款交易超时关闭,或支付完成后全额退款 TODO 芋艿:需要优化下
+    ;
+
+    private final Integer status;
+    private final String name;
+
+    @Override
+    public int[] array() {
+        return new int[0];
+    }
+
+}

+ 7 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/package-info.java

@@ -0,0 +1,7 @@
+/**
+ * pay 包下,我们放支付业务,提供业务的支付能力。
+ * 例如说:商户、应用、支付、退款等等
+ *
+ * 缩写:pay
+ */
+package cn.iocoder.yudao.coreservice.modules.pay;

+ 23 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/merchant/PayAppCoreService.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.merchant;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+
+/**
+ * 支付应用 Core Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface PayAppCoreService {
+
+    /**
+     * 支付应用的合法性
+     *
+     * 如果不合法,抛出 {@link ServiceException} 业务异常
+     *
+     * @param id 应用编号
+     * @return 应用信息
+     */
+    PayAppDO validPayApp(Long id);
+
+}

+ 39 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/merchant/PayChannelCoreService.java

@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.merchant;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+
+/**
+ * 支付渠道 Core Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface PayChannelCoreService {
+
+    /**
+     * 初始化支付客户端
+     */
+    void initPayClients();
+
+    /**
+     * 支付渠道的合法性
+     *
+     * 如果不合法,抛出 {@link ServiceException} 业务异常
+     *
+     * @param id 渠道编号
+     * @return 渠道信息
+     */
+    PayChannelDO validPayChannel(Long id);
+
+    /**
+     * 支付渠道的合法性
+     *
+     * 如果不合法,抛出 {@link ServiceException} 业务异常
+     *
+     * @param appId 应用编号
+     * @param code 支付渠道
+     * @return 渠道信息
+     */
+    PayChannelDO validPayChannel(Long appId, String code);
+
+}

+ 43 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/merchant/impl/PayAppCoreServiceImpl.java

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.merchant.impl;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.merchant.PayAppCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayAppCoreService;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.*;
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+
+/**
+ * 支付应用 Core Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Valid
+@Slf4j
+public class PayAppCoreServiceImpl implements PayAppCoreService {
+
+    @Resource
+    private PayAppCoreMapper payAppCoreMapper;
+
+    @Override
+    public PayAppDO validPayApp(Long id) {
+        PayAppDO app = payAppCoreMapper.selectById(id);
+        // 校验是否存在
+        if (app == null) {
+            throw exception(PAY_APP_NOT_FOUND);
+        }
+        // 校验是否禁用
+        if (CommonStatusEnum.DISABLE.getStatus().equals(app.getStatus())) {
+            throw exception(PAY_APP_IS_DISABLE);
+        }
+        return app;
+    }
+
+}

+ 121 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/merchant/impl/PayChannelCoreServiceImpl.java

@@ -0,0 +1,121 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.merchant.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.merchant.PayChannelCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants;
+import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayChannelCoreService;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+
+import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.*;
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+
+/**
+ * 支付渠道 Core Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Valid
+@Slf4j
+public class PayChannelCoreServiceImpl implements PayChannelCoreService {
+
+    /**
+     * 定时执行 {@link #schedulePeriodicRefresh()} 的周期
+     * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
+     */
+    private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
+
+    /**
+     * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
+     */
+    private volatile Date maxUpdateTime;
+
+    @Resource
+    private PayChannelCoreMapper payChannelCoreMapper;
+
+    @Resource
+    private PayClientFactory payClientFactory;
+
+    @Override
+    @PostConstruct
+    public void initPayClients() {
+        // 获取支付渠道,如果有更新
+        List<PayChannelDO> payChannels = this.loadPayChannelIfUpdate(maxUpdateTime);
+        if (CollUtil.isEmpty(payChannels)) {
+            return;
+        }
+
+        // 创建或更新支付 Client
+        payChannels.forEach(payChannel -> payClientFactory.createOrUpdatePayClient(payChannel.getId(),
+                payChannel.getCode(), payChannel.getConfig()));
+
+        // 写入缓存
+        assert payChannels.size() > 0; // 断言,避免告警
+        maxUpdateTime = payChannels.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime();
+        log.info("[initPayClients][初始化 PayChannel 数量为 {}]", payChannels.size());
+    }
+
+    @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
+    public void schedulePeriodicRefresh() {
+        initPayClients();
+    }
+
+    /**
+     * 如果支付渠道发生变化,从数据库中获取最新的全量支付渠道。
+     * 如果未发生变化,则返回空
+     *
+     * @param maxUpdateTime 当前支付渠道的最大更新时间
+     * @return 支付渠道列表
+     */
+    private List<PayChannelDO> loadPayChannelIfUpdate(Date maxUpdateTime) {
+        // 第一步,判断是否要更新。
+        if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据
+            log.info("[loadPayChannelIfUpdate][首次加载全量支付渠道]");
+        } else { // 判断数据库中是否有更新的支付渠道
+            if (payChannelCoreMapper.selectExistsByUpdateTimeAfter(maxUpdateTime) == null) {
+                return null;
+            }
+            log.info("[loadPayChannelIfUpdate][增量加载全量支付渠道]");
+        }
+        // 第二步,如果有更新,则从数据库加载所有支付渠道
+        return payChannelCoreMapper.selectList();
+    }
+
+    @Override
+    public PayChannelDO validPayChannel(Long id) {
+        PayChannelDO channel = payChannelCoreMapper.selectById(id);
+        this.validPayChannel(channel);
+        return channel;
+    }
+
+    @Override
+    public PayChannelDO validPayChannel(Long appId, String code) {
+        PayChannelDO channel = payChannelCoreMapper.selectByAppIdAndCode(appId, code);
+        this.validPayChannel(channel);
+        return channel;
+    }
+
+    private void validPayChannel(PayChannelDO channel) {
+        if (channel == null) {
+            throw exception(PAY_CHANNEL_NOT_FOUND);
+        }
+        if (CommonStatusEnum.DISABLE.getStatus().equals(channel.getStatus())) {
+            throw exception(PayErrorCodeCoreConstants.PAY_CHANNEL_IS_DISABLE);
+        }
+    }
+
+}

+ 29 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/PayNotifyCoreService.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.notify;
+
+import cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
+
+import javax.validation.Valid;
+
+/**
+ * 支付通知 Core Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface PayNotifyCoreService {
+
+    /**
+     * 创建支付通知任务
+     *
+     * @param reqDTO 任务信息
+     */
+    void createPayNotifyTask(@Valid PayNotifyTaskCreateReqDTO reqDTO);
+
+    /**
+     * 执行支付通知
+     *
+     * 注意,该方法提供给定时任务调用。目前是 yudao-admin-server 进行调用
+     * @return 通知数量
+     */
+    int executeNotify() throws InterruptedException;
+
+}

+ 32 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/dto/PayNotifyTaskCreateReqDTO.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 支付通知创建 DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PayNotifyTaskCreateReqDTO {
+
+    /**
+     * 类型
+     */
+    @NotNull(message = "类型不能为空")
+    private Integer type;
+    /**
+     * 数据编号
+     */
+    @NotNull(message = "数据编号不能为空")
+    private Long dataId;
+
+}

+ 256 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/impl/PayNotifyCoreServiceImpl.java

@@ -0,0 +1,256 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.notify.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.http.HttpUtil;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyLogDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.notify.PayNotifyTaskDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify.PayNotifyLogCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.notify.PayNotifyTaskCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.redis.notify.PayNotifyLockCoreRedisDAO;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyStatusEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.service.notify.PayNotifyCoreService;
+import cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo.PayNotifyOrderReqVO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo.PayRefundOrderReqVO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayOrderCoreService;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.date.DateUtils;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.SECOND_MILLIS;
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+
+/**
+ * 支付通知 Core Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Valid
+@Slf4j
+public class PayNotifyCoreServiceImpl implements PayNotifyCoreService {
+
+    /**
+     * 通知超时时间,单位:秒
+     */
+    public static final int NOTIFY_TIMEOUT = 120;
+    /**
+     * {@link #NOTIFY_TIMEOUT} 的毫秒
+     */
+    public static final long NOTIFY_TIMEOUT_MILLIS = 120 * SECOND_MILLIS;
+
+    @Resource
+    @Lazy // 循环依赖,避免报错
+    private PayOrderCoreService payOrderCoreService;
+
+    @Resource
+    private PayNotifyTaskCoreMapper payNotifyTaskCoreMapper;
+    @Resource
+    private PayNotifyLogCoreMapper payNotifyLogCoreMapper;
+
+    @Resource
+    private ThreadPoolTaskExecutor threadPoolTaskExecutor; // TODO 芋艿:未来提供独立的线程池
+
+    @Resource
+    private PayNotifyLockCoreRedisDAO payNotifyLockCoreRedisDAO;
+
+    @Resource
+    @Lazy // 循环依赖(自己依赖自己),避免报错
+    private PayNotifyCoreServiceImpl self;
+
+    @Override
+    public void createPayNotifyTask(PayNotifyTaskCreateReqDTO reqDTO) {
+        PayNotifyTaskDO task = new PayNotifyTaskDO();
+        task.setType(reqDTO.getType()).setDataId(reqDTO.getDataId());
+        task.setStatus(PayNotifyStatusEnum.WAITING.getStatus()).setNextNotifyTime(new Date())
+                .setNotifyTimes(0).setMaxNotifyTimes(PayNotifyTaskDO.NOTIFY_FREQUENCY.length + 1);
+        // 补充 merchantId + appId + notifyUrl 字段
+        if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) {
+            PayOrderDO order = payOrderCoreService.getPayOrder(task.getDataId()); // 不进行非空判断,有问题直接异常
+            task.setMerchantId(order.getMerchantId()).setAppId(order.getAppId()).
+                    setMerchantOrderId(order.getMerchantOrderId()).setNotifyUrl(order.getNotifyUrl());
+        } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
+            // TODO 芋艿,需要实现下哈
+            throw new UnsupportedOperationException("需要实现");
+        }
+
+        // 执行插入
+        payNotifyTaskCoreMapper.insert(task);
+
+        // 异步直接发起任务。虽然会有定时任务扫描,但是会导致延迟
+        self.executeNotifyAsync(task);
+    }
+
+    @Override
+    public int executeNotify() throws InterruptedException {
+        // 获得需要通知的任务
+        List<PayNotifyTaskDO> tasks = payNotifyTaskCoreMapper.selectListByNotify();
+        if (CollUtil.isEmpty(tasks)) {
+            return 0;
+        }
+
+        // 遍历,逐个通知
+        CountDownLatch latch = new CountDownLatch(tasks.size());
+        tasks.forEach(task -> threadPoolTaskExecutor.execute(() -> {
+            try {
+                executeNotifySync(task);
+            } finally {
+                latch.countDown();
+            }
+        }));
+        // 等待完成
+        this.awaitExecuteNotify(latch);
+        // 返回执行完成的任务数(成功 + 失败)
+        return tasks.size();
+    }
+
+    /**
+     * 等待全部支付通知的完成
+     * 每 1 秒会打印一次剩余任务数量
+     *
+     * @param latch Latch
+     * @throws InterruptedException 如果被打断
+     */
+    private void awaitExecuteNotify(CountDownLatch latch) throws InterruptedException {
+        long size = latch.getCount();
+        for (int i = 0; i < NOTIFY_TIMEOUT; i++) {
+            if (latch.await(1L, TimeUnit.SECONDS)) {
+                return;
+            }
+            log.info("[awaitExecuteNotify][任务处理中, 总任务数({}) 剩余任务数({})]", size, latch.getCount());
+        }
+        log.error("[awaitExecuteNotify][任务未处理完,总任务数({}) 剩余任务数({})]", size, latch.getCount());
+    }
+
+    /**
+     * 异步执行单个支付通知
+     *
+     * @param task 通知任务
+     */
+    @Async
+    public void executeNotifyAsync(PayNotifyTaskDO task) {
+        self.executeNotifySync(task); // 使用 self,避免事务不发起
+    }
+
+    /**
+     * 同步执行单个支付通知
+     *
+     * @param task 通知任务
+     */
+    public void executeNotifySync(PayNotifyTaskDO task) {
+        // 分布式锁,避免并发问题
+        payNotifyLockCoreRedisDAO.lock(task.getId(), NOTIFY_TIMEOUT_MILLIS, () -> {
+            // 校验,当前任务是否已经被通知过
+            // 虽然已经通过分布式加锁,但是可能同时满足通知的条件,然后都去获得锁。此时,第一个执行完后,第二个还是能拿到锁,然后会再执行一次。
+            PayNotifyTaskDO dbTask = payNotifyTaskCoreMapper.selectById(task.getId());
+            if (DateUtils.afterNow(dbTask.getNextNotifyTime())) {
+                log.info("[executeNotify][dbTask({}) 任务被忽略,原因是未到达下次通知时间,可能是因为并发执行了]", toJsonString(dbTask));
+                return;
+            }
+
+            // 执行通知
+            executeNotify(dbTask);
+        });
+    }
+
+    @Transactional
+    public void executeNotify(PayNotifyTaskDO task) {
+        // 发起回调
+        CommonResult<?> invokeResult = null;
+        Throwable invokeException = null;
+        try {
+            invokeResult = executeNotifyInvoke(task);
+        } catch (Throwable e) {
+            invokeException = e;
+        }
+
+        // 处理
+        Integer newStatus = this.processNotifyResult(task, invokeResult, invokeException);
+
+        // 记录 PayNotifyLog 日志
+        String response = invokeException != null ? getRootCauseMessage(invokeException) : toJsonString(invokeResult);
+        payNotifyLogCoreMapper.insert(PayNotifyLogDO.builder().taskId(task.getId())
+                .notifyTimes(task.getNotifyTimes() + 1).status(newStatus).response(response).build());
+    }
+
+    /**
+     * 执行单个支付任务的 HTTP 调用
+     *
+     * @param task 通知任务
+     * @return HTTP 响应
+     */
+    private CommonResult<?> executeNotifyInvoke(PayNotifyTaskDO task) {
+        // 拼接参数
+        Object request;
+        if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) {
+            request = PayNotifyOrderReqVO.builder().merchantOrderId(task.getMerchantOrderId())
+                            .payOrderId(task.getDataId()).build();
+        } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) {
+            request = PayRefundOrderReqVO.builder().merchantOrderId(task.getMerchantOrderId())
+                    .payRefundId(task.getDataId()).build();
+        } else {
+            throw new RuntimeException("未知的通知任务类型:" + toJsonString(task));
+        }
+        // 请求地址
+        String response = HttpUtil.post(task.getNotifyUrl(), toJsonString(request),
+                (int) NOTIFY_TIMEOUT_MILLIS);
+        // 解析结果
+        return JsonUtils.parseObject(response, CommonResult.class);
+    }
+
+    /**
+     * 处理并更新通知结果
+     *
+     * @param task 通知任务
+     * @param invokeResult 通知结果
+     * @param invokeException 通知异常
+     * @return 最终任务的状态
+     */
+    private Integer processNotifyResult(PayNotifyTaskDO task, CommonResult<?> invokeResult, Throwable invokeException) {
+        // 设置通用的更新 PayNotifyTaskDO 的字段
+        PayNotifyTaskDO updateTask = new PayNotifyTaskDO()
+                .setId(task.getId())
+                .setLastExecuteTime(new Date())
+                .setNotifyTimes(task.getNotifyTimes() + 1);
+
+        // 情况一:调用成功
+        if (invokeResult != null && invokeResult.isSuccess()) {
+            updateTask.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus());
+            return updateTask.getStatus();
+        }
+        // 情况二:调用失败、调用异常
+        // 2.1 超过最大回调次数
+        if (updateTask.getNotifyTimes() >= PayNotifyTaskDO.NOTIFY_FREQUENCY.length) {
+            updateTask.setStatus(PayNotifyStatusEnum.FAILURE.getStatus());
+            return updateTask.getStatus();
+        }
+        // 2.2 未超过最大回调次数
+        updateTask.setNextNotifyTime(DateUtils.addDate(Calendar.SECOND, PayNotifyTaskDO.NOTIFY_FREQUENCY[updateTask.getNotifyTimes()]));
+        updateTask.setStatus(invokeException != null ? PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()
+                : PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus());
+        return updateTask.getStatus();
+    }
+
+    private void processNotifySuccess(PayNotifyTaskDO task, PayNotifyTaskDO updateTask) {
+        payNotifyTaskCoreMapper.updateById(updateTask);
+    }
+
+}

+ 28 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/vo/PayNotifyOrderReqVO.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+@ApiModel(value = "支付单的通知 Request VO", description = "业务方接入支付回调时,使用该 VO 对象")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PayNotifyOrderReqVO {
+
+    @ApiModelProperty(value = "商户订单编号", required = true, example = "10")
+    @NotEmpty(message = "商户订单号不能为空")
+    private String merchantOrderId;
+
+    @ApiModelProperty(value = "支付订单编号", required = true, example = "20")
+    @NotNull(message = "支付订单编号不能为空")
+    private Long payOrderId;
+
+}

+ 28 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/vo/PayRefundOrderReqVO.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+@ApiModel(value = "退款单的通知 Request VO", description = "业务方接入退款回调时,使用该 VO 对象")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PayRefundOrderReqVO {
+
+    @ApiModelProperty(value = "商户订单编号", required = true, example = "10")
+    @NotEmpty(message = "商户订单号不能为空")
+    private String merchantOrderId;
+
+    @ApiModelProperty(value = "支付退款编号", required = true, example = "20")
+    @NotNull(message = "支付退款编号不能为空")
+    private Long payRefundId;
+
+}

+ 6 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/notify/vo/package-info.java

@@ -0,0 +1,6 @@
+/**
+ * 这里的 VO 包有点特殊,是提供给接入支付模块的业务,提供回调接口时,可以直接使用 VO
+ *
+ * 例如说,支付单的回调,使用
+ */
+package cn.iocoder.yudao.coreservice.modules.pay.service.notify.vo;

+ 51 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayOrderCoreService.java

@@ -0,0 +1,51 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order;
+
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderCreateReqDTO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitReqDTO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitRespDTO;
+
+import javax.validation.Valid;
+
+/**
+ * 支付订单 Core Service
+ *
+ * @author 芋道源码
+ */
+public interface PayOrderCoreService {
+
+   /**
+    * 获得支付单
+    *
+    * @param id 支付单编号
+    * @return 支付单
+    */
+   PayOrderDO getPayOrder(Long id);
+
+   /**
+    * 创建支付单
+    *
+    * @param reqDTO 创建请求
+    * @return 支付单编号
+    */
+   Long createPayOrder(@Valid PayOrderCreateReqDTO reqDTO);
+
+   /**
+    * 提交支付
+    * 此时,会发起支付渠道的调用
+    *
+    * @param reqDTO 提交请求
+    * @return 提交结果
+    */
+   PayOrderSubmitRespDTO submitPayOrder(@Valid PayOrderSubmitReqDTO reqDTO);
+
+   /**
+    * 通知支付单成功
+    *
+    * @param channelId 渠道编号
+    * @param channelCode 渠道编码
+    * @param notifyData 通知数据
+    */
+   void notifyPayOrder(Long channelId, String channelCode, String notifyData) throws Exception;
+
+}

+ 64 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/dto/PayOrderCreateReqDTO.java

@@ -0,0 +1,64 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order.dto;
+
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.constraints.DecimalMin;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 支付单创建 Request DTO
+ */
+@Data
+public class PayOrderCreateReqDTO implements Serializable {
+
+    /**
+     * 应用编号
+     */
+    @NotNull(message = "应用编号不能为空")
+    private Long appId;
+    /**
+     * 用户 IP
+     */
+    @NotEmpty(message = "用户 IP 不能为空")
+    private String userIp;
+
+    // ========== 商户相关字段 ==========
+
+    /**
+     * 商户订单编号
+     */
+    @NotEmpty(message = "商户订单编号不能为空")
+    private String merchantOrderId;
+    /**
+     * 商品标题
+     */
+    @NotEmpty(message = "商品标题不能为空")
+    @Length(max = 32, message = "商品标题不能超过 32")
+    private String subject;
+    /**
+     * 商品描述
+     */
+    @NotEmpty(message = "商品描述信息不能为空")
+    @Length(max = 128, message = "商品描述信息长度不能超过128")
+    private String body;
+
+    // ========== 订单相关字段 ==========
+
+    /**
+     * 支付金额,单位:分
+     */
+    @NotNull(message = "支付金额不能为空")
+    @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
+    private Integer amount;
+
+    /**
+     * 支付过期时间
+     */
+    @NotNull(message = "支付过期时间不能为空")
+    private Date expireTime;
+
+}

+ 47 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/dto/PayOrderSubmitReqDTO.java

@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order.dto;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * 支付单提交 Request DTO
+ */
+@Data
+@Accessors(chain = true)
+public class PayOrderSubmitReqDTO implements Serializable {
+
+    /**
+     * 应用编号
+     */
+    @NotNull(message = "应用编号不能为空")
+    private Long appId;
+
+    /**
+     * 支付单编号
+     */
+    @NotNull(message = "支付单编号不能为空")
+    private Long id;
+
+    /**
+     * 支付渠道
+     */
+    @NotEmpty(message = "支付渠道不能为空")
+    private String channelCode;
+
+    /**
+     * 用户 IP
+     */
+    @NotEmpty(message = "用户 IP 不能为空")
+    private String userIp;
+
+    /**
+     * 支付渠道的额外参数
+     */
+    private Map<String, String> channelExtras;
+
+}

+ 23 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/dto/PayOrderSubmitRespDTO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 支付单提交 Response DTO
+ */
+@Data
+public class PayOrderSubmitRespDTO implements Serializable {
+
+    /**
+     * 支付拓展单的编号
+     */
+    private Long extensionId;
+
+    /**
+     * 调用支付渠道的响应结果
+     */
+    private Object invokeResponse;
+
+}

+ 242 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/impl/PayOrderCoreServiceImpl.java

@@ -0,0 +1,242 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order.impl;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.coreservice.modules.pay.convert.order.PayOrderCoreConvert;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayAppDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.order.PayOrderExtensionDO;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.order.PayOrderExtensionCoreMapper;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.notify.PayNotifyTypeEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderNotifyStatusEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.enums.order.PayOrderStatusEnum;
+import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayAppCoreService;
+import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.PayChannelCoreService;
+import cn.iocoder.yudao.coreservice.modules.pay.service.notify.PayNotifyCoreService;
+import cn.iocoder.yudao.coreservice.modules.pay.service.notify.dto.PayNotifyTaskCreateReqDTO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.PayOrderCoreService;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderCreateReqDTO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitReqDTO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitRespDTO;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+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.PayOrderNotifyRespDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.Objects;
+
+import static cn.iocoder.yudao.coreservice.modules.pay.enums.PayErrorCodeCoreConstants.*;
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+
+/**
+ * 支付订单 Core Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+@Slf4j
+public class PayOrderCoreServiceImpl implements PayOrderCoreService {
+
+    @Resource
+    private PayProperties payProperties;
+
+    @Resource
+    private PayAppCoreService payAppCoreService;
+    @Resource
+    private PayChannelCoreService payChannelCoreService;
+    @Resource
+    private PayNotifyCoreService payNotifyCoreService;
+
+    @Resource
+    private PayClientFactory payClientFactory;
+
+    @Resource
+    private PayOrderCoreMapper payOrderCoreMapper;
+    @Resource
+    private PayOrderExtensionCoreMapper payOrderExtensionCoreMapper;
+
+    @Override
+    public PayOrderDO getPayOrder(Long id) {
+        return payOrderCoreMapper.selectById(id);
+    }
+
+    @Override
+    public Long createPayOrder(PayOrderCreateReqDTO reqDTO) {
+        // 校验 App
+        PayAppDO app = payAppCoreService.validPayApp(reqDTO.getAppId());
+
+        // 查询对应的支付交易单是否已经存在。如果是,则直接返回
+        PayOrderDO order = payOrderCoreMapper.selectByAppIdAndMerchantOrderId(
+                reqDTO.getAppId(), reqDTO.getMerchantOrderId());
+        if (order != null) {
+            log.warn("[createPayOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(),
+                    order.getMerchantOrderId(), JsonUtils.toJsonString(order)); // 理论来说,不会出现这个情况
+            return app.getId();
+        }
+
+        // 创建支付交易单
+        order = PayOrderCoreConvert.INSTANCE.convert(reqDTO)
+                .setMerchantId(app.getMerchantId()).setAppId(app.getId());
+        // 商户相关字段
+        order.setNotifyUrl(app.getPayNotifyUrl())
+                .setNotifyStatus(PayOrderNotifyStatusEnum.NO.getStatus());
+        // 订单相关字段
+        order.setStatus(PayOrderStatusEnum.WAITING.getStatus());
+        // 退款相关字段
+        order.setRefundStatus(PayOrderNotifyStatusEnum.NO.getStatus())
+                .setRefundTimes(0).setRefundAmount(0L);
+        payOrderCoreMapper.insert(order);
+        // 最终返回
+        return order.getId();
+    }
+
+    @Override
+    public PayOrderSubmitRespDTO submitPayOrder(PayOrderSubmitReqDTO reqDTO) {
+        // 校验 App
+       payAppCoreService.validPayApp(reqDTO.getAppId());
+        // 校验支付渠道是否有效
+        PayChannelDO channel = payChannelCoreService.validPayChannel(reqDTO.getAppId(), reqDTO.getChannelCode());
+        // 校验支付客户端是否正确初始化
+        PayClient client = payClientFactory.getPayClient(channel.getId());
+        if (client == null) {
+            log.error("[submitPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
+            throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
+        }
+
+        // 获得 PayOrderDO ,并校验其是否存在
+        PayOrderDO order = payOrderCoreMapper.selectById(reqDTO.getId());
+        if (order == null || !Objects.equals(order.getAppId(), reqDTO.getAppId())) { // 是否存在
+            throw exception(PAY_ORDER_NOT_FOUND);
+        }
+        if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
+            throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
+        }
+
+        // 插入 PayOrderExtensionDO
+        PayOrderExtensionDO orderExtension = PayOrderCoreConvert.INSTANCE.convert(reqDTO)
+                .setOrderId(order.getId()).setNo(generateOrderExtensionNo())
+                .setChannelId(channel.getId()).setChannelCode(channel.getCode())
+                .setStatus(PayOrderStatusEnum.WAITING.getStatus());
+        payOrderExtensionCoreMapper.insert(orderExtension);
+
+        // 调用三方接口
+        PayOrderUnifiedReqDTO unifiedOrderReqDTO = PayOrderCoreConvert.INSTANCE.convert2(reqDTO);
+        // 商户相关字段
+        unifiedOrderReqDTO.setMerchantOrderId(orderExtension.getNo()) // 注意,此处使用的是 PayOrderExtensionDO.no 属性!
+                .setSubject(order.getSubject()).setBody(order.getBody())
+                .setNotifyUrl(genChannelPayNotifyUrl(channel));
+        // 订单相关字段
+        unifiedOrderReqDTO.setAmount(order.getAmount()).setExpireTime(order.getExpireTime());
+        CommonResult<?> unifiedOrderResult = client.unifiedOrder(unifiedOrderReqDTO);
+        unifiedOrderResult.checkError();
+
+        // TODO 轮询三方接口,是否已经支付的任务
+        // 返回成功
+        return new PayOrderSubmitRespDTO().setExtensionId(orderExtension.getId())
+                .setInvokeResponse(unifiedOrderResult.getData());
+    }
+
+    /**
+     * 根据支付渠道的编码,生成支付渠道的回调地址
+     *
+     * @param channel 支付渠道
+     * @return 支付渠道的回调地址
+     */
+    private String genChannelPayNotifyUrl(PayChannelDO channel) {
+        // _ 转化为 - 的原因,是因为 URL 我们统一采用中划线的原则
+        return payProperties.getPayNotifyUrl() + "/" + StrUtil.replace(channel.getCode(), "_", "-")
+                + "/" + channel.getId();
+    }
+
+    private String generateOrderExtensionNo() {
+//    wx
+//    2014
+//    10
+//    27
+//    20
+//    09
+//    39
+//    5522657
+//    a690389285100
+        // 目前的算法
+        // 时间序列,年月日时分秒 14 位
+        // 纯随机,6 位 TODO 芋艿:此处估计是会有问题的,后续在调整
+        return DateUtil.format(new Date(), "yyyyMMddHHmmss") + // 时间序列
+                RandomUtil.randomInt(100000, 999999) // 随机。为什么是这个范围,因为偷懒
+                ;
+    }
+
+    @Override
+    @Transactional
+    public void notifyPayOrder(Long channelId, String channelCode, String notifyData) throws Exception {
+        // TODO 芋艿,记录回调日志
+        log.info("[notifyPayOrder][channelId({}) 回调数据({})]", channelId, notifyData);
+
+        // 校验支付渠道是否有效
+        PayChannelDO channel = payChannelCoreService.validPayChannel(channelId);
+        // 校验支付客户端是否正确初始化
+        PayClient client = payClientFactory.getPayClient(channel.getId());
+        if (client == null) {
+            log.error("[notifyPayOrder][渠道编号({}) 找不到对应的支付客户端]", channel.getId());
+            throw exception(PAY_CHANNEL_CLIENT_NOT_FOUND);
+        }
+        // 解析支付结果
+        PayOrderNotifyRespDTO notifyRespDTO = client.parseOrderNotify(notifyData);
+
+        // TODO 芋艿,先最严格的校验。即使调用方重复调用,实际哪个订单已经被重复回调的支付,也返回 false 。也没问题,因为实际已经回调成功了。
+        // 1.1 查询 PayOrderExtensionDO
+        PayOrderExtensionDO orderExtension = payOrderExtensionCoreMapper.selectByNo(
+                notifyRespDTO.getOrderExtensionNo());
+        if (orderExtension == null) {
+            throw exception(PAY_ORDER_EXTENSION_NOT_FOUND);
+        }
+        if (!PayOrderStatusEnum.WAITING.getStatus().equals(orderExtension.getStatus())) { // 校验状态,必须是待支付
+            throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
+        }
+        // 1.2 更新 PayOrderExtensionDO
+        int updateCounts = payOrderExtensionCoreMapper.updateByIdAndStatus(orderExtension.getId(),
+                PayOrderStatusEnum.WAITING.getStatus(), PayOrderExtensionDO.builder().id(orderExtension.getId())
+                        .status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(notifyData).build());
+        if (updateCounts == 0) { // 校验状态,必须是待支付
+            throw exception(PAY_ORDER_EXTENSION_STATUS_IS_NOT_WAITING);
+        }
+        log.info("[notifyPayOrder][支付拓展单({}) 更新为已支付]", orderExtension.getId());
+
+        // 2.1 判断 PayOrderDO 是否处于待支付
+        PayOrderDO order = payOrderCoreMapper.selectById(orderExtension.getOrderId());
+        if (order == null) {
+            throw exception(PAY_ORDER_NOT_FOUND);
+        }
+        if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付
+            throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
+        }
+        // 2.2 更新 PayOrderDO
+        updateCounts = payOrderCoreMapper.updateByIdAndStatus(order.getId(), PayOrderStatusEnum.WAITING.getStatus(),
+                PayOrderDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelId(channelId).channelCode(channelCode)
+                        .successTime(notifyRespDTO.getSuccessTime()).successExtensionId(orderExtension.getId())
+                        .channelOrderNo(notifyRespDTO.getChannelOrderNo()).channelUserId(notifyRespDTO.getChannelUserId())
+                        .notifyTime(new Date()).build());
+        if (updateCounts == 0) { // 校验状态,必须是待支付
+            throw exception(PAY_ORDER_STATUS_IS_NOT_WAITING);
+        }
+        log.info("[notifyPayOrder][支付订单({}) 更新为已支付]", order.getId());
+
+        // 3. 插入支付通知记录
+        payNotifyCoreService.createPayNotifyTask(PayNotifyTaskCreateReqDTO.builder()
+                .type(PayNotifyTypeEnum.ORDER.getType()).dataId(order.getId()).build());
+    }
+
+}

+ 1 - 0
yudao-core-service/src/main/java/cn/iocoder/yudao/coreservice/modules/pay/service/package-info.java

@@ -0,0 +1 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service;

+ 38 - 0
yudao-core-service/src/test-integration/java/cn/iocoder/yudao/coreservice/BaseDbAndRedisIntegrationTest.java

@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.coreservice;
+
+import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration;
+import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;
+import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
+import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration;
+import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
+import org.redisson.spring.starter.RedissonAutoConfiguration;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisIntegrationTest.Application.class)
+@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件
+public class BaseDbAndRedisIntegrationTest {
+
+    @Import({
+            // DB 配置类
+            DynamicDataSourceAutoConfiguration.class, // Dynamic Datasource 配置类
+            YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类
+            DataSourceAutoConfiguration.class, // Spring DB 自动配置类
+            DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
+            // MyBatis 配置类
+            YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
+            MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
+
+            // Redis 配置类
+            RedisAutoConfiguration.class, // Spring Redis 自动配置类
+            YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
+            RedissonAutoConfiguration.class, // Redisson 自动高配置类
+    })
+    public static class Application {
+    }
+
+}

+ 30 - 0
yudao-core-service/src/test-integration/java/cn/iocoder/yudao/coreservice/BaseDbIntegrationTest.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.coreservice;
+
+import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration;
+import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;
+import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration;
+import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbIntegrationTest.Application.class)
+@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件
+public class BaseDbIntegrationTest {
+
+    @Import({
+            // DB 配置类
+            DynamicDataSourceAutoConfiguration.class, // Dynamic Datasource 配置类
+            YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类
+            DataSourceAutoConfiguration.class, // Spring DB 自动配置类
+            DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
+            // MyBatis 配置类
+            YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
+            MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
+    })
+    public static class Application {
+    }
+
+}

+ 23 - 0
yudao-core-service/src/test-integration/java/cn/iocoder/yudao/coreservice/BaseRedisIntegrationTest.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.coreservice;
+
+import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
+import org.redisson.spring.starter.RedissonAutoConfiguration;
+import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.ActiveProfiles;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisIntegrationTest.Application.class)
+@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件
+public class BaseRedisIntegrationTest {
+
+    @Import({
+            // Redis 配置类
+            RedisAutoConfiguration.class, // Spring Redis 自动配置类
+            YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
+            RedissonAutoConfiguration.class, // Redisson 自动高配置类
+    })
+    public static class Application {
+    }
+
+}

+ 29 - 0
yudao-core-service/src/test-integration/java/cn/iocoder/yudao/coreservice/modules/pay/dal/dataobject/merchant/PayChannelDOTest.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant;
+
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPayClientConfig;
+import org.junit.jupiter.api.Test;
+
+public class PayChannelDOTest {
+
+    @Test
+    public void testSerialization() {
+        PayChannelDO payChannelDO = new PayChannelDO();
+        // 创建配置
+        WXPayClientConfig config = new WXPayClientConfig();
+        config.setAppId("wx041349c6f39b268b");
+        config.setMchId("1545083881");
+        config.setApiVersion(WXPayClientConfig.API_VERSION_V2);
+        config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p");
+        payChannelDO.setConfig(config);
+
+        // 序列化
+        String text = JsonUtils.toJsonString(payChannelDO);
+        System.out.println(text);
+
+        // 反序列化
+        payChannelDO = JsonUtils.parseObject(text, PayChannelDO.class);
+        System.out.println(payChannelDO.getConfig().getClass());
+    }
+
+}

+ 56 - 0
yudao-core-service/src/test-integration/java/cn/iocoder/yudao/coreservice/modules/pay/dal/mysql/merchant/PayChannelCoreMapperTest.java

@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.coreservice.modules.pay.dal.mysql.merchant;
+
+import cn.hutool.core.io.IoUtil;
+import cn.iocoder.yudao.coreservice.BaseDbAndRedisIntegrationTest;
+import cn.iocoder.yudao.coreservice.modules.pay.dal.dataobject.merchant.PayChannelDO;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPayClientConfig;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
+import org.junit.jupiter.api.Test;
+
+import javax.annotation.Resource;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.util.List;
+
+@Resource
+public class PayChannelCoreMapperTest extends BaseDbAndRedisIntegrationTest {
+
+    @Resource
+    private PayChannelCoreMapper payChannelCoreMapper;
+
+    /**
+     * 插入初始配置
+     */
+    @Test
+    public void testInsert() throws FileNotFoundException {
+        PayChannelDO payChannelDO = new PayChannelDO();
+        payChannelDO.setCode(PayChannelEnum.WX_PUB.getCode());
+        payChannelDO.setStatus(CommonStatusEnum.ENABLE.getStatus());
+        payChannelDO.setFeeRate(1D);
+        payChannelDO.setMerchantId(1L);
+        payChannelDO.setAppId(6L);
+        // 配置
+        WXPayClientConfig config = new WXPayClientConfig();
+        config.setAppId("wx041349c6f39b268b");
+        config.setMchId("1545083881");
+        config.setApiVersion(WXPayClientConfig.API_VERSION_V2);
+        config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p");
+        config.setPrivateKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_key.pem")));
+        config.setPrivateCertContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem")));
+        config.setApiV3Key("joerVi8y5DJ3o4ttA0o1uH47Xz1u2Ase");
+        payChannelDO.setConfig(config);
+        // 执行插入
+        payChannelCoreMapper.insert(payChannelDO);
+    }
+
+    /**
+     * 查询所有支付配置,看看是否都是 ok 的
+     */
+    @Test
+    public void testSelectList() {
+        List<PayChannelDO> payChannels = payChannelCoreMapper.selectList();
+        System.out.println(payChannels.size());
+    }
+
+}

+ 52 - 0
yudao-core-service/src/test-integration/java/cn/iocoder/yudao/coreservice/modules/pay/service/order/PayOrderCoreServiceTest.java

@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service.order;
+
+import cn.iocoder.yudao.coreservice.BaseDbIntegrationTest;
+import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.impl.PayAppCoreServiceImpl;
+import cn.iocoder.yudao.coreservice.modules.pay.service.merchant.impl.PayChannelCoreServiceImpl;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderCreateReqDTO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.dto.PayOrderSubmitReqDTO;
+import cn.iocoder.yudao.coreservice.modules.pay.service.order.impl.PayOrderCoreServiceImpl;
+import cn.iocoder.yudao.framework.common.util.date.DateUtils;
+import cn.iocoder.yudao.framework.pay.config.YudaoPayAutoConfiguration;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import javax.annotation.Resource;
+import java.time.Duration;
+
+@Import({PayOrderCoreServiceImpl.class, PayAppCoreServiceImpl.class,
+        PayChannelCoreServiceImpl.class, YudaoPayAutoConfiguration.class})
+public class PayOrderCoreServiceTest extends BaseDbIntegrationTest {
+
+    @Resource
+    private PayOrderCoreService payOrderCoreService;
+
+    @Test
+    public void testCreatePayOrder() {
+        // 构造请求
+        PayOrderCreateReqDTO reqDTO = new PayOrderCreateReqDTO();
+        reqDTO.setAppId(6L);
+        reqDTO.setUserIp("127.0.0.1");
+        reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis()));
+        reqDTO.setSubject("标题");
+        reqDTO.setBody("内容");
+        reqDTO.setAmount(100);
+        reqDTO.setExpireTime(DateUtils.addTime(Duration.ofDays(1)));
+        // 发起请求
+        payOrderCoreService.createPayOrder(reqDTO);
+    }
+
+    @Test
+    public void testSubmitPayOrder() {
+        // 构造请求
+        PayOrderSubmitReqDTO reqDTO = new PayOrderSubmitReqDTO();
+        reqDTO.setId(10L);
+        reqDTO.setAppId(6L);
+        reqDTO.setChannelCode(PayChannelEnum.WX_PUB.getCode());
+        reqDTO.setUserIp("127.0.0.1");
+        // 发起请求
+        payOrderCoreService.submitPayOrder(reqDTO);
+    }
+
+}

+ 1 - 0
yudao-core-service/src/test-integration/java/cn/iocoder/yudao/coreservice/modules/pay/service/package-info.java

@@ -0,0 +1 @@
+package cn.iocoder.yudao.coreservice.modules.pay.service;

+ 92 - 0
yudao-core-service/src/test-integration/resources/application-integration-test.yaml

@@ -0,0 +1,92 @@
+spring:
+  main:
+    lazy-initialization: true # 开启懒加载,加快速度
+    banner-mode: off # 单元测试,禁用 Banner
+
+--- #################### 数据库相关配置 ####################
+
+spring:
+  # 数据源配置项
+  autoconfigure:
+    exclude:
+      - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源
+  datasource:
+    druid: # Druid 【监控】相关的全局配置
+      web-stat-filter:
+        enabled: true
+    dynamic: # 多数据源配置
+      druid: # Druid 【连接池】相关的全局配置
+        initial-size: 5 # 初始连接数
+        min-idle: 10 # 最小连接池数量
+        max-active: 20 # 最大连接池数量
+        max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒
+        time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒
+        min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒
+        max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒
+        validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
+        test-while-idle: true
+        test-on-borrow: false
+        test-on-return: false
+      primary: master
+      datasource:
+        master:
+          name: ruoyi-vue-pro
+          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
+          driver-class-name: com.mysql.jdbc.Driver
+          username: root
+          password: 123456
+        slave: # 模拟从库,可根据自己需要修改
+          name: ruoyi-vue-pro
+          url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
+          driver-class-name: com.mysql.jdbc.Driver
+          username: root
+          password: 123456
+
+  # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
+  redis:
+    host: 127.0.0.1 # 地址
+    port: 6379 # 端口
+    database: 0 # 数据库索引
+
+mybatis:
+  lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试
+mybatis-plus:
+  configuration:
+    map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印日志
+  global-config:
+    db-config:
+      id-type: AUTO # 自增 ID
+      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
+      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
+  mapper-locations: classpath*:mapper/*.xml
+  type-aliases-package: ${yudao.core-service.base-package}.modules.*.dal.dataobject
+
+--- #################### 定时任务相关配置 ####################
+
+--- #################### 配置中心相关配置 ####################
+
+--- #################### 服务保障相关配置 ####################
+
+# Lock4j 配置项(单元测试,禁用 Lock4j)
+
+# Resilience4j 配置项
+resilience4j:
+  ratelimiter:
+    instances:
+      backendA:
+        limit-for-period: 1 # 每个周期内,允许的请求数。默认为 50
+        limit-refresh-period: 60s # 每个周期的时长,单位:微秒。默认为 500
+        timeout-duration: 1s # 被限流时,阻塞等待的时长,单位:微秒。默认为 5s
+        register-health-indicator: true # 是否注册到健康监测
+
+--- #################### 监控相关配置 ####################
+
+--- #################### 芋道相关配置 ####################
+
+yudao:
+  info:
+    version: 1.0.0
+    base-package: cn.iocoder.yudao.adminserver
+  core-service:
+    base-package: cn.iocoder.yudao.coreservice

+ 2 - 3
yudao-core-service/src/test/java/cn/iocoder/yudao/coreservice/modules/infra/service/logger/InfApiAccessLogCoreServiceTest.java

@@ -5,14 +5,13 @@ import cn.iocoder.yudao.coreservice.BaseDbUnitTest;
 import cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger.InfApiAccessLogDO;
 import cn.iocoder.yudao.coreservice.modules.infra.dal.mysql.logger.InfApiAccessLogCoreMapper;
 import cn.iocoder.yudao.coreservice.modules.infra.service.logger.impl.InfApiAccessLogCoreServiceImpl;
-import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateDTO;
+import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiAccessLogCreateReqDTO;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
 import org.junit.jupiter.api.Test;
 import org.springframework.context.annotation.Import;
 
 import javax.annotation.Resource;
-import java.util.concurrent.Future;
 
 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -32,7 +31,7 @@ public class InfApiAccessLogCoreServiceTest extends BaseDbUnitTest {
     @Test
     public void testCreateApiAccessLogAsync() {
         // 准备参数
-        ApiAccessLogCreateDTO createDTO = RandomUtils.randomPojo(ApiAccessLogCreateDTO.class,
+        ApiAccessLogCreateReqDTO createDTO = RandomUtils.randomPojo(ApiAccessLogCreateReqDTO.class,
                 dto -> dto.setUserType(RandomUtil.randomEle(UserTypeEnum.values()).getValue()));
 
         // 调用

+ 2 - 2
yudao-core-service/src/test/java/cn/iocoder/yudao/coreservice/modules/infra/service/logger/InfApiErrorLogCoreServiceTest.java

@@ -5,7 +5,7 @@ import cn.iocoder.yudao.coreservice.BaseDbUnitTest;
 import cn.iocoder.yudao.coreservice.modules.infra.dal.dataobject.logger.InfApiErrorLogDO;
 import cn.iocoder.yudao.coreservice.modules.infra.dal.mysql.logger.InfApiErrorLogCoreMapper;
 import cn.iocoder.yudao.coreservice.modules.infra.service.logger.impl.InfApiErrorLogCoreServiceImpl;
-import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateDTO;
+import cn.iocoder.yudao.framework.apilog.core.service.dto.ApiErrorLogCreateReqDTO;
 import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
 import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
 import org.junit.jupiter.api.Test;
@@ -31,7 +31,7 @@ public class InfApiErrorLogCoreServiceTest extends BaseDbUnitTest {
     @Test
     public void testCreateApiErrorLogAsync() {
         // 准备参数
-        ApiErrorLogCreateDTO createDTO = RandomUtils.randomPojo(ApiErrorLogCreateDTO.class,
+        ApiErrorLogCreateReqDTO createDTO = RandomUtils.randomPojo(ApiErrorLogCreateReqDTO.class,
                 dto -> dto.setUserType(RandomUtil.randomEle(UserTypeEnum.values()).getValue()));
 
         // 调用

+ 18 - 0
yudao-dependencies/pom.xml

@@ -52,6 +52,7 @@
         <aliyun-java-sdk-core.version>4.5.25</aliyun-java-sdk-core.version>
         <aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version>
         <yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version>
+        <justauth.version>1.4.0</justauth.version>
     </properties>
 
     <dependencyManagement>
@@ -92,6 +93,16 @@
                 <artifactId>yudao-spring-boot-starter-biz-sms</artifactId>
                 <version>${revision}</version>
             </dependency>
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
+                <version>${revision}</version>
+            </dependency>
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-biz-weixin</artifactId>
+                <version>${revision}</version>
+            </dependency>
 
             <!-- Spring 核心 -->
             <dependency>
@@ -421,6 +432,13 @@
                 <version>${aliyun-java-sdk-dysmsapi.version}</version>
             </dependency>
             <!-- SMS SDK end -->
+
+            <dependency>
+                <groupId>com.xkcoding.justauth</groupId>
+                <artifactId>justauth-spring-boot-starter</artifactId>
+                <version>${justauth.version}</version>
+            </dependency>
+
         </dependencies>
     </dependencyManagement>
 

+ 3 - 0
yudao-framework/pom.xml

@@ -28,6 +28,9 @@
         <module>yudao-spring-boot-starter-biz-operatelog</module>
         <module>yudao-spring-boot-starter-biz-dict</module>
         <module>yudao-spring-boot-starter-biz-sms</module>
+        <module>yudao-spring-boot-starter-biz-pay</module>
+        <module>yudao-spring-boot-starter-biz-weixin</module>
+        <module>yudao-spring-boot-starter-extension</module>
     </modules>
 
     <artifactId>yudao-framework</artifactId>

+ 46 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java

@@ -6,6 +6,8 @@ import java.util.Date;
 
 /**
  * 时间工具类
+ *
+ * @author 芋道源码
  */
 public class DateUtils {
 
@@ -14,6 +16,11 @@ public class DateUtils {
      */
     public static final String TIME_ZONE_DEFAULT = "GMT+8";
 
+    /**
+     * 秒转换成毫秒
+     */
+    public static final long SECOND_MILLIS = 1000;
+
     public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss";
 
     public static Date addTime(Duration duration) {
@@ -74,4 +81,43 @@ public class DateUtils {
         return a.compareTo(b) > 0 ? a : b;
     }
 
+    public static boolean beforeNow(Date date) {
+        return date.getTime() < System.currentTimeMillis();
+    }
+
+    public static boolean afterNow(Date date) {
+        return date.getTime() >= System.currentTimeMillis();
+    }
+
+    /**
+     * 计算当期时间相差的日期
+     *
+     * @param field  日历字段.<br/>eg:Calendar.MONTH,Calendar.DAY_OF_MONTH,<br/>Calendar.HOUR_OF_DAY等.
+     * @param amount 相差的数值
+     * @return 计算后的日志
+     */
+    public static Date addDate(int field, int amount) {
+        return addDate(null, field, amount);
+    }
+
+    /**
+     * 计算当期时间相差的日期
+     *
+     * @param date   设置时间
+     * @param field  日历字段 例如说,{@link Calendar#DAY_OF_MONTH} 等
+     * @param amount 相差的数值
+     * @return 计算后的日志
+     */
+    public static Date addDate(Date date, int field, int amount) {
+        if (amount == 0) {
+            return date;
+        }
+        Calendar c = Calendar.getInstance();
+        if (date != null) {
+            c.setTime(date);
+        }
+        c.add(field, amount);
+        return c.getTime();
+    }
+
 }

+ 34 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.framework.common.util.io;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.IdUtil;
+import lombok.SneakyThrows;
+
+import java.io.File;
+
+/**
+ * 文件工具类
+ *
+ * @author 芋道源码
+ */
+public class FileUtils {
+
+    /**
+     * 创建临时文件
+     * 该文件会在 JVM 退出时,进行删除
+     *
+     * @param data 文件内容
+     * @return 文件
+     */
+    @SneakyThrows
+    public static File createTempFile(String data) {
+        // 创建文件,通过 UUID 保证唯一
+        File file = File.createTempFile(IdUtil.simpleUUID(), null);
+        // 标记 JVM 退出时,自动删除
+        file.deleteOnExit();
+        // 写入内容
+        FileUtil.writeUtf8String(data, file);
+        return file;
+    }
+
+}

+ 9 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/ObjectUtils.java

@@ -29,4 +29,13 @@ public class ObjectUtils {
         return obj1.compareTo(obj2) > 0 ? obj1 : obj2;
     }
 
+    public static <T> T defaultIfNull(T... array) {
+        for (T item : array) {
+            if (item != null) {
+                return item;
+            }
+        }
+        return null;
+    }
+
 }

+ 76 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml

@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>yudao-framework</artifactId>
+        <groupId>cn.iocoder.boot</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>yudao-spring-boot-starter-biz-pay</artifactId>
+    <name>${artifactId}</name>
+    <description>支付拓展,接入国内多个支付渠道
+        1. 支付宝,基于官方 SDK 接入
+        2. 微信支付,基于 weixin-java-pay 接入
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-common</artifactId>
+        </dependency>
+
+        <!-- Spring 核心 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter</artifactId>
+        </dependency>
+
+        <!-- 工具类相关 -->
+        <dependency>
+            <groupId>jakarta.validation</groupId>
+            <artifactId>jakarta.validation-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.hibernate.validator</groupId>
+            <artifactId>hibernate-validator</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+        </dependency>
+
+        <!-- 三方云服务相关 -->
+        <dependency>
+            <groupId>com.alipay.sdk</groupId>
+            <artifactId>alipay-sdk-java</artifactId>
+            <version>4.17.9.ALL</version>
+        </dependency>
+        <dependency>
+            <groupId>com.github.binarywang</groupId>
+            <artifactId>weixin-java-pay</artifactId>
+            <version>4.1.9.B</version>
+        </dependency>
+        <!-- TODO 芋艿:清理 -->
+
+        <!-- Test 测试相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>

+ 32 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/PayProperties.java

@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.framework.pay.config;
+
+import lombok.Data;
+import org.hibernate.validator.constraints.URL;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.validation.annotation.Validated;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.time.Duration;
+
+@ConfigurationProperties(prefix = "yudao.pay")
+@Validated
+@Data
+public class PayProperties {
+
+    /**
+     * 支付回调地址
+     * 注意,支付渠道统一回调到 payNotifyUrl 地址,由支付模块统一处理;然后,自己的支付模块,在回调 PayAppDO.payNotifyUrl 地址
+     */
+    @NotEmpty(message = "支付回调地址不能为空")
+    @URL(message = "支付回调地址的格式必须是 URL")
+    private String payNotifyUrl;
+    /**
+     * 退款回调地址
+     * 注意点,同 {@link #payNotifyUrl} 属性
+     */
+    @NotNull(message = "短信发送频率不能为空")
+    @URL(message = "退款回调地址的格式必须是 URL")
+    private String refundNotifyUrl;
+
+}

+ 22 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/config/YudaoPayAutoConfiguration.java

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.framework.pay.config;
+
+import cn.iocoder.yudao.framework.pay.core.client.PayClientFactory;
+import cn.iocoder.yudao.framework.pay.core.client.impl.PayClientFactoryImpl;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * 支付配置类
+ *
+ * @author 芋道源码
+ */
+@EnableConfigurationProperties(PayProperties.class)
+public class YudaoPayAutoConfiguration {
+
+    @Bean
+    public PayClientFactory payClientFactory() {
+        return new PayClientFactoryImpl();
+    }
+
+}

+ 33 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/AbstractPayCodeMapping.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.framework.pay.core.client;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 将 API 的错误码,转换为通用的错误码
+ *
+ * @see PayCommonResult
+ * @see PayFrameworkErrorCodeConstants
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public abstract class AbstractPayCodeMapping {
+
+    public final ErrorCode apply(String apiCode, String apiMsg) {
+        if (apiCode == null) {
+            log.error("[apply][API 错误码为空,请排查]");
+            return PayFrameworkErrorCodeConstants.EXCEPTION;
+        }
+        ErrorCode errorCode = this.apply0(apiCode, apiMsg);
+        if (errorCode == null) {
+            log.error("[apply][API 错误码({}) 错误提示({}) 无法匹配]", apiCode, apiMsg);
+            return PayFrameworkErrorCodeConstants.PAY_UNKNOWN;
+        }
+        return errorCode;
+    }
+
+    protected abstract ErrorCode apply0(String apiCode, String apiMsg);
+
+}

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

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.framework.pay.core.client;
+
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
+
+/**
+ * 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能
+ *
+ * @author 芋道源码
+ */
+public interface PayClient {
+
+    /**
+     * 获得渠道编号
+     *
+     * @return 渠道编号
+     */
+    Long getId();
+
+    /**
+     * 调用支付渠道,统一下单
+     *
+     * @param reqDTO 下单信息
+     * @return 各支付渠道的返回结果
+     */
+    PayCommonResult<?> unifiedOrder(PayOrderUnifiedReqDTO reqDTO);
+
+    /**
+     * 解析支付单的通知结果
+     *
+     * @param data 通知结果
+     * @return 解析结果
+     * @throws Exception 解析失败,抛出异常
+     */
+    PayOrderNotifyRespDTO parseOrderNotify(String data) throws Exception;
+
+}

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

@@ -0,0 +1,16 @@
+package cn.iocoder.yudao.framework.pay.core.client;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+/**
+ * 支付客户端的配置,本质是支付渠道的配置
+ * 每个不同的渠道,需要不同的配置,通过子类来定义
+ *
+ * @author 芋道源码
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+// @JsonTypeInfo 注解的作用,Jackson 多态
+// 1. 序列化到时数据库时,增加 @class 属性。
+// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
+public interface PayClientConfig {
+}

+ 28 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClientFactory.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.pay.core.client;
+
+/**
+ * 支付客户端的工厂接口
+ *
+ * @author 芋道源码
+ */
+public interface PayClientFactory {
+
+    /**
+     * 获得支付客户端
+     *
+     * @param channelId 渠道编号
+     * @return 支付客户端
+     */
+     PayClient getPayClient(Long channelId);
+
+    /**
+     * 创建支付客户端
+     *
+     * @param channelId 渠道编号
+     * @param channelCode 渠道编码
+     * @param config 支付配置
+     */
+    <Config extends PayClientConfig> void createOrUpdatePayClient(Long channelId, String channelCode,
+                                                                  Config config);
+
+}

+ 57 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayCommonResult.java

@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.framework.pay.core.client;
+
+import cn.hutool.core.exceptions.ExceptionUtil;
+import cn.hutool.core.lang.Assert;
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+/**
+ * 支付的 CommonResult 拓展类
+ *
+ * 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段
+ *
+ * @author 芋道源码
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class PayCommonResult<T> extends CommonResult<T> {
+
+    /**
+     * API 返回错误码
+     *
+     * 由于第三方的错误码可能是字符串,所以使用 String 类型
+     */
+    private String apiCode;
+    /**
+     * API 返回提示
+     */
+    private String apiMsg;
+
+    private PayCommonResult() {
+    }
+
+    public static <T> PayCommonResult<T> build(String apiCode, String apiMsg, T data, AbstractPayCodeMapping codeMapping) {
+        Assert.notNull(codeMapping, "参数 codeMapping 不能为空");
+        PayCommonResult<T> result = new PayCommonResult<T>().setApiCode(apiCode).setApiMsg(apiMsg);
+        result.setData(data);
+        // 翻译错误码
+        if (codeMapping != null) {
+            ErrorCode errorCode = codeMapping.apply(apiCode, apiMsg);
+            result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg());
+        }
+        return result;
+    }
+
+    public static <T> PayCommonResult<T> error(Throwable ex) {
+        PayCommonResult<T> result = new PayCommonResult<>();
+        result.setCode(PayFrameworkErrorCodeConstants.EXCEPTION.getCode());
+        result.setMsg(ExceptionUtil.getRootCauseMessage(ex));
+        return result;
+    }
+
+}

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

@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.framework.pay.core.client.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Date;
+
+/**
+ * 支付通知 Response DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class PayOrderNotifyRespDTO {
+
+    /**
+     * 支付订单号(支付模块的)
+     */
+    private String orderExtensionNo;
+    /**
+     * 支付渠道编号
+     */
+    private String channelOrderNo;
+    /**
+     * 支付渠道用户编号
+     */
+    private String channelUserId;
+    /**
+     * 支付成功时间
+     */
+    private Date successTime;
+
+    /**
+     * 通知的原始数据
+     *
+     * 主要用于持久化,方便后续修复数据,或者排错
+     */
+    private String data;
+
+}

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

@@ -0,0 +1,76 @@
+package cn.iocoder.yudao.framework.pay.core.client.dto;
+
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+import org.hibernate.validator.constraints.URL;
+
+import javax.validation.constraints.DecimalMin;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * 统一下单 Request DTO
+ *
+ * @author 芋道源码
+ */
+@Data
+public class PayOrderUnifiedReqDTO {
+
+    /**
+     * 用户 IP
+     */
+    @NotEmpty(message = "用户 IP 不能为空")
+    private String userIp;
+
+    // ========== 商户相关字段 ==========
+
+    /**
+     * 商户订单编号
+     */
+    @NotEmpty(message = "商户订单编号不能为空")
+    private String merchantOrderId;
+    /**
+     * 商品标题
+     */
+    @NotEmpty(message = "商品标题不能为空")
+    @Length(max = 32, message = "商品标题不能超过 32")
+    private String subject;
+    /**
+     * 商品描述信息
+     */
+    @NotEmpty(message = "商品描述信息不能为空")
+    @Length(max = 128, message = "商品描述信息长度不能超过128")
+    private String body;
+    /**
+     * 支付结果的回调地址
+     */
+    @NotEmpty(message = "支付结果的回调地址不能为空")
+    @URL(message = "支付结果的回调地址必须是 URL 格式")
+    private String notifyUrl;
+
+    // ========== 订单相关字段 ==========
+
+    /**
+     * 支付金额,单位:分
+     */
+    @NotNull(message = "支付金额不能为空")
+    @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零")
+    private Long amount;
+
+    /**
+     * 支付过期时间
+     */
+    @NotNull(message = "支付过期时间不能为空")
+    private Date expireTime;
+
+    // ========== 拓展参数 ==========
+    /**
+     * 支付渠道的额外参数
+     *
+     * 例如说,微信公众号需要传递 openid 参数
+     */
+    private Map<String, String> channelExtras;
+
+}

+ 97 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java

@@ -0,0 +1,97 @@
+package cn.iocoder.yudao.framework.pay.core.client.impl;
+
+import cn.hutool.extra.validation.ValidationUtil;
+import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
+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.PayCommonResult;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
+import lombok.extern.slf4j.Slf4j;
+
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+
+/**
+ * 支付客户端的抽象类,提供模板方法,减少子类的冗余代码
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public abstract class AbstractPayClient<Config extends PayClientConfig> implements PayClient {
+
+    /**
+     * 渠道编号
+     */
+    private final Long channelId;
+    /**
+     * 渠道编码
+     */
+    private final String channelCode;
+    /**
+     * 错误码枚举类
+     */
+    protected AbstractPayCodeMapping codeMapping;
+    /**
+     * 支付配置
+     */
+    protected Config config;
+
+    protected Double calculateAmount(Long amount) {
+        return amount / 100.0;
+    }
+
+    public AbstractPayClient(Long channelId, String channelCode, Config config, AbstractPayCodeMapping codeMapping) {
+        this.channelId = channelId;
+        this.channelCode = channelCode;
+        this.codeMapping = codeMapping;
+        this.config = config;
+    }
+
+    /**
+     * 初始化
+     */
+    public final void init() {
+        doInit();
+        log.info("[init][配置({}) 初始化完成]", config);
+    }
+
+    /**
+     * 自定义初始化
+     */
+    protected abstract void doInit();
+
+    public final void refresh(Config config) {
+        // 判断是否更新
+        if (config.equals(this.config)) {
+            return;
+        }
+        log.info("[refresh][配置({})发生变化,重新初始化]", config);
+        this.config = config;
+        // 初始化
+        this.init();
+    }
+
+    @Override
+    public Long getId() {
+        return channelId;
+    }
+
+    @Override
+    public final PayCommonResult<?> unifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
+        ValidationUtil.validate(reqDTO);
+        // 执行短信发送
+        PayCommonResult<?> result;
+        try {
+            result = doUnifiedOrder(reqDTO);
+        } catch (Throwable ex) {
+            // 打印异常日志
+            log.error("[unifiedOrder][request({}) 发起支付失败]", toJsonString(reqDTO), ex);
+            // 封装返回
+            return PayCommonResult.error(ex);
+        }
+        return result;
+    }
+
+    protected abstract PayCommonResult<?> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
+            throws Throwable;
+
+}

+ 71 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/PayClientFactoryImpl.java

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.framework.pay.core.client.impl;
+
+import cn.hutool.core.lang.Assert;
+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.PayClientFactory;
+import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayPayClientConfig;
+import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayQrPayClient;
+import cn.iocoder.yudao.framework.pay.core.client.impl.alipay.AlipayWapPayClient;
+import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPayClientConfig;
+import cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXPubPayClient;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * 支付客户端的工厂实现类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class PayClientFactoryImpl implements PayClientFactory {
+
+    /**
+     * 支付客户端 Map
+     * key:渠道编号
+     */
+    private final ConcurrentMap<Long, AbstractPayClient<?>> channelIdClients = new ConcurrentHashMap<>();
+
+    @Override
+    public PayClient getPayClient(Long channelId) {
+        AbstractPayClient<?> client = channelIdClients.get(channelId);
+        if (client == null) {
+            log.error("[getPayClient][渠道编号({}) 找不到客户端]", channelId);
+        }
+        return client;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <Config extends PayClientConfig> void createOrUpdatePayClient(Long channelId, String channelCode,
+                                                                         Config config) {
+        AbstractPayClient<Config> client = (AbstractPayClient<Config>) channelIdClients.get(channelId);
+        if (client == null) {
+            client = this.createPayClient(channelId, channelCode, config);
+            client.init();
+            channelIdClients.put(client.getId(), client);
+        } else {
+            client.refresh(config);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private <Config extends PayClientConfig> AbstractPayClient<Config> createPayClient(
+            Long channelId, String channelCode, Config config) {
+        PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode);
+        Assert.notNull(channelEnum, String.format("支付渠道(%s) 为空", channelEnum));
+        // 创建客户端
+        switch (channelEnum) {
+            case WX_PUB: return (AbstractPayClient<Config>) new WXPubPayClient(channelId, (WXPayClientConfig) config);
+            case ALIPAY_WAP: return (AbstractPayClient<Config>) new AlipayWapPayClient(channelId, (AlipayPayClientConfig) config);
+            case ALIPAY_QR: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
+        }
+        // 创建失败,错误日志 + 抛出异常
+        log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", config);
+        throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", config));
+    }
+
+}

+ 89 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java

@@ -0,0 +1,89 @@
+package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
+
+import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
+import lombok.Data;
+
+// TODO 芋艿:参数校验
+/**
+ * 支付宝的 PayClientConfig 实现类
+ * 属性主要来自 {@link com.alipay.api.AlipayConfig} 的必要属性
+ *
+ * @author 芋道源码
+ */
+@Data
+public class AlipayPayClientConfig implements PayClientConfig {
+
+    /**
+     * 网关地址 - 线上
+     */
+    public static final String SERVER_URL_PROD = "https://openapi.alipay.com/gateway.do";
+    /**
+     * 网关地址 - 沙箱
+     */
+    public static final String SERVER_URL_SANDBOX = "https://openapi.alipaydev.com/gateway.do";
+
+    /**
+     * 公钥类型 - 公钥模式
+     */
+    private static final Integer MODE_PUBLIC_KEY = 1;
+    /**
+     * 公钥类型 - 证书模式
+     */
+    private static final Integer MODE_CERTIFICATE = 2;
+
+    /**
+     * 签名算法类型 - RSA
+     */
+    public static final String SIGN_TYPE_DEFAULT = "RSA2";
+
+    /**
+     * 网关地址
+     * 1. {@link #SERVER_URL_PROD}
+     * 2. {@link #SERVER_URL_SANDBOX}
+     */
+    private String serverUrl;
+
+    /**
+     * 开放平台上创建的应用的 ID
+     */
+    private String appId;
+
+    /**
+     * 签名算法类型,推荐:RSA2
+     *
+     * {@link #SIGN_TYPE_DEFAULT}
+     */
+    private String signType;
+
+    /**
+     * 公钥类型
+     * 1. {@link #MODE_PUBLIC_KEY} 情况,privateKey + alipayPublicKey
+     * 2. {@link #MODE_CERTIFICATE} 情况,appCertContent + alipayPublicCertContent + rootCertContent
+     */
+    private Integer mode;
+
+    // ========== 公钥模式 ==========
+    /**
+     * 商户私钥
+     */
+    private String privateKey;
+    /**
+     * 支付宝公钥字符串
+     */
+    private String alipayPublicKey;
+
+    // ========== 证书模式 ==========
+    /**
+     * 指定商户公钥应用证书内容字符串
+     */
+    private String appCertContent;
+    /**
+     * 指定支付宝公钥证书内容字符串
+     */
+    private String alipayPublicCertContent;
+    /**
+     * 指定根证书内容字符串
+     */
+    private String rootCertContent;
+
+}

+ 24 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AlipayPayCodeMapping.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
+import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
+
+import java.util.Objects;
+
+/**
+ * 支付宝的 PayCodeMapping 实现类
+ *
+ * @author 芋道源码
+ */
+public class AlipayPayCodeMapping extends AbstractPayCodeMapping {
+
+    @Override
+    protected ErrorCode apply0(String apiCode, String apiMsg) {
+        if (Objects.equals(apiCode, "10000")) {
+            return GlobalErrorCodeConstants.SUCCESS;
+        }
+        return null;
+    }
+
+}

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

@@ -0,0 +1,74 @@
+package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.AlipayConfig;
+import com.alipay.api.DefaultAlipayClient;
+import com.alipay.api.domain.AlipayTradePrecreateModel;
+import com.alipay.api.request.AlipayTradePrecreateRequest;
+import com.alipay.api.response.AlipayTradePrecreateResponse;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+
+/**
+ * 支付宝【扫码支付】的 PayClient 实现类
+ * 文档:https://opendocs.alipay.com/apis/02890k
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class AlipayQrPayClient extends AbstractPayClient<AlipayPayClientConfig> {
+
+    private DefaultAlipayClient client;
+
+    public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) {
+        super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config, new AlipayPayCodeMapping());
+    }
+
+    @Override
+    @SneakyThrows
+    protected void doInit() {
+        AlipayConfig alipayConfig = new AlipayConfig();
+        BeanUtil.copyProperties(config, alipayConfig, false);
+        // 真实客户端
+        this.client = new DefaultAlipayClient(alipayConfig);
+    }
+
+    @Override
+    public PayCommonResult<AlipayTradePrecreateResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
+        // 构建 AlipayTradePrecreateModel 请求
+        AlipayTradePrecreateModel model = new AlipayTradePrecreateModel();
+        model.setOutTradeNo(reqDTO.getMerchantOrderId());
+        model.setSubject(reqDTO.getSubject());
+        model.setBody(reqDTO.getBody());
+        model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString()); // 单位:元
+        // TODO 芋艿:userIp + expireTime
+        // 构建 AlipayTradePrecreateRequest
+        AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest();
+        request.setBizModel(model);
+
+        // 执行请求
+        AlipayTradePrecreateResponse response;
+        try {
+            response = client.execute(request);
+        } catch (AlipayApiException e) {
+            log.error("[unifiedOrder][request({}) 发起支付失败]", toJsonString(reqDTO), e);
+            return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
+        }
+        // TODO 芋艿:sub Code 需要测试下各种失败的情况
+        return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping);
+    }
+
+    @Override
+    public PayOrderNotifyRespDTO parseOrderNotify(String data) throws Exception {
+        // TODO 芋艿:待完成
+        return null;
+    }
+}

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

@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.framework.pay.core.client.impl.alipay;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.AlipayConfig;
+import com.alipay.api.DefaultAlipayClient;
+import com.alipay.api.domain.AlipayTradeWapPayModel;
+import com.alipay.api.request.AlipayTradeWapPayRequest;
+import com.alipay.api.response.AlipayTradeWapPayResponse;
+import lombok.SneakyThrows;
+
+/**
+ * 支付宝【手机网站】的 PayClient 实现类
+ * 文档:https://opendocs.alipay.com/apis/api_1/alipay.trade.wap.pay
+ *
+ * @author 芋道源码
+ */
+public class AlipayWapPayClient extends AbstractPayClient<AlipayPayClientConfig> {
+
+    private DefaultAlipayClient client;
+
+    public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) {
+        super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config, new AlipayPayCodeMapping());
+    }
+
+    @Override
+    @SneakyThrows
+    protected void doInit() {
+        AlipayConfig alipayConfig = new AlipayConfig();
+        BeanUtil.copyProperties(config, alipayConfig, false);
+        this.client = new DefaultAlipayClient(alipayConfig);
+    }
+
+    @Override
+    public PayCommonResult<AlipayTradeWapPayResponse> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
+        // 构建 AlipayTradeWapPayModel 请求
+        AlipayTradeWapPayModel model = new AlipayTradeWapPayModel();
+        model.setOutTradeNo(reqDTO.getMerchantOrderId());
+        model.setSubject(reqDTO.getSubject());
+        model.setBody(reqDTO.getBody());
+        model.setTotalAmount(calculateAmount(reqDTO.getAmount()).toString());
+        model.setProductCode("QUICK_WAP_PAY"); // TODO 芋艿:这里咋整
+        model.setSellerId("2088102147948060"); // TODO 芋艿:这里咋整
+        // TODO 芋艿:userIp + expireTime
+        // 构建 AlipayTradeWapPayRequest
+        AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
+        request.setBizModel(model);
+
+        // 执行请求
+        AlipayTradeWapPayResponse response;
+        try {
+            response = client.pageExecute(request);
+        } catch (AlipayApiException e) {
+            return PayCommonResult.build(e.getErrCode(), e.getErrMsg(), null, codeMapping);
+        }
+//         TODO 芋艿:sub Code
+        return PayCommonResult.build(response.getCode(), response.getMsg(), response, codeMapping);
+    }
+
+    @Override
+    public PayOrderNotifyRespDTO parseOrderNotify(String data) throws Exception {
+        // TODO 芋艿:待完成
+        return null;
+    }
+}

+ 56 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXCodeMapping.java

@@ -0,0 +1,56 @@
+package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
+import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
+
+import java.util.Objects;
+
+import static cn.iocoder.yudao.framework.pay.core.enums.PayFrameworkErrorCodeConstants.*;
+
+/**
+ * 微信支付 PayCodeMapping 实现类
+ *
+ * @author 芋道源码
+ */
+public class WXCodeMapping extends AbstractPayCodeMapping {
+
+    /**
+     * 错误码 - 成功
+     * 由于 weixin-java-pay 封装的 Result 未返回 code,所以自己定义下
+     */
+    public static final String CODE_SUCCESS = "SUCCESS";
+    /**
+     * 错误提示 - 成功
+     */
+    public static final String MESSAGE_SUCCESS = "成功";
+
+    @Override
+    protected ErrorCode apply0(String apiCode, String apiMsg) {
+        if (Objects.equals(apiCode, CODE_SUCCESS)) {
+            return GlobalErrorCodeConstants.SUCCESS;
+        }
+        if (Objects.equals(apiCode, "FAIL")) {
+            if (Objects.equals(apiMsg, "AppID不存在,请检查后再试")) {
+                return PAY_CONFIG_APP_ID_ERROR;
+            }
+            if (Objects.equals(apiMsg, "签名错误,请检查后再试")
+                || Objects.equals(apiMsg, "签名错误")) {
+                return PAY_CONFIG_SIGN_ERROR;
+            }
+        }
+        if (Objects.equals(apiCode, "PARAM_ERROR")) {
+            if (Objects.equals(apiMsg, "无效的openid")) {
+                return PAY_OPENID_ERROR;
+            }
+        }
+        if (Objects.equals(apiCode, "CustomErrorCode")) {
+            if (StrUtil.contains(apiMsg, "必填字段")) {
+                return PAY_PARAM_MISSING;
+            }
+        }
+        return null;
+    }
+
+}

+ 88 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPayClientConfig.java

@@ -0,0 +1,88 @@
+package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
+
+import cn.hutool.core.io.IoUtil;
+import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
+import lombok.Data;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+
+// TODO 芋艿:参数校验
+/**
+ * 微信支付的 PayClientConfig 实现类
+ * 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性
+ *
+ * @author 芋道源码
+ */
+@Data
+public class WXPayClientConfig implements PayClientConfig {
+
+    // TODO 芋艿:V2 or V3 客户端
+    /**
+     * API 版本 - V2
+     *
+     * https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=4_1
+     */
+    public static final String API_VERSION_V2 = "v2";
+    /**
+     * API 版本 - V3
+     *
+     * https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml
+     */
+    public static final String API_VERSION_V3 = "v3";
+
+    /**
+     * 公众号或者小程序的 appid
+     */
+    private String appId;
+    /**
+     * 商户号
+     */
+    private String mchId;
+    /**
+     * API 版本
+     */
+    private String apiVersion;
+
+    // ========== V2 版本的参数 ==========
+
+    /**
+     * 商户密钥
+     */
+    private String mchKey;
+//    /**
+//     * apiclient_cert.p12 证书文件的绝对路径或者以 classpath: 开头的类路径.
+//     * 对应的字符串
+//     *
+//     * 注意,可通过 {@link #main(String[])} 读取
+//     */
+//    private String keyContent;
+
+    // ========== V3 版本的参数 ==========
+    /**
+     * apiclient_key.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
+     * 对应的字符串
+     *
+     * 注意,可通过 {@link #main(String[])} 读取
+     */
+    private String privateKeyContent;
+    /**
+     * apiclient_cert.pem 证书文件的绝对路径或者以 classpath: 开头的类路径.
+     * 对应的字符串
+     *
+     * 注意,可通过 {@link #main(String[])} 读取
+     */
+    private String privateCertContent;
+    /**
+     * apiV3 秘钥值
+     */
+    private String apiV3Key;
+
+    public static void main(String[] args) throws FileNotFoundException {
+        String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.p12";
+//        String path = "/Users/yunai/Downloads/wx_pay/apiclient_key.pem";
+//        String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.pem";
+        System.out.println(IoUtil.readUtf8(new FileInputStream(path)));
+    }
+
+}

+ 144 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java

@@ -0,0 +1,144 @@
+package cn.iocoder.yudao.framework.pay.core.client.impl.wx;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.io.FileUtils;
+import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
+import cn.iocoder.yudao.framework.pay.core.client.PayCommonResult;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderNotifyRespDTO;
+import cn.iocoder.yudao.framework.pay.core.client.dto.PayOrderUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
+import cn.iocoder.yudao.framework.pay.core.enums.PayChannelEnum;
+import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
+import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
+import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result;
+import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.constant.WxPayConstants;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Objects;
+
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.CODE_SUCCESS;
+import static cn.iocoder.yudao.framework.pay.core.client.impl.wx.WXCodeMapping.MESSAGE_SUCCESS;
+
+/**
+ * 微信支付(公众号)的 PayClient 实现类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class WXPubPayClient extends AbstractPayClient<WXPayClientConfig> {
+
+    private WxPayService client;
+
+    public WXPubPayClient(Long channelId, WXPayClientConfig config) {
+        super(channelId, PayChannelEnum.WX_PUB.getCode(), config, new WXCodeMapping());
+    }
+
+    @Override
+    protected void doInit() {
+        WxPayConfig payConfig = new WxPayConfig();
+        BeanUtil.copyProperties(config, payConfig, "keyContent");
+        payConfig.setTradeType(WxPayConstants.TradeType.JSAPI); // 设置使用 JS API 支付方式
+//        if (StrUtil.isNotEmpty(config.getKeyContent())) {
+//            payConfig.setKeyContent(config.getKeyContent().getBytes(StandardCharsets.UTF_8));
+//        }
+        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());
+        }
+        // 真实客户端
+        this.client = new WxPayServiceImpl();
+        client.setConfig(payConfig);
+    }
+
+    @Override
+    public PayCommonResult<WxPayMpOrderResult> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) {
+        WxPayMpOrderResult response;
+        try {
+            switch (config.getApiVersion()) {
+                case WXPayClientConfig.API_VERSION_V2:
+                    response = this.unifiedOrderV2(reqDTO);
+                    break;
+                case WXPayClientConfig.API_VERSION_V3:
+                    WxPayUnifiedOrderV3Result.JsapiResult responseV3 = this.unifiedOrderV3(reqDTO);
+                    // 将 V3 的结果,统一转换成 V2。返回的字段是一致的
+                    response = new WxPayMpOrderResult();
+                    BeanUtil.copyProperties(responseV3, response, true);
+                    break;
+                default:
+                    throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion()));
+            }
+        } catch (WxPayException e) {
+            log.error("[unifiedOrder][request({}) 发起支付失败,原因({})]", toJsonString(reqDTO), e);
+            return PayCommonResult.build(ObjectUtils.defaultIfNull(e.getErrCode(), e.getReturnCode(), "CustomErrorCode"),
+                    ObjectUtils.defaultIfNull(e.getErrCodeDes(), e.getCustomErrorMsg()),null, codeMapping);
+        }
+        return PayCommonResult.build(CODE_SUCCESS, MESSAGE_SUCCESS, response, codeMapping);
+    }
+
+    private WxPayMpOrderResult unifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+        // 构建 WxPayUnifiedOrderRequest 对象
+        WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
+                .outTradeNo(reqDTO.getMerchantOrderId())
+                // TODO 芋艿:貌似没 title?
+                .body(reqDTO.getBody())
+                .totalFee(reqDTO.getAmount().intValue()) // 单位分
+                .timeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss"))
+                .spbillCreateIp(reqDTO.getUserIp())
+                .openid(getOpenid(reqDTO))
+                .notifyUrl(reqDTO.getNotifyUrl())
+                .build();
+        // 执行请求
+        return client.createOrder(request);
+    }
+
+    private WxPayUnifiedOrderV3Result.JsapiResult unifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException {
+        // 构建 WxPayUnifiedOrderRequest 对象
+        WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request();
+        request.setOutTradeNo(reqDTO.getMerchantOrderId());
+        // TODO 芋艿:貌似没 title?
+        request.setDescription(reqDTO.getBody());
+        request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount().intValue())); // 单位分
+        request.setTimeExpire(DateUtil.format(reqDTO.getExpireTime(), "yyyyMMddHHmmss"));
+        request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO)));
+        request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp()));
+        request.setNotifyUrl(reqDTO.getNotifyUrl());
+        // 执行请求
+        return client.createOrderV3(TradeTypeEnum.JSAPI, request);
+    }
+
+    private static String getOpenid(PayOrderUnifiedReqDTO reqDTO) {
+        String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid");
+        if (StrUtil.isEmpty(openid)) {
+            throw new IllegalArgumentException("支付请求的 openid 不能为空!");
+        }
+        return openid;
+    }
+
+    @Override
+    public PayOrderNotifyRespDTO parseOrderNotify(String data) throws WxPayException {
+        WxPayOrderNotifyResult notifyResult = client.parseOrderNotifyResult(data);
+        Assert.isTrue(Objects.equals(notifyResult.getResultCode(), "SUCCESS"), "支付结果非 SUCCESS");
+        // 转换结果
+        return PayOrderNotifyRespDTO.builder().orderExtensionNo(notifyResult.getOutTradeNo())
+                .channelOrderNo(notifyResult.getTransactionId()).channelUserId(notifyResult.getOpenid())
+                .successTime(DateUtil.parse(notifyResult.getTimeEnd(), "yyyyMMddHHmmss"))
+                .data(data).build();
+    }
+
+}

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

@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.framework.pay.core.enums;
+
+import cn.hutool.core.util.ArrayUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 支付渠道的编码的枚举
+ * 枚举值
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum PayChannelEnum {
+
+    WX_PUB("wx_pub", "微信 JSAPI 支付"), // 公众号的网页
+    WX_LITE("wx_lit","微信小程序支付"),
+    WX_APP("wx_app", "微信 App 支付"),
+
+    ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付"),
+    ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付"),
+    ALIPAY_APP("alipay_app", "支付宝App 支付"),
+    ALIPAY_QR("alipay_qr", "支付宝扫码支付");
+
+    /**
+     * 编码
+     *
+     * 参考 https://www.pingxx.com/api/支付渠道属性值.html
+     */
+    private String code;
+    /**
+     * 名字
+     */
+    private String name;
+
+    public static PayChannelEnum getByCode(String code) {
+        return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values());
+    }
+
+}

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

@@ -0,0 +1,27 @@
+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 PAY_UNKNOWN = new ErrorCode(2002000000, "未知错误,需要解析");
+
+    // ========== 配置相关相关 2002000100 ==========
+    ErrorCode PAY_CONFIG_APP_ID_ERROR = new ErrorCode(2002000100, "支付渠道 AppId 不正确");
+    ErrorCode PAY_CONFIG_SIGN_ERROR = new ErrorCode(2002000100, "签名错误"); // 例如说,微信支付,配置错了 mchId 或者 mchKey
+
+
+    // ========== 其它相关 2002000900 开头 ==========
+    ErrorCode PAY_OPENID_ERROR = new ErrorCode(2002000900, "无效的 openid"); // 例如说,微信 openid 未授权过
+    ErrorCode PAY_PARAM_MISSING = new ErrorCode(2002000901, "请求参数缺失"); // 例如说,支付少传了金额
+
+    ErrorCode EXCEPTION = new ErrorCode(2002000999, "调用异常");
+
+}

+ 2 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+  cn.iocoder.yudao.framework.pay.config.YudaoPayAutoConfiguration

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