Browse Source

Merge remote-tracking branch 'origin/master' into feature/crm

# Conflicts:
#	yudao-module-crm/yudao-module-crm-biz/src/test/java/cn/iocoder/yudao/module/crm/service/contract/ContractServiceImplTest.java
#	yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/DictDataController.java
YunaiV 1 year ago
parent
commit
73e76ab453
100 changed files with 4929 additions and 1858 deletions
  1. 33 31
      README.md
  2. 2 2
      pom.xml
  3. 598 0
      sql/dm/flowable-patch/src/main/java/liquibase/database/core/DmDatabase.java
  4. 165 0
      sql/dm/flowable-patch/src/main/java/liquibase/datatype/core/BooleanType.java
  5. 2068 0
      sql/dm/flowable-patch/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java
  6. 1 0
      sql/dm/flowable-patch/src/main/resources/META-INF/package-info.md
  7. 21 0
      sql/dm/flowable-patch/src/main/resources/META-INF/services/liquibase.database.Database
  8. 0 0
      sql/mysql/optinal/crm.sql
  9. 0 0
      sql/mysql/optinal/crm_data.sql
  10. 0 0
      sql/mysql/optinal/crm_menu.sql
  11. 0 0
      sql/mysql/optinal/mall.sql
  12. 8 0
      sql/mysql/optinal/pay_wallet.sql
  13. 63 44
      sql/mysql/ruoyi-vue-pro.sql
  14. 15 13
      yudao-dependencies/pom.xml
  15. 0 2
      yudao-framework/pom.xml
  16. 6 2
      yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java
  17. 10 0
      yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/loader/ErrorCodeLoader.java
  18. 24 15
      yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java
  19. 1 1
      yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/OperateLogFrameworkServiceImpl.java
  20. 9 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/PayClient.java
  21. 15 2
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java
  22. 19 3
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/AbstractPayClient.java
  23. 77 31
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java
  24. 6 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/mock/MockPayClient.java
  25. 10 3
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java
  26. 4 0
      yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java
  27. 3 3
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClient.java
  28. 0 17
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCodeMapping.java
  29. 0 68
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCommonResult.java
  30. 25 0
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsSendRespDTO.java
  31. 2 76
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java
  32. 31 60
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java
  33. 0 42
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java
  34. 0 22
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java
  35. 13 13
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java
  36. 0 41
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsChannelProperties.java
  37. 62 145
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java
  38. 0 50
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMapping.java
  39. 0 55
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientIntegrationTest.java
  40. 0 46
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClientIntegrationTest.java
  41. 45 83
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java
  42. 0 43
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java
  43. 63 55
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java
  44. 0 50
      yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMappingTest.java
  45. 0 56
      yudao-framework/yudao-spring-boot-starter-biz-social/pom.xml
  46. 0 36
      yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/config/YudaoSocialAutoConfiguration.java
  47. 0 94
      yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/YudaoAuthRequestFactory.java
  48. 0 45
      yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/enums/AuthExtendSource.java
  49. 0 97
      yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/request/AuthWeChatMiniAppRequest.java
  50. 0 178
      yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/request/AuthWeChatMpRequest.java
  51. 0 1
      yudao-framework/yudao-spring-boot-starter-biz-social/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  52. 0 45
      yudao-framework/yudao-spring-boot-starter-biz-weixin/pom.xml
  53. 0 7
      yudao-framework/yudao-spring-boot-starter-biz-weixin/src/main/java/cn/iocoder/yudao/framework/weixin/package-info.java
  54. 0 34
      yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/java/cn/iocoder/yudao/framework/weixin/WxMpServiceTest.java
  55. 0 11
      yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/resources/application.yml
  56. 40 0
      yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java
  57. 3 16
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java
  58. 31 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQProducerAutoConfiguration.java
  59. 2 1
      yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  60. 9 1
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
  61. 7 0
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java
  62. 11 2
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
  63. 6 2
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java
  64. 16 8
      yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
  65. 49 2
      yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
  66. 0 14
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java
  67. 13 8
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java
  68. 152 9
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java
  69. 0 24
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java
  70. 0 9
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java
  71. 0 24
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java
  72. 0 36
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java
  73. 0 31
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java
  74. 0 49
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java
  75. 83 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java
  76. 31 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/listener/WebSocketMessageListener.java
  77. 29 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/message/JsonWebSocketMessage.java
  78. 42 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/LoginUserHandshakeInterceptor.java
  79. 24 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java
  80. 104 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/AbstractWebSocketMessageSender.java
  81. 52 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/WebSocketMessageSender.java
  82. 35 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java
  83. 28 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java
  84. 67 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java
  85. 20 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java
  86. 37 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java
  87. 39 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java
  88. 62 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java
  89. 34 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessage.java
  90. 23 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java
  91. 57 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java
  92. 35 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java
  93. 30 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java
  94. 61 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java
  95. 49 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java
  96. 53 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManager.java
  97. 125 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManagerImpl.java
  98. 67 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/util/WebSocketFrameworkUtils.java
  99. 3 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java
  100. 1 0
      yudao-framework/yudao-spring-boot-starter-websocket/《芋道 Spring Boot WebSocket 入门》.md

+ 33 - 31
README.md

@@ -33,10 +33,11 @@
 * 数据库可使用 MySQL、Oracle、PostgreSQL、SQL Server、MariaDB、国产达梦 DM、TiDB 等
 * 消息队列可使用 Event、Redis、RabbitMQ、Kafka、RocketMQ 等
 * 权限认证使用 Spring Security & Token & Redis,支持多终端、多种用户的认证系统,支持 SSO 单点登录
-* 支持加载动态权限菜单,按钮级别权限控制,本地缓存提升性能
+* 支持加载动态权限菜单,按钮级别权限控制,Redis 缓存提升性能
 * 支持 SaaS 多租户,可自定义每个租户的权限,提供透明化的多租户底层封装
 * 工作流使用 Flowable,支持动态表单、在线设计流程、会签 / 或签、多种任务分配方式
-* 高效率开发,使用代码生成器可以一键生成前后端代码 + 单元测试 + Swagger 接口文档 + Validator 参数校验
+* 高效率开发,使用代码生成器可以一键生成 Java、Vue 前后端代码、SQL 脚本、接口文档,支持单表、树表、主子表
+* 实时通信,采用 Spring WebSocket 实现,内置 Token 身份校验,支持 WebSocket 集群
 * 集成微信小程序、微信公众号、企业微信、钉钉等三方登陆,集成支付宝、微信等支付与退款
 * 集成阿里云、腾讯云等短信渠道,集成 MinIO、阿里云、腾讯云、七牛云等云存储服务
 * 集成报表设计器、大屏设计器,通过拖拽即可生成酷炫的报表与大屏
@@ -57,14 +58,14 @@
 
 ### 前端项目
 
-| 项目                                                                         | Star                                                                                                                                                                                                                                                                                                                     | 简介                             |
-|----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|
-| [yudao-ui-admin-vue3](https://gitee.com/yudaocode/yudao-ui-admin-vue3)     | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-vue3/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-vue3) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-vue3.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-vue3)         | 基于 Vue3 + element-plus 实现的管理后台 |
-| [yudao-ui-admin-vben](https://gitee.com/yudaocode/yudao-ui-admin-vben)     | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-vben/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-vben) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-vben.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-vben)         | 基于 Vue3 + element-plus 实现的管理后台 |
-| [yudao-mall-uniapp](https://gitee.com/yudaocode/yudao-mall-uniapp)         | [![Gitee star](https://gitee.com/yudaocode/yudao-mall-uniapp/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-mall-uniapp) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-mall-uniapp.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-mall-uniapp)                 | 基于 uni-app 实现的商城小程序            |
-| [yudao-ui-admin-vue2](https://gitee.com/yudaocode/yudao-ui-admin-vue2)     | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-vue2/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-vue2) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-vue2.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-vue2)         | 基于 Vue2 + element-ui 实现的管理后台   |
-| [yudao-ui-admin-uniapp](https://gitee.com/yudaocode/yudao-ui-admin-uniapp) | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-uniapp/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-uniapp) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-uniapp.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-uniapp) | 基于 Vue2 + element-ui 实现的管理后台   |
-| [yudao-ui-go-view](https://gitee.com/yudaocode/yudao-ui-go-view)           | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-go-view/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-go-view) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-go-view.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-go-view)                     | 基于 Vue3 + naive-ui 实现的大屏报表     |
+| 项目                                                                         | Star                                                                                                                                                                                                                                                                                                                     | 简介                                     |
+|----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------|
+| [yudao-ui-admin-vue3](https://gitee.com/yudaocode/yudao-ui-admin-vue3)     | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-vue3/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-vue3) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-vue3.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-vue3)         | 基于 Vue3 + element-plus 实现的管理后台         |
+| [yudao-ui-admin-vben](https://gitee.com/yudaocode/yudao-ui-admin-vben)     | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-vben/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-vben) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-vben.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-vben)         | 基于 Vue3 + vben(ant-design-vue) 实现的管理后台 |
+| [yudao-mall-uniapp](https://gitee.com/yudaocode/yudao-mall-uniapp)         | [![Gitee star](https://gitee.com/yudaocode/yudao-mall-uniapp/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-mall-uniapp) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-mall-uniapp.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-mall-uniapp)                 | 基于 uni-app 实现的商城小程序                    |
+| [yudao-ui-admin-vue2](https://gitee.com/yudaocode/yudao-ui-admin-vue2)     | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-vue2/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-vue2) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-vue2.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-vue2)         | 基于 Vue2 + element-ui 实现的管理后台           |
+| [yudao-ui-admin-uniapp](https://gitee.com/yudaocode/yudao-ui-admin-uniapp) | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-admin-uniapp/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-admin-uniapp) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-admin-uniapp.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-admin-uniapp) | 基于 Vue2 + element-ui 实现的管理后台           |
+| [yudao-ui-go-view](https://gitee.com/yudaocode/yudao-ui-go-view)           | [![Gitee star](https://gitee.com/yudaocode/yudao-ui-go-view/badge/star.svg?theme=white)](https://gitee.com/yudaocode/yudao-ui-go-view) [![GitHub stars](https://img.shields.io/github/stars/yudaocode/yudao-ui-go-view.svg?style=social&label=Stars)](https://github.com/yudaocode/yudao-ui-go-view)                     | 基于 Vue3 + naive-ui 实现的大屏报表             |
 
 ## 🐰 分支说明
 
@@ -170,27 +171,28 @@
 
 ### 基础设施
 
-|     | 功能       | 描述                                           |
-|-----|----------|----------------------------------------------|
-| 🚀  | 代码生成     | 前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载       |
-| 🚀  | 系统接口     | 基于 Swagger 自动生成相关的 RESTful API 接口文档          |
-| 🚀  | 数据库文档    | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式      |
-|     | 表单构建     | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件         |
-| 🚀  | 配置管理     | 对系统动态配置常用参数,支持 SpringBoot 加载                 |
-| ⭐️  | 定时任务     | 在线(添加、修改、删除)任务调度包含执行结果日志                     |
-| 🚀  | 文件服务     | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等   | 
-| 🚀  | API 日志   | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题   |
-|     | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈              |
-|     | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理           |
-| 🚀  | 消息队列     | 基于 Redis 实现消息队列,Stream 提供集群消费,Pub/Sub 提供广播消费 |
-| 🚀  | Java 监控  | 基于 Spring Boot Admin 实现 Java 应用的监控           |
-| 🚀  | 链路追踪     | 接入 SkyWalking 组件,实现链路追踪                      |
-| 🚀  | 日志中心     | 接入 SkyWalking 组件,实现日志中心                      |
-| 🚀  | 分布式锁     | 基于 Redis 实现分布式锁,满足并发场景                       |
-| 🚀  | 幂等组件     | 基于 Redis 实现幂等组件,解决重复请求问题                     |
-| 🚀  | 服务保障     | 基于 Resilience4j 实现服务的稳定性,包括限流、熔断等功能          |
-| 🚀  | 日志服务     | 轻量级日志中心,查看远程服务器的日志                           |
-| 🚀  | 单元测试     | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等    |
+|     | 功能        | 描述                                           |
+|-----|-----------|----------------------------------------------|
+| 🚀  | 代码生成      | 前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载       |
+| 🚀  | 系统接口      | 基于 Swagger 自动生成相关的 RESTful API 接口文档          |
+| 🚀  | 数据库文档     | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式      |
+|     | 表单构建      | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件         |
+| 🚀  | 配置管理      | 对系统动态配置常用参数,支持 SpringBoot 加载                 |
+| ⭐️  | 定时任务      | 在线(添加、修改、删除)任务调度包含执行结果日志                     |
+| 🚀  | 文件服务      | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等   | 
+| 🚀  | WebSocket | 提供 WebSocket 接入示例,支持一对一、一对多发送方式              | 
+| 🚀  | API 日志    | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题   |
+|     | MySQL 监控  | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈              |
+|     | Redis 监控  | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理           |
+| 🚀  | 消息队列      | 基于 Redis 实现消息队列,Stream 提供集群消费,Pub/Sub 提供广播消费 |
+| 🚀  | Java 监控   | 基于 Spring Boot Admin 实现 Java 应用的监控           |
+| 🚀  | 链路追踪      | 接入 SkyWalking 组件,实现链路追踪                      |
+| 🚀  | 日志中心      | 接入 SkyWalking 组件,实现日志中心                      |
+| 🚀  | 分布式锁      | 基于 Redis 实现分布式锁,满足并发场景                       |
+| 🚀  | 幂等组件      | 基于 Redis 实现幂等组件,解决重复请求问题                     |
+| 🚀  | 服务保障      | 基于 Resilience4j 实现服务的稳定性,包括限流、熔断等功能          |
+| 🚀  | 日志服务      | 轻量级日志中心,查看远程服务器的日志                           |
+| 🚀  | 单元测试      | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等    |
 
 ### 数据报表
 

+ 2 - 2
pom.xml

@@ -21,7 +21,7 @@
 <!--        <module>yudao-module-mp</module>-->
 <!--        <module>yudao-module-pay</module>-->
 <!--        <module>yudao-module-mall</module>-->
-        <module>yudao-module-crm</module>
+<!--        <module>yudao-module-crm</module>-->
         <!-- 示例项目 -->
 <!--        <module>yudao-example</module>-->
     </modules>
@@ -31,7 +31,7 @@
     <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 
     <properties>
-        <revision>1.8.3-snapshot</revision>
+        <revision>1.9.0-snapshot</revision>
         <!-- Maven 相关 -->
         <java.version>1.8</java.version>
         <maven.compiler.source>${java.version}</maven.compiler.source>

+ 598 - 0
sql/dm/flowable-patch/src/main/java/liquibase/database/core/DmDatabase.java

@@ -0,0 +1,598 @@
+package liquibase.database.core;
+
+import java.lang.reflect.Method;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import liquibase.CatalogAndSchema;
+import liquibase.Scope;
+import liquibase.database.AbstractJdbcDatabase;
+import liquibase.database.DatabaseConnection;
+import liquibase.database.OfflineConnection;
+import liquibase.database.jvm.JdbcConnection;
+import liquibase.exception.DatabaseException;
+import liquibase.exception.UnexpectedLiquibaseException;
+import liquibase.exception.ValidationErrors;
+import liquibase.executor.ExecutorService;
+import liquibase.statement.DatabaseFunction;
+import liquibase.statement.SequenceCurrentValueFunction;
+import liquibase.statement.SequenceNextValueFunction;
+import liquibase.statement.core.RawCallStatement;
+import liquibase.statement.core.RawSqlStatement;
+import liquibase.structure.DatabaseObject;
+import liquibase.structure.core.Catalog;
+import liquibase.structure.core.Index;
+import liquibase.structure.core.PrimaryKey;
+import liquibase.structure.core.Schema;
+import liquibase.util.JdbcUtils;
+import liquibase.util.StringUtil;
+
+public class DmDatabase extends AbstractJdbcDatabase {
+    private static final String PRODUCT_NAME = "DM DBMS";
+
+    @Override
+    protected String getDefaultDatabaseProductName() {
+        return PRODUCT_NAME;
+    }
+
+    /**
+     * Is this AbstractDatabase subclass the correct one to use for the given connection.
+     *
+     * @param conn
+     */
+    @Override
+    public boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException {
+        return PRODUCT_NAME.equalsIgnoreCase(conn.getDatabaseProductName());
+    }
+
+    /**
+     * If this database understands the given url, return the default driver class name.  Otherwise return null.
+     *
+     * @param url
+     */
+    @Override
+    public String getDefaultDriver(String url) {
+        if(url.startsWith("jdbc:dm")) {
+            return "dm.jdbc.driver.DmDriver";
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns an all-lower-case short name of the product.  Used for end-user selecting of database type
+     * such as the DBMS precondition.
+     */
+    @Override
+    public String getShortName() {
+        return "dm";
+    }
+
+    @Override
+    public Integer getDefaultPort() {
+        return 5236;
+    }
+
+    /**
+     * Returns whether this database support initially deferrable columns.
+     */
+    @Override
+    public boolean supportsInitiallyDeferrableColumns() {
+        return true;
+    }
+
+    @Override
+    public boolean supportsTablespaces() {
+        return true;
+    }
+
+    @Override
+    public int getPriority() {
+        return PRIORITY_DEFAULT;
+    }
+
+    private static final Pattern PROXY_USER = Pattern.compile(".*(?:thin|oci)\\:(.+)/@.*");
+
+    protected final int SHORT_IDENTIFIERS_LENGTH = 30;
+    protected final int LONG_IDENTIFIERS_LEGNTH = 128;
+    public static final int ORACLE_12C_MAJOR_VERSION = 12;
+
+    private Set<String> reservedWords = new HashSet<>();
+    private Set<String> userDefinedTypes;
+    private Map<String, String> savedSessionNlsSettings;
+
+    private Boolean canAccessDbaRecycleBin;
+    private Integer databaseMajorVersion;
+    private Integer databaseMinorVersion;
+
+    /**
+     * Default constructor for an object that represents the Oracle Database DBMS.
+     */
+    public DmDatabase() {
+        super.unquotedObjectsAreUppercased = true;
+        //noinspection HardCodedStringLiteral
+        super.setCurrentDateTimeFunction("SYSTIMESTAMP");
+        // Setting list of Oracle's native functions
+        //noinspection HardCodedStringLiteral
+        dateFunctions.add(new DatabaseFunction("SYSDATE"));
+        //noinspection HardCodedStringLiteral
+        dateFunctions.add(new DatabaseFunction("SYSTIMESTAMP"));
+        //noinspection HardCodedStringLiteral
+        dateFunctions.add(new DatabaseFunction("CURRENT_TIMESTAMP"));
+        //noinspection HardCodedStringLiteral
+        super.sequenceNextValueFunction = "%s.nextval";
+        //noinspection HardCodedStringLiteral
+        super.sequenceCurrentValueFunction = "%s.currval";
+    }
+
+    private void tryProxySession(final String url, final Connection con) {
+        Matcher m = PROXY_USER.matcher(url);
+        if (m.matches()) {
+            Properties props = new Properties();
+            props.put("PROXY_USER_NAME", m.group(1));
+            try {
+                Method method = con.getClass().getMethod("openProxySession", int.class, Properties.class);
+                method.setAccessible(true);
+                method.invoke(con, 1, props);
+            } catch (Exception e) {
+                Scope.getCurrentScope().getLog(getClass()).info("Could not open proxy session on OracleDatabase: " + e.getCause().getMessage());
+            }
+        }
+    }
+
+    @Override
+    public int getDatabaseMajorVersion() throws DatabaseException {
+        if (databaseMajorVersion == null) {
+            return super.getDatabaseMajorVersion();
+        } else {
+            return databaseMajorVersion;
+        }
+    }
+
+    @Override
+    public int getDatabaseMinorVersion() throws DatabaseException {
+        if (databaseMinorVersion == null) {
+            return super.getDatabaseMinorVersion();
+        } else {
+            return databaseMinorVersion;
+        }
+    }
+
+    @Override
+    public String getJdbcCatalogName(CatalogAndSchema schema) {
+        return null;
+    }
+
+    @Override
+    public String getJdbcSchemaName(CatalogAndSchema schema) {
+        return correctObjectName((schema.getCatalogName() == null) ? schema.getSchemaName() : schema.getCatalogName(), Schema.class);
+    }
+
+    @Override
+    protected String getAutoIncrementClause(final String generationType, final Boolean defaultOnNull) {
+        if (StringUtil.isEmpty(generationType)) {
+            return super.getAutoIncrementClause();
+        }
+
+        String autoIncrementClause = "GENERATED %s AS IDENTITY"; // %s -- [ ALWAYS | BY DEFAULT [ ON NULL ] ]
+        String generationStrategy = generationType;
+        if (Boolean.TRUE.equals(defaultOnNull) && generationType.toUpperCase().equals("BY DEFAULT")) {
+            generationStrategy += " ON NULL";
+        }
+        return String.format(autoIncrementClause, generationStrategy);
+    }
+
+    @Override
+    public String generatePrimaryKeyName(String tableName) {
+        if (tableName.length() > 27) {
+            //noinspection HardCodedStringLiteral
+            return "PK_" + tableName.toUpperCase(Locale.US).substring(0, 27);
+        } else {
+            //noinspection HardCodedStringLiteral
+            return "PK_" + tableName.toUpperCase(Locale.US);
+        }
+    }
+
+    @Override
+    public boolean isReservedWord(String objectName) {
+        return reservedWords.contains(objectName.toUpperCase());
+    }
+
+    @Override
+    public boolean supportsSequences() {
+        return true;
+    }
+
+    /**
+     * Oracle supports catalogs in liquibase terms
+     *
+     * @return false
+     */
+    @Override
+    public boolean supportsSchemas() {
+        return false;
+    }
+
+    @Override
+    protected String getConnectionCatalogName() throws DatabaseException {
+        if (getConnection() instanceof OfflineConnection) {
+            return getConnection().getCatalog();
+        }
+        try {
+            //noinspection HardCodedStringLiteral
+            return Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor("jdbc", this).queryForObject(new RawCallStatement("select sys_context( 'userenv', 'current_schema' ) from dual"), String.class);
+        } catch (Exception e) {
+            //noinspection HardCodedStringLiteral
+            Scope.getCurrentScope().getLog(getClass()).info("Error getting default schema", e);
+        }
+        return null;
+    }
+
+    @Override
+    public String getDefaultCatalogName() {//NOPMD
+        return (super.getDefaultCatalogName() == null) ? null : super.getDefaultCatalogName().toUpperCase(Locale.US);
+    }
+
+    /**
+     * <p>Returns an Oracle date literal with the same value as a string formatted using ISO 8601.</p>
+     *
+     * <p>Convert an ISO8601 date string to one of the following results:
+     * to_date('1995-05-23', 'YYYY-MM-DD')
+     * to_date('1995-05-23 09:23:59', 'YYYY-MM-DD HH24:MI:SS')</p>
+     * <p>
+     * Implementation restriction:<br>
+     * Currently, only the following subsets of ISO8601 are supported:<br>
+     * <ul>
+     * <li>YYYY-MM-DD</li>
+     * <li>YYYY-MM-DDThh:mm:ss</li>
+     * </ul>
+     */
+    @Override
+    public String getDateLiteral(String isoDate) {
+        String normalLiteral = super.getDateLiteral(isoDate);
+
+        if (isDateOnly(isoDate)) {
+            return "TO_DATE(" + normalLiteral + ", 'YYYY-MM-DD')";
+        } else if (isTimeOnly(isoDate)) {
+            return "TO_DATE(" + normalLiteral + ", 'HH24:MI:SS')";
+        } else if (isTimestamp(isoDate)) {
+            return "TO_TIMESTAMP(" + normalLiteral + ", 'YYYY-MM-DD HH24:MI:SS.FF')";
+        } else if (isDateTime(isoDate)) {
+            int seppos = normalLiteral.lastIndexOf('.');
+            if (seppos != -1) {
+                normalLiteral = normalLiteral.substring(0, seppos) + "'";
+            }
+            return "TO_DATE(" + normalLiteral + ", 'YYYY-MM-DD HH24:MI:SS')";
+        }
+        return "UNSUPPORTED:" + isoDate;
+    }
+
+    @Override
+    public boolean isSystemObject(DatabaseObject example) {
+        if (example == null) {
+            return false;
+        }
+
+        if (this.isLiquibaseObject(example)) {
+            return false;
+        }
+
+        if (example instanceof Schema) {
+            //noinspection HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral
+            if ("SYSTEM".equals(example.getName()) || "SYS".equals(example.getName()) || "CTXSYS".equals(example.getName()) || "XDB".equals(example.getName())) {
+                return true;
+            }
+            //noinspection HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral
+            if ("SYSTEM".equals(example.getSchema().getCatalogName()) || "SYS".equals(example.getSchema().getCatalogName()) || "CTXSYS".equals(example.getSchema().getCatalogName()) || "XDB".equals(example.getSchema().getCatalogName())) {
+                return true;
+            }
+        } else if (isSystemObject(example.getSchema())) {
+            return true;
+        }
+        if (example instanceof Catalog) {
+            //noinspection HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral
+            if (("SYSTEM".equals(example.getName()) || "SYS".equals(example.getName()) || "CTXSYS".equals(example.getName()) || "XDB".equals(example.getName()))) {
+                return true;
+            }
+        } else if (example.getName() != null) {
+            //noinspection HardCodedStringLiteral
+            if (example.getName().startsWith("BIN$")) { //oracle deleted table
+                boolean filteredInOriginalQuery = this.canAccessDbaRecycleBin();
+                if (!filteredInOriginalQuery) {
+                    filteredInOriginalQuery = StringUtil.trimToEmpty(example.getSchema().getName()).equalsIgnoreCase(this.getConnection().getConnectionUserName());
+                }
+
+                if (filteredInOriginalQuery) {
+                    return !((example instanceof PrimaryKey) || (example instanceof Index) || (example instanceof
+                            liquibase.statement.UniqueConstraint));
+                } else {
+                    return true;
+                }
+            } else //noinspection HardCodedStringLiteral
+                if (example.getName().startsWith("AQ$")) { //oracle AQ tables
+                    return true;
+                } else //noinspection HardCodedStringLiteral
+                    if (example.getName().startsWith("DR$")) { //oracle index tables
+                        return true;
+                    } else //noinspection HardCodedStringLiteral
+                        if (example.getName().startsWith("SYS_IOT_OVER")) { //oracle system table
+                            return true;
+                        } else //noinspection HardCodedStringLiteral,HardCodedStringLiteral
+                            if ((example.getName().startsWith("MDRT_") || example.getName().startsWith("MDRS_")) && example.getName().endsWith("$")) {
+                                // CORE-1768 - Oracle creates these for spatial indices and will remove them when the index is removed.
+                                return true;
+                            } else //noinspection HardCodedStringLiteral
+                                if (example.getName().startsWith("MLOG$_")) { //Created by materliaized view logs for every table that is part of a materialized view. Not available for DDL operations.
+                                    return true;
+                                } else //noinspection HardCodedStringLiteral
+                                    if (example.getName().startsWith("RUPD$_")) { //Created by materialized view log tables using primary keys. Not available for DDL operations.
+                                        return true;
+                                    } else //noinspection HardCodedStringLiteral
+                                        if (example.getName().startsWith("WM$_")) { //Workspace Manager backup tables.
+                                            return true;
+                                        } else //noinspection HardCodedStringLiteral
+                                            if ("CREATE$JAVA$LOB$TABLE".equals(example.getName())) { //This table contains the name of the Java object, the date it was loaded, and has a BLOB column to store the Java object.
+                                                return true;
+                                            } else //noinspection HardCodedStringLiteral
+                                                if ("JAVA$CLASS$MD5$TABLE".equals(example.getName())) { //This is a hash table that tracks the loading of Java objects into a schema.
+                                                    return true;
+                                                } else //noinspection HardCodedStringLiteral
+                                                    if (example.getName().startsWith("ISEQ$$_")) { //System-generated sequence
+                                                        return true;
+                                                    } else //noinspection HardCodedStringLiteral
+                                                        if (example.getName().startsWith("USLOG$")) { //for update materialized view
+                                                            return true;
+                                                        } else if (example.getName().startsWith("SYS_FBA")) { //for Flashback tables
+                                                            return true;
+                                                        }
+        }
+
+        return super.isSystemObject(example);
+    }
+
+    @Override
+    public boolean supportsAutoIncrement() {
+        // Oracle supports Identity beginning with version 12c
+        boolean isAutoIncrementSupported = false;
+
+        try {
+            if (getDatabaseMajorVersion() >= 12) {
+                isAutoIncrementSupported = true;
+            }
+
+            // Returning true will generate create table command with 'IDENTITY' clause, example:
+            // CREATE TABLE AutoIncTest (IDPrimaryKey NUMBER(19) GENERATED BY DEFAULT AS IDENTITY NOT NULL, TypeID NUMBER(3) NOT NULL, Description NVARCHAR2(50), CONSTRAINT PK_AutoIncTest PRIMARY KEY (IDPrimaryKey));
+
+            // While returning false will continue to generate create table command without 'IDENTITY' clause, example:
+            // CREATE TABLE AutoIncTest (IDPrimaryKey NUMBER(19) NOT NULL, TypeID NUMBER(3) NOT NULL, Description NVARCHAR2(50), CONSTRAINT PK_AutoIncTest PRIMARY KEY (IDPrimaryKey));
+
+        } catch (DatabaseException ex) {
+            isAutoIncrementSupported = false;
+        }
+
+        return isAutoIncrementSupported;
+    }
+
+
+//    public Set<UniqueConstraint> findUniqueConstraints(String schema) throws DatabaseException {
+//        Set<UniqueConstraint> returnSet = new HashSet<UniqueConstraint>();
+//
+//        List<Map> maps = new Executor(this).queryForList(new RawSqlStatement("SELECT UC.CONSTRAINT_NAME, UCC.TABLE_NAME, UCC.COLUMN_NAME FROM USER_CONSTRAINTS UC, USER_CONS_COLUMNS UCC WHERE UC.CONSTRAINT_NAME=UCC.CONSTRAINT_NAME AND CONSTRAINT_TYPE='U' ORDER BY UC.CONSTRAINT_NAME"));
+//
+//        UniqueConstraint constraint = null;
+//        for (Map map : maps) {
+//            if (constraint == null || !constraint.getName().equals(constraint.getName())) {
+//                returnSet.add(constraint);
+//                Table table = new Table((String) map.get("TABLE_NAME"));
+//                constraint = new UniqueConstraint(map.get("CONSTRAINT_NAME").toString(), table);
+//            }
+//        }
+//        if (constraint != null) {
+//            returnSet.add(constraint);
+//        }
+//
+//        return returnSet;
+//    }
+
+    @Override
+    public boolean supportsRestrictForeignKeys() {
+        return false;
+    }
+
+    @Override
+    public int getDataTypeMaxParameters(String dataTypeName) {
+        //noinspection HardCodedStringLiteral
+        if ("BINARY_FLOAT".equals(dataTypeName.toUpperCase())) {
+            return 0;
+        }
+        //noinspection HardCodedStringLiteral
+        if ("BINARY_DOUBLE".equals(dataTypeName.toUpperCase())) {
+            return 0;
+        }
+        return super.getDataTypeMaxParameters(dataTypeName);
+    }
+
+    public String getSystemTableWhereClause(String tableNameColumn) {
+        List<String> clauses = new ArrayList<String>(Arrays.asList("BIN$",
+                "AQ$",
+                "DR$",
+                "SYS_IOT_OVER",
+                "MLOG$_",
+                "RUPD$_",
+                "WM$_",
+                "ISEQ$$_",
+                "USLOG$",
+                "SYS_FBA"));
+
+        for (int i = 0;i<clauses.size(); i++) {
+            clauses.set(i, tableNameColumn+" NOT LIKE '"+clauses.get(i)+"%'");
+        }
+        return "("+ StringUtil.join(clauses, " AND ") + ")";
+    }
+
+    @Override
+    public boolean jdbcCallsCatalogsSchemas() {
+        return true;
+    }
+
+    public Set<String> getUserDefinedTypes() {
+        if (userDefinedTypes == null) {
+            userDefinedTypes = new HashSet<>();
+            if ((getConnection() != null) && !(getConnection() instanceof OfflineConnection)) {
+                try {
+                    try {
+                        //noinspection HardCodedStringLiteral
+                        userDefinedTypes.addAll(Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor("jdbc", this).queryForList(new RawSqlStatement("SELECT DISTINCT TYPE_NAME FROM ALL_TYPES"), String.class));
+                    } catch (DatabaseException e) { //fall back to USER_TYPES if the user cannot see ALL_TYPES
+                        //noinspection HardCodedStringLiteral
+                        userDefinedTypes.addAll(Scope.getCurrentScope().getSingleton(ExecutorService.class).getExecutor("jdbc", this).queryForList(new RawSqlStatement("SELECT TYPE_NAME FROM USER_TYPES"), String.class));
+                    }
+                } catch (DatabaseException e) {
+                    //ignore error
+                }
+            }
+        }
+
+        return userDefinedTypes;
+    }
+
+    @Override
+    public String generateDatabaseFunctionValue(DatabaseFunction databaseFunction) {
+        //noinspection HardCodedStringLiteral
+        if ((databaseFunction != null) && "current_timestamp".equalsIgnoreCase(databaseFunction.toString())) {
+            return databaseFunction.toString();
+        }
+        if ((databaseFunction instanceof SequenceNextValueFunction) || (databaseFunction instanceof
+                SequenceCurrentValueFunction)) {
+            String quotedSeq = super.generateDatabaseFunctionValue(databaseFunction);
+            // replace "myschema.my_seq".nextval with "myschema"."my_seq".nextval
+            return quotedSeq.replaceFirst("\"([^\\.\"]+)\\.([^\\.\"]+)\"", "\"$1\".\"$2\"");
+
+        }
+
+        return super.generateDatabaseFunctionValue(databaseFunction);
+    }
+
+    @Override
+    public ValidationErrors validate() {
+        ValidationErrors errors = super.validate();
+        DatabaseConnection connection = getConnection();
+        if ((connection == null) || (connection instanceof OfflineConnection)) {
+            //noinspection HardCodedStringLiteral
+            Scope.getCurrentScope().getLog(getClass()).info("Cannot validate offline database");
+            return errors;
+        }
+
+        if (!canAccessDbaRecycleBin()) {
+            errors.addWarning(getDbaRecycleBinWarning());
+        }
+
+        return errors;
+
+    }
+
+    public String getDbaRecycleBinWarning() {
+        //noinspection HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,
+        // HardCodedStringLiteral
+        //noinspection HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral
+        return "Liquibase needs to access the DBA_RECYCLEBIN table so we can automatically handle the case where " +
+                "constraints are deleted and restored. Since Oracle doesn't properly restore the original table names " +
+                "referenced in the constraint, we use the information from the DBA_RECYCLEBIN to automatically correct this" +
+                " issue.\n" +
+                "\n" +
+                "The user you used to connect to the database (" + getConnection().getConnectionUserName() +
+                ") needs to have \"SELECT ON SYS.DBA_RECYCLEBIN\" permissions set before we can perform this operation. " +
+                "Please run the following SQL to set the appropriate permissions, and try running the command again.\n" +
+                "\n" +
+                "     GRANT SELECT ON SYS.DBA_RECYCLEBIN TO " + getConnection().getConnectionUserName() + ";";
+    }
+
+    public boolean canAccessDbaRecycleBin() {
+        if (canAccessDbaRecycleBin == null) {
+            DatabaseConnection connection = getConnection();
+            if ((connection == null) || (connection instanceof OfflineConnection)) {
+                return false;
+            }
+
+            Statement statement = null;
+            try {
+                statement = ((JdbcConnection) connection).createStatement();
+                @SuppressWarnings("HardCodedStringLiteral") ResultSet resultSet = statement.executeQuery("select 1 from dba_recyclebin where 0=1");
+                resultSet.close(); //don't need to do anything with the result set, just make sure statement ran.
+                this.canAccessDbaRecycleBin = true;
+            } catch (Exception e) {
+                //noinspection HardCodedStringLiteral
+                if ((e instanceof SQLException) && e.getMessage().startsWith("ORA-00942")) { //ORA-00942: table or view does not exist
+                    this.canAccessDbaRecycleBin = false;
+                } else {
+                    //noinspection HardCodedStringLiteral
+                    Scope.getCurrentScope().getLog(getClass()).warning("Cannot check dba_recyclebin access", e);
+                    this.canAccessDbaRecycleBin = false;
+                }
+            } finally {
+                JdbcUtils.close(null, statement);
+            }
+        }
+
+        return canAccessDbaRecycleBin;
+    }
+
+    @Override
+    public boolean supportsNotNullConstraintNames() {
+        return true;
+    }
+
+    /**
+     * Tests if the given String would be a valid identifier in Oracle DBMS. In Oracle, a valid identifier has
+     * the following form (case-insensitive comparison):
+     * 1st character: A-Z
+     * 2..n characters: A-Z0-9$_#
+     * The maximum length of an identifier differs by Oracle version and object type.
+     */
+    public boolean isValidOracleIdentifier(String identifier, Class<? extends DatabaseObject> type) {
+        if ((identifier == null) || (identifier.length() < 1))
+            return false;
+
+        if (!identifier.matches("^(i?)[A-Z][A-Z0-9\\$\\_\\#]*$"))
+            return false;
+
+        /*
+         * @todo It seems we currently do not have a class for tablespace identifiers, and all other classes
+         * we do know seem to be supported as 12cR2 long identifiers, so:
+         */
+        return (identifier.length() <= LONG_IDENTIFIERS_LEGNTH);
+    }
+
+    /**
+     * Returns the maximum number of bytes (NOT: characters) for an identifier. For Oracle <=12c Release 20, this
+     * is 30 bytes, and starting from 12cR2, up to 128 (except for tablespaces, PDB names and some other rather rare
+     * object types).
+     *
+     * @return the maximum length of an object identifier, in bytes
+     */
+    public int getIdentifierMaximumLength() {
+        try {
+            if (getDatabaseMajorVersion() < ORACLE_12C_MAJOR_VERSION) {
+                return SHORT_IDENTIFIERS_LENGTH;
+            } else if ((getDatabaseMajorVersion() == ORACLE_12C_MAJOR_VERSION) && (getDatabaseMinorVersion() <= 1)) {
+                return SHORT_IDENTIFIERS_LENGTH;
+            } else {
+                return LONG_IDENTIFIERS_LEGNTH;
+            }
+        } catch (DatabaseException ex) {
+            throw new UnexpectedLiquibaseException("Cannot determine the Oracle database version number", ex);
+        }
+
+    }
+}

+ 165 - 0
sql/dm/flowable-patch/src/main/java/liquibase/datatype/core/BooleanType.java

@@ -0,0 +1,165 @@
+package liquibase.datatype.core;
+
+import liquibase.change.core.LoadDataChange;
+import liquibase.database.Database;
+import liquibase.database.core.*;
+import liquibase.datatype.DataTypeInfo;
+import liquibase.datatype.DatabaseDataType;
+import liquibase.datatype.LiquibaseDataType;
+import liquibase.exception.UnexpectedLiquibaseException;
+import liquibase.statement.DatabaseFunction;
+import liquibase.util.StringUtil;
+
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+@DataTypeInfo(name = "boolean", aliases = {"java.sql.Types.BOOLEAN", "java.lang.Boolean", "bit", "bool"}, minParameters = 0, maxParameters = 0, priority = LiquibaseDataType.PRIORITY_DEFAULT)
+public class BooleanType extends LiquibaseDataType {
+
+    @Override
+    public DatabaseDataType toDatabaseDataType(Database database) {
+        String originalDefinition = StringUtil.trimToEmpty(getRawDefinition());
+        if ((database instanceof Firebird3Database)) {
+            return new DatabaseDataType("BOOLEAN");
+        }
+
+        if ((database instanceof Db2zDatabase) || (database instanceof FirebirdDatabase)) {
+            return new DatabaseDataType("SMALLINT");
+        } else if (database instanceof MSSQLDatabase) {
+            return new DatabaseDataType(database.escapeDataTypeName("bit"));
+        } else if (database instanceof MySQLDatabase) {
+            if (originalDefinition.toLowerCase(Locale.US).startsWith("bit")) {
+                return new DatabaseDataType("BIT", getParameters());
+            }
+            return new DatabaseDataType("BIT", 1);
+        } else if (database instanceof OracleDatabase) {
+            return new DatabaseDataType("NUMBER", 1);
+        } else if ((database instanceof SybaseASADatabase) || (database instanceof SybaseDatabase)) {
+            return new DatabaseDataType("BIT");
+        } else if (database instanceof DerbyDatabase) {
+            if (((DerbyDatabase) database).supportsBooleanDataType()) {
+                return new DatabaseDataType("BOOLEAN");
+            } else {
+                return new DatabaseDataType("SMALLINT");
+            }
+        } else if (database instanceof DB2Database) {
+            if (((DB2Database) database).supportsBooleanDataType())
+                return new DatabaseDataType("BOOLEAN");
+            else
+                return new DatabaseDataType("SMALLINT");
+        } else if (database instanceof HsqlDatabase) {
+            return new DatabaseDataType("BOOLEAN");
+        } else if (database instanceof PostgresDatabase) {
+            if (originalDefinition.toLowerCase(Locale.US).startsWith("bit")) {
+                return new DatabaseDataType("BIT", getParameters());
+            }
+        } else if (database instanceof DmDatabase) { // dhb52: DM Support
+            return new DatabaseDataType("bit");
+        }
+
+        return super.toDatabaseDataType(database);
+    }
+
+    @Override
+    public String objectToSql(Object value, Database database) {
+        if ((value == null) || "null".equals(value.toString().toLowerCase(Locale.US))) {
+            return null;
+        }
+
+        String returnValue;
+        if (value instanceof String) {
+            value = ((String) value).replaceAll("'", "");
+            if ("true".equals(((String) value).toLowerCase(Locale.US)) || "1".equals(value) || "b'1'".equals(((String) value).toLowerCase(Locale.US)) || "t".equals(((String) value).toLowerCase(Locale.US)) || ((String) value).toLowerCase(Locale.US).equals(this.getTrueBooleanValue(database).toLowerCase(Locale.US))) {
+                returnValue = this.getTrueBooleanValue(database);
+            } else if ("false".equals(((String) value).toLowerCase(Locale.US)) || "0".equals(value) || "b'0'".equals(
+                ((String) value).toLowerCase(Locale.US)) || "f".equals(((String) value).toLowerCase(Locale.US)) || ((String) value).toLowerCase(Locale.US).equals(this.getFalseBooleanValue(database).toLowerCase(Locale.US))) {
+                returnValue = this.getFalseBooleanValue(database);
+            } else if (database instanceof PostgresDatabase && Pattern.matches("b?([01])\\1*(::bit|::\"bit\")?", (String) value)) {
+                returnValue = "b'"
+                    + value.toString()
+                    .replace("b", "")
+                    .replace("\"", "")
+                    .replace("::it", "")
+                    + "'::\"bit\"";
+            } else {
+                throw new UnexpectedLiquibaseException("Unknown boolean value: " + value);
+            }
+        } else if (value instanceof Long) {
+            if (Long.valueOf(1).equals(value)) {
+                returnValue = this.getTrueBooleanValue(database);
+            } else {
+                returnValue = this.getFalseBooleanValue(database);
+            }
+        } else if (value instanceof Number) {
+            if (value.equals(1) || "1".equals(value.toString()) || "1.0".equals(value.toString())) {
+                returnValue = this.getTrueBooleanValue(database);
+            } else {
+                returnValue = this.getFalseBooleanValue(database);
+            }
+        } else if (value instanceof DatabaseFunction) {
+            return value.toString();
+        } else if (value instanceof Boolean) {
+            if (((Boolean) value)) {
+                returnValue = this.getTrueBooleanValue(database);
+            } else {
+                returnValue = this.getFalseBooleanValue(database);
+            }
+        } else {
+            throw new UnexpectedLiquibaseException("Cannot convert type " + value.getClass() + " to a boolean value");
+        }
+
+        return returnValue;
+    }
+
+    protected boolean isNumericBoolean(Database database) {
+        if (database instanceof Firebird3Database) {
+            return false;
+        }
+        if (database instanceof DerbyDatabase) {
+            return !((DerbyDatabase) database).supportsBooleanDataType();
+        } else if (database instanceof DB2Database) {
+            return !((DB2Database) database).supportsBooleanDataType();
+        }
+        return (database instanceof Db2zDatabase)
+            || (database instanceof FirebirdDatabase)
+            || (database instanceof MSSQLDatabase)
+            || (database instanceof MySQLDatabase)
+            || (database instanceof OracleDatabase)
+            || (database instanceof SQLiteDatabase)
+            || (database instanceof SybaseASADatabase)
+            || (database instanceof SybaseDatabase)
+            || (database instanceof DmDatabase); // dhb52: DM Support
+    }
+
+    /**
+     * The database-specific value to use for "false" "boolean" columns.
+     */
+    public String getFalseBooleanValue(Database database) {
+        if (isNumericBoolean(database)) {
+            return "0";
+        }
+        if (database instanceof InformixDatabase) {
+            return "'f'";
+        }
+        return "FALSE";
+    }
+
+    /**
+     * The database-specific value to use for "true" "boolean" columns.
+     */
+    public String getTrueBooleanValue(Database database) {
+        if (isNumericBoolean(database)) {
+            return "1";
+        }
+        if (database instanceof InformixDatabase) {
+            return "'t'";
+        }
+        return "TRUE";
+    }
+
+    @Override
+    public LoadDataChange.LOAD_DATA_TYPE getLoadTypeName() {
+        return LoadDataChange.LOAD_DATA_TYPE.BOOLEAN;
+    }
+
+}

+ 2068 - 0
sql/dm/flowable-patch/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java

@@ -0,0 +1,2068 @@
+/* Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.flowable.common.engine.impl;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.ServiceLoader;
+import java.util.Set;
+
+import javax.naming.InitialContext;
+import javax.sql.DataSource;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.ibatis.builder.xml.XMLConfigBuilder;
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.apache.ibatis.datasource.pooled.PooledDataSource;
+import org.apache.ibatis.mapping.Environment;
+import org.apache.ibatis.plugin.Interceptor;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.apache.ibatis.session.defaults.DefaultSqlSessionFactory;
+import org.apache.ibatis.transaction.TransactionFactory;
+import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
+import org.apache.ibatis.transaction.managed.ManagedTransactionFactory;
+import org.apache.ibatis.type.ArrayTypeHandler;
+import org.apache.ibatis.type.BigDecimalTypeHandler;
+import org.apache.ibatis.type.BlobInputStreamTypeHandler;
+import org.apache.ibatis.type.BlobTypeHandler;
+import org.apache.ibatis.type.BooleanTypeHandler;
+import org.apache.ibatis.type.ByteTypeHandler;
+import org.apache.ibatis.type.ClobTypeHandler;
+import org.apache.ibatis.type.DateOnlyTypeHandler;
+import org.apache.ibatis.type.DateTypeHandler;
+import org.apache.ibatis.type.DoubleTypeHandler;
+import org.apache.ibatis.type.FloatTypeHandler;
+import org.apache.ibatis.type.IntegerTypeHandler;
+import org.apache.ibatis.type.JdbcType;
+import org.apache.ibatis.type.LongTypeHandler;
+import org.apache.ibatis.type.NClobTypeHandler;
+import org.apache.ibatis.type.NStringTypeHandler;
+import org.apache.ibatis.type.ShortTypeHandler;
+import org.apache.ibatis.type.SqlxmlTypeHandler;
+import org.apache.ibatis.type.StringTypeHandler;
+import org.apache.ibatis.type.TimeOnlyTypeHandler;
+import org.apache.ibatis.type.TypeHandlerRegistry;
+import org.flowable.common.engine.api.FlowableException;
+import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType;
+import org.flowable.common.engine.api.delegate.event.FlowableEventDispatcher;
+import org.flowable.common.engine.api.delegate.event.FlowableEventListener;
+import org.flowable.common.engine.api.engine.EngineLifecycleListener;
+import org.flowable.common.engine.impl.agenda.AgendaOperationRunner;
+import org.flowable.common.engine.impl.cfg.CommandExecutorImpl;
+import org.flowable.common.engine.impl.cfg.IdGenerator;
+import org.flowable.common.engine.impl.cfg.TransactionContextFactory;
+import org.flowable.common.engine.impl.cfg.standalone.StandaloneMybatisTransactionContextFactory;
+import org.flowable.common.engine.impl.db.CommonDbSchemaManager;
+import org.flowable.common.engine.impl.db.DbSqlSessionFactory;
+import org.flowable.common.engine.impl.db.LogSqlExecutionTimePlugin;
+import org.flowable.common.engine.impl.db.MybatisTypeAliasConfigurator;
+import org.flowable.common.engine.impl.db.MybatisTypeHandlerConfigurator;
+import org.flowable.common.engine.impl.db.SchemaManager;
+import org.flowable.common.engine.impl.event.EventDispatchAction;
+import org.flowable.common.engine.impl.event.FlowableEventDispatcherImpl;
+import org.flowable.common.engine.impl.interceptor.Command;
+import org.flowable.common.engine.impl.interceptor.CommandConfig;
+import org.flowable.common.engine.impl.interceptor.CommandContextFactory;
+import org.flowable.common.engine.impl.interceptor.CommandContextInterceptor;
+import org.flowable.common.engine.impl.interceptor.CommandExecutor;
+import org.flowable.common.engine.impl.interceptor.CommandInterceptor;
+import org.flowable.common.engine.impl.interceptor.CrDbRetryInterceptor;
+import org.flowable.common.engine.impl.interceptor.DefaultCommandInvoker;
+import org.flowable.common.engine.impl.interceptor.LogInterceptor;
+import org.flowable.common.engine.impl.interceptor.SessionFactory;
+import org.flowable.common.engine.impl.interceptor.TransactionContextInterceptor;
+import org.flowable.common.engine.impl.lock.LockManager;
+import org.flowable.common.engine.impl.lock.LockManagerImpl;
+import org.flowable.common.engine.impl.logging.LoggingListener;
+import org.flowable.common.engine.impl.logging.LoggingSession;
+import org.flowable.common.engine.impl.logging.LoggingSessionFactory;
+import org.flowable.common.engine.impl.persistence.GenericManagerFactory;
+import org.flowable.common.engine.impl.persistence.StrongUuidGenerator;
+import org.flowable.common.engine.impl.persistence.cache.EntityCache;
+import org.flowable.common.engine.impl.persistence.cache.EntityCacheImpl;
+import org.flowable.common.engine.impl.persistence.entity.ByteArrayEntityManager;
+import org.flowable.common.engine.impl.persistence.entity.ByteArrayEntityManagerImpl;
+import org.flowable.common.engine.impl.persistence.entity.Entity;
+import org.flowable.common.engine.impl.persistence.entity.PropertyEntityManager;
+import org.flowable.common.engine.impl.persistence.entity.PropertyEntityManagerImpl;
+import org.flowable.common.engine.impl.persistence.entity.TableDataManager;
+import org.flowable.common.engine.impl.persistence.entity.TableDataManagerImpl;
+import org.flowable.common.engine.impl.persistence.entity.data.ByteArrayDataManager;
+import org.flowable.common.engine.impl.persistence.entity.data.PropertyDataManager;
+import org.flowable.common.engine.impl.persistence.entity.data.impl.MybatisByteArrayDataManager;
+import org.flowable.common.engine.impl.persistence.entity.data.impl.MybatisPropertyDataManager;
+import org.flowable.common.engine.impl.runtime.Clock;
+import org.flowable.common.engine.impl.service.CommonEngineServiceImpl;
+import org.flowable.common.engine.impl.util.DefaultClockImpl;
+import org.flowable.common.engine.impl.util.IoUtil;
+import org.flowable.common.engine.impl.util.ReflectUtil;
+import org.flowable.eventregistry.api.EventRegistryEventConsumer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+public abstract class AbstractEngineConfiguration {
+
+    protected final Logger logger = LoggerFactory.getLogger(getClass());
+
+    /** The tenant id indicating 'no tenant' */
+    public static final String NO_TENANT_ID = "";
+
+    /**
+     * Checks the version of the DB schema against the library when the form engine is being created and throws an exception if the versions don't match.
+     */
+    public static final String DB_SCHEMA_UPDATE_FALSE = "false";
+    public static final String DB_SCHEMA_UPDATE_CREATE = "create";
+    public static final String DB_SCHEMA_UPDATE_CREATE_DROP = "create-drop";
+
+    /**
+     * Creates the schema when the form engine is being created and drops the schema when the form engine is being closed.
+     */
+    public static final String DB_SCHEMA_UPDATE_DROP_CREATE = "drop-create";
+
+    /**
+     * Upon building of the process engine, a check is performed and an update of the schema is performed if it is necessary.
+     */
+    public static final String DB_SCHEMA_UPDATE_TRUE = "true";
+
+    protected boolean forceCloseMybatisConnectionPool = true;
+
+    protected String databaseType;
+    protected String jdbcDriver = "org.h2.Driver";
+    protected String jdbcUrl = "jdbc:h2:tcp://localhost/~/flowable";
+    protected String jdbcUsername = "sa";
+    protected String jdbcPassword = "";
+    protected String dataSourceJndiName;
+    protected int jdbcMaxActiveConnections = 16;
+    protected int jdbcMaxIdleConnections = 8;
+    protected int jdbcMaxCheckoutTime;
+    protected int jdbcMaxWaitTime;
+    protected boolean jdbcPingEnabled;
+    protected String jdbcPingQuery;
+    protected int jdbcPingConnectionNotUsedFor;
+    protected int jdbcDefaultTransactionIsolationLevel;
+    protected DataSource dataSource;
+    protected SchemaManager commonSchemaManager;
+    protected SchemaManager schemaManager;
+    protected Command<Void> schemaManagementCmd;
+
+    protected String databaseSchemaUpdate = DB_SCHEMA_UPDATE_FALSE;
+
+    /**
+     * Whether to use a lock when performing the database schema create or update operations.
+     */
+    protected boolean useLockForDatabaseSchemaUpdate = false;
+
+    protected String xmlEncoding = "UTF-8";
+
+    // COMMAND EXECUTORS ///////////////////////////////////////////////
+
+    protected CommandExecutor commandExecutor;
+    protected Collection<? extends CommandInterceptor> defaultCommandInterceptors;
+    protected CommandConfig defaultCommandConfig;
+    protected CommandConfig schemaCommandConfig;
+    protected CommandContextFactory commandContextFactory;
+    protected CommandInterceptor commandInvoker;
+
+    protected AgendaOperationRunner agendaOperationRunner = (commandContext, runnable) -> runnable.run();
+
+    protected List<CommandInterceptor> customPreCommandInterceptors;
+    protected List<CommandInterceptor> customPostCommandInterceptors;
+    protected List<CommandInterceptor> commandInterceptors;
+
+    protected Map<String, AbstractEngineConfiguration> engineConfigurations = new HashMap<>();
+    protected Map<String, AbstractServiceConfiguration> serviceConfigurations = new HashMap<>();
+
+    protected ClassLoader classLoader;
+    /**
+     * Either use Class.forName or ClassLoader.loadClass for class loading. See http://forums.activiti.org/content/reflectutilloadclass-and-custom- classloader
+     */
+    protected boolean useClassForNameClassLoading = true;
+
+    protected List<EngineLifecycleListener> engineLifecycleListeners;
+
+    // Event Registry //////////////////////////////////////////////////
+    protected Map<String, EventRegistryEventConsumer> eventRegistryEventConsumers = new HashMap<>();
+
+    // MYBATIS SQL SESSION FACTORY /////////////////////////////////////
+
+    protected boolean isDbHistoryUsed = true;
+    protected DbSqlSessionFactory dbSqlSessionFactory;
+    protected SqlSessionFactory sqlSessionFactory;
+    protected TransactionFactory transactionFactory;
+    protected TransactionContextFactory transactionContextFactory;
+
+    /**
+     * If set to true, enables bulk insert (grouping sql inserts together). Default true.
+     * For some databases (eg DB2+z/OS) needs to be set to false.
+     */
+    protected boolean isBulkInsertEnabled = true;
+
+    /**
+     * Some databases have a limit of how many parameters one sql insert can have (eg SQL Server, 2000 params (!= insert statements) ). Tweak this parameter in case of exceptions indicating too much
+     * is being put into one bulk insert, or make it higher if your database can cope with it and there are inserts with a huge amount of data.
+     * <p>
+     * By default: 100 (55 for mssql server as it has a hard limit of 2000 parameters in a statement)
+     */
+    protected int maxNrOfStatementsInBulkInsert = 100;
+
+    public int DEFAULT_MAX_NR_OF_STATEMENTS_BULK_INSERT_SQL_SERVER = 55; // currently Execution has most params (35). 2000 / 35 = 57.
+
+    protected String mybatisMappingFile;
+    protected Set<Class<?>> customMybatisMappers;
+    protected Set<String> customMybatisXMLMappers;
+    protected List<Interceptor> customMybatisInterceptors;
+
+    protected Set<String> dependentEngineMyBatisXmlMappers;
+    protected List<MybatisTypeAliasConfigurator> dependentEngineMybatisTypeAliasConfigs;
+    protected List<MybatisTypeHandlerConfigurator> dependentEngineMybatisTypeHandlerConfigs;
+
+    // SESSION FACTORIES ///////////////////////////////////////////////
+    protected List<SessionFactory> customSessionFactories;
+    protected Map<Class<?>, SessionFactory> sessionFactories;
+
+    protected boolean enableEventDispatcher = true;
+    protected FlowableEventDispatcher eventDispatcher;
+    protected List<FlowableEventListener> eventListeners;
+    protected Map<String, List<FlowableEventListener>> typedEventListeners;
+    protected List<EventDispatchAction> additionalEventDispatchActions;
+
+    protected LoggingListener loggingListener;
+
+    protected boolean transactionsExternallyManaged;
+
+    /**
+     * Flag that can be set to configure or not a relational database is used. This is useful for custom implementations that do not use relational databases at all.
+     *
+     * If true (default), the {@link AbstractEngineConfiguration#getDatabaseSchemaUpdate()} value will be used to determine what needs to happen wrt the database schema.
+     *
+     * If false, no validation or schema creation will be done. That means that the database schema must have been created 'manually' before but the engine does not validate whether the schema is
+     * correct. The {@link AbstractEngineConfiguration#getDatabaseSchemaUpdate()} value will not be used.
+     */
+    protected boolean usingRelationalDatabase = true;
+
+    /**
+     * Flag that can be set to configure whether or not a schema is used. This is useful for custom implementations that do not use relational databases at all.
+     * Setting {@link #usingRelationalDatabase} to true will automatically imply using a schema.
+     */
+    protected boolean usingSchemaMgmt = true;
+
+    /**
+     * Allows configuring a database table prefix which is used for all runtime operations of the process engine. For example, if you specify a prefix named 'PRE1.', Flowable will query for executions
+     * in a table named 'PRE1.ACT_RU_EXECUTION_'.
+     *
+     * <p>
+     * <strong>NOTE: the prefix is not respected by automatic database schema management. If you use {@link AbstractEngineConfiguration#DB_SCHEMA_UPDATE_CREATE_DROP} or
+     * {@link AbstractEngineConfiguration#DB_SCHEMA_UPDATE_TRUE}, Flowable will create the database tables using the default names, regardless of the prefix configured here.</strong>
+     */
+    protected String databaseTablePrefix = "";
+
+    /**
+     * Escape character for doing wildcard searches.
+     *
+     * This will be added at then end of queries that include for example a LIKE clause. For example: SELECT * FROM table WHERE column LIKE '%\%%' ESCAPE '\';
+     */
+    protected String databaseWildcardEscapeCharacter;
+
+    /**
+     * database catalog to use
+     */
+    protected String databaseCatalog = "";
+
+    /**
+     * In some situations you want to set the schema to use for table checks / generation if the database metadata doesn't return that correctly, see https://jira.codehaus.org/browse/ACT-1220,
+     * https://jira.codehaus.org/browse/ACT-1062
+     */
+    protected String databaseSchema;
+
+    /**
+     * Set to true in case the defined databaseTablePrefix is a schema-name, instead of an actual table name prefix. This is relevant for checking if Flowable-tables exist, the databaseTablePrefix
+     * will not be used here - since the schema is taken into account already, adding a prefix for the table-check will result in wrong table-names.
+     */
+    protected boolean tablePrefixIsSchema;
+
+    /**
+     * Set to true if the latest version of a definition should be retrieved, ignoring a possible parent deployment id value
+     */
+    protected boolean alwaysLookupLatestDefinitionVersion;
+
+    /**
+     * Set to true if by default lookups should fallback to the default tenant (an empty string by default or a defined tenant value)
+     */
+    protected boolean fallbackToDefaultTenant;
+
+    /**
+     * Default tenant provider that is executed when looking up definitions, in case the global or local fallback to default tenant value is true
+     */
+    protected DefaultTenantProvider defaultTenantProvider = (tenantId, scope, scopeKey) -> NO_TENANT_ID;
+
+    /**
+     * Enables the MyBatis plugin that logs the execution time of sql statements.
+     */
+    protected boolean enableLogSqlExecutionTime;
+
+    protected Properties databaseTypeMappings = getDefaultDatabaseTypeMappings();
+
+    /**
+     * Duration between the checks when acquiring a lock.
+     */
+    protected Duration lockPollRate = Duration.ofSeconds(10);
+
+    /**
+     * Duration to wait for the DB Schema lock before giving up.
+     */
+    protected Duration schemaLockWaitTime = Duration.ofMinutes(5);
+
+    // DATA MANAGERS //////////////////////////////////////////////////////////////////
+
+    protected PropertyDataManager propertyDataManager;
+    protected ByteArrayDataManager byteArrayDataManager;
+    protected TableDataManager tableDataManager;
+
+    // ENTITY MANAGERS ////////////////////////////////////////////////////////////////
+
+    protected PropertyEntityManager propertyEntityManager;
+    protected ByteArrayEntityManager byteArrayEntityManager;
+
+    protected List<EngineDeployer> customPreDeployers;
+    protected List<EngineDeployer> customPostDeployers;
+    protected List<EngineDeployer> deployers;
+
+    // CONFIGURATORS ////////////////////////////////////////////////////////////
+
+    protected boolean enableConfiguratorServiceLoader = true; // Enabled by default. In certain environments this should be set to false (eg osgi)
+    protected List<EngineConfigurator> configurators; // The injected configurators
+    protected List<EngineConfigurator> allConfigurators; // Including auto-discovered configurators
+    protected EngineConfigurator idmEngineConfigurator;
+    protected EngineConfigurator eventRegistryConfigurator;
+
+    public static final String PRODUCT_NAME_POSTGRES = "PostgreSQL";
+    public static final String PRODUCT_NAME_CRDB = "CockroachDB";
+
+    public static final String DATABASE_TYPE_H2 = "h2";
+    public static final String DATABASE_TYPE_HSQL = "hsql";
+    public static final String DATABASE_TYPE_MYSQL = "mysql";
+    public static final String DATABASE_TYPE_ORACLE = "oracle";
+    public static final String DATABASE_TYPE_POSTGRES = "postgres";
+    public static final String DATABASE_TYPE_MSSQL = "mssql";
+    public static final String DATABASE_TYPE_DB2 = "db2";
+    public static final String DATABASE_TYPE_COCKROACHDB = "cockroachdb";
+
+    public static Properties getDefaultDatabaseTypeMappings() {
+        Properties databaseTypeMappings = new Properties();
+        databaseTypeMappings.setProperty("H2", DATABASE_TYPE_H2);
+        databaseTypeMappings.setProperty("HSQL Database Engine", DATABASE_TYPE_HSQL);
+        databaseTypeMappings.setProperty("MySQL", DATABASE_TYPE_MYSQL);
+        databaseTypeMappings.setProperty("MariaDB", DATABASE_TYPE_MYSQL);
+        databaseTypeMappings.setProperty("Oracle", DATABASE_TYPE_ORACLE);
+        databaseTypeMappings.setProperty(PRODUCT_NAME_POSTGRES, DATABASE_TYPE_POSTGRES);
+        databaseTypeMappings.setProperty("Microsoft SQL Server", DATABASE_TYPE_MSSQL);
+        databaseTypeMappings.setProperty(DATABASE_TYPE_DB2, DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/NT", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/NT64", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2 UDP", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/LINUX", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/LINUX390", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/LINUXX8664", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/LINUXZ64", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/LINUXPPC64", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/LINUXPPC64LE", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/400 SQL", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/6000", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2 UDB iSeries", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/AIX64", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/HPUX", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/HP64", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/SUN", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/SUN64", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/PTX", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2/2", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty("DB2 UDB AS400", DATABASE_TYPE_DB2);
+        databaseTypeMappings.setProperty(PRODUCT_NAME_CRDB, DATABASE_TYPE_COCKROACHDB);
+        databaseTypeMappings.setProperty("DM DBMS", DATABASE_TYPE_ORACLE); // dhb52: DM support
+        return databaseTypeMappings;
+    }
+
+    protected Map<Object, Object> beans;
+
+    protected IdGenerator idGenerator;
+    protected boolean usePrefixId;
+
+    protected Clock clock;
+    protected ObjectMapper objectMapper;
+
+    // Variables
+
+    public static final int DEFAULT_GENERIC_MAX_LENGTH_STRING = 4000;
+    public static final int DEFAULT_ORACLE_MAX_LENGTH_STRING = 2000;
+
+    /**
+     * Define a max length for storing String variable types in the database. Mainly used for the Oracle NVARCHAR2 limit of 2000 characters
+     */
+    protected int maxLengthStringVariableType = -1;
+
+    protected void initEngineConfigurations() {
+        addEngineConfiguration(getEngineCfgKey(), getEngineScopeType(), this);
+    }
+
+    // DataSource
+    // ///////////////////////////////////////////////////////////////
+
+    protected void initDataSource() {
+        if (dataSource == null) {
+            if (dataSourceJndiName != null) {
+                try {
+                    dataSource = (DataSource) new InitialContext().lookup(dataSourceJndiName);
+                } catch (Exception e) {
+                    throw new FlowableException("couldn't lookup datasource from " + dataSourceJndiName + ": " + e.getMessage(), e);
+                }
+
+            } else if (jdbcUrl != null) {
+                if ((jdbcDriver == null) || (jdbcUsername == null)) {
+                    throw new FlowableException("DataSource or JDBC properties have to be specified in a process engine configuration");
+                }
+
+                logger.debug("initializing datasource to db: {}", jdbcUrl);
+
+                if (logger.isInfoEnabled()) {
+                    logger.info("Configuring Datasource with following properties (omitted password for security)");
+                    logger.info("datasource driver : {}", jdbcDriver);
+                    logger.info("datasource url : {}", jdbcUrl);
+                    logger.info("datasource user name : {}", jdbcUsername);
+                }
+
+                PooledDataSource pooledDataSource = new PooledDataSource(this.getClass().getClassLoader(), jdbcDriver, jdbcUrl, jdbcUsername, jdbcPassword);
+
+                if (jdbcMaxActiveConnections > 0) {
+                    pooledDataSource.setPoolMaximumActiveConnections(jdbcMaxActiveConnections);
+                }
+                if (jdbcMaxIdleConnections > 0) {
+                    pooledDataSource.setPoolMaximumIdleConnections(jdbcMaxIdleConnections);
+                }
+                if (jdbcMaxCheckoutTime > 0) {
+                    pooledDataSource.setPoolMaximumCheckoutTime(jdbcMaxCheckoutTime);
+                }
+                if (jdbcMaxWaitTime > 0) {
+                    pooledDataSource.setPoolTimeToWait(jdbcMaxWaitTime);
+                }
+                if (jdbcPingEnabled) {
+                    pooledDataSource.setPoolPingEnabled(true);
+                    if (jdbcPingQuery != null) {
+                        pooledDataSource.setPoolPingQuery(jdbcPingQuery);
+                    }
+                    pooledDataSource.setPoolPingConnectionsNotUsedFor(jdbcPingConnectionNotUsedFor);
+                }
+                if (jdbcDefaultTransactionIsolationLevel > 0) {
+                    pooledDataSource.setDefaultTransactionIsolationLevel(jdbcDefaultTransactionIsolationLevel);
+                }
+                dataSource = pooledDataSource;
+            }
+        }
+
+        if (databaseType == null) {
+            initDatabaseType();
+        }
+    }
+
+    public void initDatabaseType() {
+        Connection connection = null;
+        try {
+            connection = dataSource.getConnection();
+            DatabaseMetaData databaseMetaData = connection.getMetaData();
+            String databaseProductName = databaseMetaData.getDatabaseProductName();
+            logger.debug("database product name: '{}'", databaseProductName);
+
+            // CRDB does not expose the version through the jdbc driver, so we need to fetch it through version().
+            if (PRODUCT_NAME_POSTGRES.equalsIgnoreCase(databaseProductName)) {
+                try (PreparedStatement preparedStatement = connection.prepareStatement("select version() as version;");
+                     ResultSet resultSet = preparedStatement.executeQuery()) {
+                    String version = null;
+                    if (resultSet.next()) {
+                        version = resultSet.getString("version");
+                    }
+
+                    if (StringUtils.isNotEmpty(version) && version.toLowerCase().startsWith(PRODUCT_NAME_CRDB.toLowerCase())) {
+                        databaseProductName = PRODUCT_NAME_CRDB;
+                        logger.info("CockroachDB version '{}' detected", version);
+                    }
+                }
+            }
+
+            databaseType = databaseTypeMappings.getProperty(databaseProductName);
+            if (databaseType == null) {
+                throw new FlowableException("couldn't deduct database type from database product name '" + databaseProductName + "'");
+            }
+            logger.debug("using database type: {}", databaseType);
+
+        } catch (SQLException e) {
+            throw new RuntimeException("Exception while initializing Database connection", e);
+        } finally {
+            try {
+                if (connection != null) {
+                    connection.close();
+                }
+            } catch (SQLException e) {
+                logger.error("Exception while closing the Database connection", e);
+            }
+        }
+
+        // Special care for MSSQL, as it has a hard limit of 2000 params per statement (incl bulk statement).
+        // Especially with executions, with 100 as default, this limit is passed.
+        if (DATABASE_TYPE_MSSQL.equals(databaseType)) {
+            maxNrOfStatementsInBulkInsert = DEFAULT_MAX_NR_OF_STATEMENTS_BULK_INSERT_SQL_SERVER;
+        }
+    }
+
+    public void initSchemaManager() {
+        if (this.commonSchemaManager == null) {
+            this.commonSchemaManager = new CommonDbSchemaManager();
+        }
+    }
+
+    // session factories ////////////////////////////////////////////////////////
+
+    public void addSessionFactory(SessionFactory sessionFactory) {
+        sessionFactories.put(sessionFactory.getSessionType(), sessionFactory);
+    }
+
+    public void initCommandContextFactory() {
+        if (commandContextFactory == null) {
+            commandContextFactory = new CommandContextFactory();
+        }
+    }
+
+    public void initTransactionContextFactory() {
+        if (transactionContextFactory == null) {
+            transactionContextFactory = new StandaloneMybatisTransactionContextFactory();
+        }
+    }
+
+    public void initCommandExecutors() {
+        initDefaultCommandConfig();
+        initSchemaCommandConfig();
+        initCommandInvoker();
+        initCommandInterceptors();
+        initCommandExecutor();
+    }
+
+
+    public void initDefaultCommandConfig() {
+        if (defaultCommandConfig == null) {
+            defaultCommandConfig = new CommandConfig();
+        }
+    }
+
+    public void initSchemaCommandConfig() {
+        if (schemaCommandConfig == null) {
+            schemaCommandConfig = new CommandConfig();
+        }
+    }
+
+    public void initCommandInvoker() {
+        if (commandInvoker == null) {
+            commandInvoker = new DefaultCommandInvoker();
+        }
+    }
+
+    public void initCommandInterceptors() {
+        if (commandInterceptors == null) {
+            commandInterceptors = new ArrayList<>();
+            if (customPreCommandInterceptors != null) {
+                commandInterceptors.addAll(customPreCommandInterceptors);
+            }
+            commandInterceptors.addAll(getDefaultCommandInterceptors());
+            if (customPostCommandInterceptors != null) {
+                commandInterceptors.addAll(customPostCommandInterceptors);
+            }
+            commandInterceptors.add(commandInvoker);
+        }
+    }
+
+    public Collection<? extends CommandInterceptor> getDefaultCommandInterceptors() {
+        if (defaultCommandInterceptors == null) {
+            List<CommandInterceptor> interceptors = new ArrayList<>();
+            interceptors.add(new LogInterceptor());
+
+            if (DATABASE_TYPE_COCKROACHDB.equals(databaseType)) {
+                interceptors.add(new CrDbRetryInterceptor());
+            }
+
+            CommandInterceptor transactionInterceptor = createTransactionInterceptor();
+            if (transactionInterceptor != null) {
+                interceptors.add(transactionInterceptor);
+            }
+
+            if (commandContextFactory != null) {
+                String engineCfgKey = getEngineCfgKey();
+                CommandContextInterceptor commandContextInterceptor = new CommandContextInterceptor(commandContextFactory,
+                    classLoader, useClassForNameClassLoading, clock, objectMapper);
+                engineConfigurations.put(engineCfgKey, this);
+                commandContextInterceptor.setEngineCfgKey(engineCfgKey);
+                commandContextInterceptor.setEngineConfigurations(engineConfigurations);
+                interceptors.add(commandContextInterceptor);
+            }
+
+            if (transactionContextFactory != null) {
+                interceptors.add(new TransactionContextInterceptor(transactionContextFactory));
+            }
+
+            List<CommandInterceptor> additionalCommandInterceptors = getAdditionalDefaultCommandInterceptors();
+            if (additionalCommandInterceptors != null) {
+                interceptors.addAll(additionalCommandInterceptors);
+            }
+
+            defaultCommandInterceptors = interceptors;
+        }
+        return defaultCommandInterceptors;
+    }
+
+    public abstract String getEngineCfgKey();
+
+    public abstract String getEngineScopeType();
+
+    public List<CommandInterceptor> getAdditionalDefaultCommandInterceptors() {
+        return null;
+    }
+
+    public void initCommandExecutor() {
+        if (commandExecutor == null) {
+            CommandInterceptor first = initInterceptorChain(commandInterceptors);
+            commandExecutor = new CommandExecutorImpl(getDefaultCommandConfig(), first);
+        }
+    }
+
+    public CommandInterceptor initInterceptorChain(List<CommandInterceptor> chain) {
+        if (chain == null || chain.isEmpty()) {
+            throw new FlowableException("invalid command interceptor chain configuration: " + chain);
+        }
+        for (int i = 0; i < chain.size() - 1; i++) {
+            chain.get(i).setNext(chain.get(i + 1));
+        }
+        return chain.get(0);
+    }
+
+    public abstract CommandInterceptor createTransactionInterceptor();
+
+
+    public void initBeans() {
+        if (beans == null) {
+            beans = new HashMap<>();
+        }
+    }
+
+    // id generator
+    // /////////////////////////////////////////////////////////////
+
+    public void initIdGenerator() {
+        if (idGenerator == null) {
+            idGenerator = new StrongUuidGenerator();
+        }
+    }
+
+    public void initObjectMapper() {
+        if (objectMapper == null) {
+            objectMapper = new ObjectMapper();
+            objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+        }
+    }
+
+    public void initClock() {
+        if (clock == null) {
+            clock = new DefaultClockImpl();
+        }
+    }
+
+    // Data managers ///////////////////////////////////////////////////////////
+
+    public void initDataManagers() {
+        if (propertyDataManager == null) {
+            propertyDataManager = new MybatisPropertyDataManager(idGenerator);
+        }
+
+        if (byteArrayDataManager == null) {
+            byteArrayDataManager = new MybatisByteArrayDataManager(idGenerator);
+        }
+    }
+
+    // Entity managers //////////////////////////////////////////////////////////
+
+    public void initEntityManagers() {
+        if (propertyEntityManager == null) {
+            propertyEntityManager = new PropertyEntityManagerImpl(this, propertyDataManager);
+        }
+
+        if (byteArrayEntityManager == null) {
+            byteArrayEntityManager = new ByteArrayEntityManagerImpl(byteArrayDataManager, getEngineCfgKey(), this::getEventDispatcher);
+        }
+
+        if (tableDataManager == null) {
+            tableDataManager = new TableDataManagerImpl(this);
+        }
+    }
+
+    // services
+    // /////////////////////////////////////////////////////////////////
+
+    protected void initService(Object service) {
+        if (service instanceof CommonEngineServiceImpl) {
+            ((CommonEngineServiceImpl) service).setCommandExecutor(commandExecutor);
+        }
+    }
+
+    // myBatis SqlSessionFactory
+    // ////////////////////////////////////////////////
+
+    public void initSessionFactories() {
+        if (sessionFactories == null) {
+            sessionFactories = new HashMap<>();
+
+            if (usingRelationalDatabase) {
+                initDbSqlSessionFactory();
+            }
+
+            addSessionFactory(new GenericManagerFactory(EntityCache.class, EntityCacheImpl.class));
+
+            if (isLoggingSessionEnabled()) {
+                if (!sessionFactories.containsKey(LoggingSession.class)) {
+                    LoggingSessionFactory loggingSessionFactory = new LoggingSessionFactory();
+                    loggingSessionFactory.setLoggingListener(loggingListener);
+                    loggingSessionFactory.setObjectMapper(objectMapper);
+                    sessionFactories.put(LoggingSession.class, loggingSessionFactory);
+                }
+            }
+
+            commandContextFactory.setSessionFactories(sessionFactories);
+
+        } else {
+            if (usingRelationalDatabase) {
+                initDbSqlSessionFactoryEntitySettings();
+            }
+        }
+
+        if (customSessionFactories != null) {
+            for (SessionFactory sessionFactory : customSessionFactories) {
+                addSessionFactory(sessionFactory);
+            }
+        }
+    }
+
+    public void initDbSqlSessionFactory() {
+        if (dbSqlSessionFactory == null) {
+            dbSqlSessionFactory = createDbSqlSessionFactory();
+        }
+        dbSqlSessionFactory.setDatabaseType(databaseType);
+        dbSqlSessionFactory.setSqlSessionFactory(sqlSessionFactory);
+        dbSqlSessionFactory.setDbHistoryUsed(isDbHistoryUsed);
+        dbSqlSessionFactory.setDatabaseTablePrefix(databaseTablePrefix);
+        dbSqlSessionFactory.setTablePrefixIsSchema(tablePrefixIsSchema);
+        dbSqlSessionFactory.setDatabaseCatalog(databaseCatalog);
+        dbSqlSessionFactory.setDatabaseSchema(databaseSchema);
+        dbSqlSessionFactory.setMaxNrOfStatementsInBulkInsert(maxNrOfStatementsInBulkInsert);
+
+        initDbSqlSessionFactoryEntitySettings();
+
+        addSessionFactory(dbSqlSessionFactory);
+    }
+
+    public DbSqlSessionFactory createDbSqlSessionFactory() {
+        return new DbSqlSessionFactory(usePrefixId);
+    }
+
+    protected abstract void initDbSqlSessionFactoryEntitySettings();
+
+    protected void defaultInitDbSqlSessionFactoryEntitySettings(List<Class<? extends Entity>> insertOrder, List<Class<? extends Entity>> deleteOrder) {
+        if (insertOrder != null) {
+            for (Class<? extends Entity> clazz : insertOrder) {
+                dbSqlSessionFactory.getInsertionOrder().add(clazz);
+
+                if (isBulkInsertEnabled) {
+                    dbSqlSessionFactory.getBulkInserteableEntityClasses().add(clazz);
+                }
+            }
+        }
+
+        if (deleteOrder != null) {
+            for (Class<? extends Entity> clazz : deleteOrder) {
+                dbSqlSessionFactory.getDeletionOrder().add(clazz);
+            }
+        }
+    }
+
+    public void initTransactionFactory() {
+        if (transactionFactory == null) {
+            if (transactionsExternallyManaged) {
+                transactionFactory = new ManagedTransactionFactory();
+                Properties properties = new Properties();
+                properties.put("closeConnection", "false");
+                this.transactionFactory.setProperties(properties);
+            } else {
+                transactionFactory = new JdbcTransactionFactory();
+            }
+        }
+    }
+
+    public void initSqlSessionFactory() {
+        if (sqlSessionFactory == null) {
+            InputStream inputStream = null;
+            try {
+                inputStream = getMyBatisXmlConfigurationStream();
+
+                Environment environment = new Environment("default", transactionFactory, dataSource);
+                Reader reader = new InputStreamReader(inputStream);
+                Properties properties = new Properties();
+                properties.put("prefix", databaseTablePrefix);
+
+                String wildcardEscapeClause = "";
+                if ((databaseWildcardEscapeCharacter != null) && (databaseWildcardEscapeCharacter.length() != 0)) {
+                    wildcardEscapeClause = " escape '" + databaseWildcardEscapeCharacter + "'";
+                }
+                properties.put("wildcardEscapeClause", wildcardEscapeClause);
+
+                // set default properties
+                properties.put("limitBefore", "");
+                properties.put("limitAfter", "");
+                properties.put("limitBetween", "");
+                properties.put("limitBeforeNativeQuery", "");
+                properties.put("limitAfterNativeQuery", "");
+                properties.put("blobType", "BLOB");
+                properties.put("boolValue", "TRUE");
+
+                if (databaseType != null) {
+                    properties.load(getResourceAsStream(pathToEngineDbProperties()));
+                }
+
+                Configuration configuration = initMybatisConfiguration(environment, reader, properties);
+                sqlSessionFactory = new DefaultSqlSessionFactory(configuration);
+
+            } catch (Exception e) {
+                throw new FlowableException("Error while building ibatis SqlSessionFactory: " + e.getMessage(), e);
+            } finally {
+                IoUtil.closeSilently(inputStream);
+            }
+        } else {
+            // This is needed when the SQL Session Factory is created by another engine.
+            // When custom XML Mappers are registered with this engine they need to be loaded in the configuration as well
+            applyCustomMybatisCustomizations(sqlSessionFactory.getConfiguration());
+        }
+    }
+
+    public String pathToEngineDbProperties() {
+        return "org/flowable/common/db/properties/" + databaseType + ".properties";
+    }
+
+    public Configuration initMybatisConfiguration(Environment environment, Reader reader, Properties properties) {
+        XMLConfigBuilder parser = new XMLConfigBuilder(reader, "", properties);
+        Configuration configuration = parser.getConfiguration();
+
+        if (databaseType != null) {
+            configuration.setDatabaseId(databaseType);
+        }
+
+        configuration.setEnvironment(environment);
+
+        initMybatisTypeHandlers(configuration);
+        initCustomMybatisInterceptors(configuration);
+        if (isEnableLogSqlExecutionTime()) {
+            initMyBatisLogSqlExecutionTimePlugin(configuration);
+        }
+
+        configuration = parseMybatisConfiguration(parser);
+        return configuration;
+    }
+
+    public void initCustomMybatisMappers(Configuration configuration) {
+        if (getCustomMybatisMappers() != null) {
+            for (Class<?> clazz : getCustomMybatisMappers()) {
+                if (!configuration.hasMapper(clazz)) {
+                    configuration.addMapper(clazz);
+                }
+            }
+        }
+    }
+
+    public void initMybatisTypeHandlers(Configuration configuration) {
+        // When mapping into Map<String, Object> there is currently a problem with MyBatis.
+        // It will return objects which are driver specific.
+        // Therefore we are registering the mappings between Object.class and the specific jdbc type here.
+        // see https://github.com/mybatis/mybatis-3/issues/2216 for more info
+        TypeHandlerRegistry handlerRegistry = configuration.getTypeHandlerRegistry();
+
+        handlerRegistry.register(Object.class, JdbcType.BOOLEAN, new BooleanTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.BIT, new BooleanTypeHandler());
+
+        handlerRegistry.register(Object.class, JdbcType.TINYINT, new ByteTypeHandler());
+
+        handlerRegistry.register(Object.class, JdbcType.SMALLINT, new ShortTypeHandler());
+
+        handlerRegistry.register(Object.class, JdbcType.INTEGER, new IntegerTypeHandler());
+
+        handlerRegistry.register(Object.class, JdbcType.FLOAT, new FloatTypeHandler());
+
+        handlerRegistry.register(Object.class, JdbcType.DOUBLE, new DoubleTypeHandler());
+
+        handlerRegistry.register(Object.class, JdbcType.CHAR, new StringTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.CLOB, new ClobTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.VARCHAR, new StringTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.LONGVARCHAR, new StringTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.NVARCHAR, new NStringTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.NCHAR, new NStringTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.NCLOB, new NClobTypeHandler());
+
+        handlerRegistry.register(Object.class, JdbcType.ARRAY, new ArrayTypeHandler());
+
+        handlerRegistry.register(Object.class, JdbcType.BIGINT, new LongTypeHandler());
+
+        handlerRegistry.register(Object.class, JdbcType.REAL, new BigDecimalTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.DECIMAL, new BigDecimalTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.NUMERIC, new BigDecimalTypeHandler());
+
+        handlerRegistry.register(Object.class, JdbcType.BLOB, new BlobInputStreamTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.LONGVARBINARY, new BlobTypeHandler());
+
+        handlerRegistry.register(Object.class, JdbcType.DATE, new DateOnlyTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.TIME, new TimeOnlyTypeHandler());
+        handlerRegistry.register(Object.class, JdbcType.TIMESTAMP, new DateTypeHandler());
+
+        handlerRegistry.register(Object.class, JdbcType.SQLXML, new SqlxmlTypeHandler());
+    }
+
+    public void initCustomMybatisInterceptors(Configuration configuration) {
+        if (customMybatisInterceptors!=null){
+            for (Interceptor interceptor :customMybatisInterceptors){
+                configuration.addInterceptor(interceptor);
+            }
+        }
+    }
+
+    public void initMyBatisLogSqlExecutionTimePlugin(Configuration configuration) {
+        configuration.addInterceptor(new LogSqlExecutionTimePlugin());
+    }
+
+    public Configuration parseMybatisConfiguration(XMLConfigBuilder parser) {
+        Configuration configuration = parser.parse();
+
+        applyCustomMybatisCustomizations(configuration);
+        return configuration;
+    }
+
+    protected void applyCustomMybatisCustomizations(Configuration configuration) {
+        initCustomMybatisMappers(configuration);
+
+        if (dependentEngineMybatisTypeAliasConfigs != null) {
+            for (MybatisTypeAliasConfigurator typeAliasConfig : dependentEngineMybatisTypeAliasConfigs) {
+                typeAliasConfig.configure(configuration.getTypeAliasRegistry());
+            }
+        }
+        if (dependentEngineMybatisTypeHandlerConfigs != null) {
+            for (MybatisTypeHandlerConfigurator typeHandlerConfig : dependentEngineMybatisTypeHandlerConfigs) {
+                typeHandlerConfig.configure(configuration.getTypeHandlerRegistry());
+            }
+        }
+
+        parseDependentEngineMybatisXMLMappers(configuration);
+        parseCustomMybatisXMLMappers(configuration);
+    }
+
+    public void parseCustomMybatisXMLMappers(Configuration configuration) {
+        if (getCustomMybatisXMLMappers() != null) {
+            for (String resource : getCustomMybatisXMLMappers()) {
+                parseMybatisXmlMapping(configuration, resource);
+            }
+        }
+    }
+
+    public void parseDependentEngineMybatisXMLMappers(Configuration configuration) {
+        if (getDependentEngineMyBatisXmlMappers() != null) {
+            for (String resource : getDependentEngineMyBatisXmlMappers()) {
+                parseMybatisXmlMapping(configuration, resource);
+            }
+        }
+    }
+
+    protected void parseMybatisXmlMapping(Configuration configuration, String resource) {
+        // see XMLConfigBuilder.mapperElement()
+        XMLMapperBuilder mapperParser = new XMLMapperBuilder(getResourceAsStream(resource), configuration, resource, configuration.getSqlFragments());
+        mapperParser.parse();
+    }
+
+    protected InputStream getResourceAsStream(String resource) {
+        ClassLoader classLoader = getClassLoader();
+        if (classLoader != null) {
+            return getClassLoader().getResourceAsStream(resource);
+        } else {
+            return this.getClass().getClassLoader().getResourceAsStream(resource);
+        }
+    }
+
+    public void setMybatisMappingFile(String file) {
+        this.mybatisMappingFile = file;
+    }
+
+    public String getMybatisMappingFile() {
+        return mybatisMappingFile;
+    }
+
+    public abstract InputStream getMyBatisXmlConfigurationStream();
+
+    public void initConfigurators() {
+
+        allConfigurators = new ArrayList<>();
+        allConfigurators.addAll(getEngineSpecificEngineConfigurators());
+
+        // Configurators that are explicitly added to the config
+        if (configurators != null) {
+            allConfigurators.addAll(configurators);
+        }
+
+        // Auto discovery through ServiceLoader
+        if (enableConfiguratorServiceLoader) {
+            ClassLoader classLoader = getClassLoader();
+            if (classLoader == null) {
+                classLoader = ReflectUtil.getClassLoader();
+            }
+
+            ServiceLoader<EngineConfigurator> configuratorServiceLoader = ServiceLoader.load(EngineConfigurator.class, classLoader);
+            int nrOfServiceLoadedConfigurators = 0;
+            for (EngineConfigurator configurator : configuratorServiceLoader) {
+                allConfigurators.add(configurator);
+                nrOfServiceLoadedConfigurators++;
+            }
+
+            if (nrOfServiceLoadedConfigurators > 0) {
+                logger.info("Found {} auto-discoverable Process Engine Configurator{}", nrOfServiceLoadedConfigurators, nrOfServiceLoadedConfigurators > 1 ? "s" : "");
+            }
+
+            if (!allConfigurators.isEmpty()) {
+
+                // Order them according to the priorities (useful for dependent
+                // configurator)
+                allConfigurators.sort(new Comparator<EngineConfigurator>() {
+
+                    @Override
+                    public int compare(EngineConfigurator configurator1, EngineConfigurator configurator2) {
+                        int priority1 = configurator1.getPriority();
+                        int priority2 = configurator2.getPriority();
+
+                        if (priority1 < priority2) {
+                            return -1;
+                        } else if (priority1 > priority2) {
+                            return 1;
+                        }
+                        return 0;
+                    }
+                });
+
+                // Execute the configurators
+                logger.info("Found {} Engine Configurators in total:", allConfigurators.size());
+                for (EngineConfigurator configurator : allConfigurators) {
+                    logger.info("{} (priority:{})", configurator.getClass(), configurator.getPriority());
+                }
+
+            }
+
+        }
+    }
+
+    public void close() {
+        if (forceCloseMybatisConnectionPool && dataSource instanceof PooledDataSource) {
+            /*
+             * When the datasource is created by a Flowable engine (i.e. it's an instance of PooledDataSource),
+             * the connection pool needs to be closed when closing the engine.
+             * Note that calling forceCloseAll() multiple times (as is the case when running with multiple engine) is ok.
+             */
+            ((PooledDataSource) dataSource).forceCloseAll();
+        }
+    }
+
+    protected List<EngineConfigurator> getEngineSpecificEngineConfigurators() {
+        // meant to be overridden if needed
+        return Collections.emptyList();
+    }
+
+    public void configuratorsBeforeInit() {
+        for (EngineConfigurator configurator : allConfigurators) {
+            logger.info("Executing beforeInit() of {} (priority:{})", configurator.getClass(), configurator.getPriority());
+            configurator.beforeInit(this);
+        }
+    }
+
+    public void configuratorsAfterInit() {
+        for (EngineConfigurator configurator : allConfigurators) {
+            logger.info("Executing configure() of {} (priority:{})", configurator.getClass(), configurator.getPriority());
+            configurator.configure(this);
+        }
+    }
+
+    public LockManager getLockManager(String lockName) {
+        return new LockManagerImpl(commandExecutor, lockName, getLockPollRate(), getEngineCfgKey());
+    }
+
+    // getters and setters
+    // //////////////////////////////////////////////////////
+
+    public abstract String getEngineName();
+
+    public ClassLoader getClassLoader() {
+        return classLoader;
+    }
+
+    public AbstractEngineConfiguration setClassLoader(ClassLoader classLoader) {
+        this.classLoader = classLoader;
+        return this;
+    }
+
+    public boolean isUseClassForNameClassLoading() {
+        return useClassForNameClassLoading;
+    }
+
+    public AbstractEngineConfiguration setUseClassForNameClassLoading(boolean useClassForNameClassLoading) {
+        this.useClassForNameClassLoading = useClassForNameClassLoading;
+        return this;
+    }
+
+    public void addEngineLifecycleListener(EngineLifecycleListener engineLifecycleListener) {
+        if (this.engineLifecycleListeners == null) {
+            this.engineLifecycleListeners = new ArrayList<>();
+        }
+        this.engineLifecycleListeners.add(engineLifecycleListener);
+    }
+
+    public List<EngineLifecycleListener> getEngineLifecycleListeners() {
+        return engineLifecycleListeners;
+    }
+
+    public AbstractEngineConfiguration setEngineLifecycleListeners(List<EngineLifecycleListener> engineLifecycleListeners) {
+        this.engineLifecycleListeners = engineLifecycleListeners;
+        return this;
+    }
+
+    public String getDatabaseType() {
+        return databaseType;
+    }
+
+    public AbstractEngineConfiguration setDatabaseType(String databaseType) {
+        this.databaseType = databaseType;
+        return this;
+    }
+
+    public DataSource getDataSource() {
+        return dataSource;
+    }
+
+    public AbstractEngineConfiguration setDataSource(DataSource dataSource) {
+        this.dataSource = dataSource;
+        return this;
+    }
+
+    public SchemaManager getSchemaManager() {
+        return schemaManager;
+    }
+
+    public AbstractEngineConfiguration setSchemaManager(SchemaManager schemaManager) {
+        this.schemaManager = schemaManager;
+        return this;
+    }
+
+    public SchemaManager getCommonSchemaManager() {
+        return commonSchemaManager;
+    }
+
+    public AbstractEngineConfiguration setCommonSchemaManager(SchemaManager commonSchemaManager) {
+        this.commonSchemaManager = commonSchemaManager;
+        return this;
+    }
+
+    public Command<Void> getSchemaManagementCmd() {
+        return schemaManagementCmd;
+    }
+
+    public AbstractEngineConfiguration setSchemaManagementCmd(Command<Void> schemaManagementCmd) {
+        this.schemaManagementCmd = schemaManagementCmd;
+        return this;
+    }
+
+    public String getJdbcDriver() {
+        return jdbcDriver;
+    }
+
+    public AbstractEngineConfiguration setJdbcDriver(String jdbcDriver) {
+        this.jdbcDriver = jdbcDriver;
+        return this;
+    }
+
+    public String getJdbcUrl() {
+        return jdbcUrl;
+    }
+
+    public AbstractEngineConfiguration setJdbcUrl(String jdbcUrl) {
+        this.jdbcUrl = jdbcUrl;
+        return this;
+    }
+
+    public String getJdbcUsername() {
+        return jdbcUsername;
+    }
+
+    public AbstractEngineConfiguration setJdbcUsername(String jdbcUsername) {
+        this.jdbcUsername = jdbcUsername;
+        return this;
+    }
+
+    public String getJdbcPassword() {
+        return jdbcPassword;
+    }
+
+    public AbstractEngineConfiguration setJdbcPassword(String jdbcPassword) {
+        this.jdbcPassword = jdbcPassword;
+        return this;
+    }
+
+    public int getJdbcMaxActiveConnections() {
+        return jdbcMaxActiveConnections;
+    }
+
+    public AbstractEngineConfiguration setJdbcMaxActiveConnections(int jdbcMaxActiveConnections) {
+        this.jdbcMaxActiveConnections = jdbcMaxActiveConnections;
+        return this;
+    }
+
+    public int getJdbcMaxIdleConnections() {
+        return jdbcMaxIdleConnections;
+    }
+
+    public AbstractEngineConfiguration setJdbcMaxIdleConnections(int jdbcMaxIdleConnections) {
+        this.jdbcMaxIdleConnections = jdbcMaxIdleConnections;
+        return this;
+    }
+
+    public int getJdbcMaxCheckoutTime() {
+        return jdbcMaxCheckoutTime;
+    }
+
+    public AbstractEngineConfiguration setJdbcMaxCheckoutTime(int jdbcMaxCheckoutTime) {
+        this.jdbcMaxCheckoutTime = jdbcMaxCheckoutTime;
+        return this;
+    }
+
+    public int getJdbcMaxWaitTime() {
+        return jdbcMaxWaitTime;
+    }
+
+    public AbstractEngineConfiguration setJdbcMaxWaitTime(int jdbcMaxWaitTime) {
+        this.jdbcMaxWaitTime = jdbcMaxWaitTime;
+        return this;
+    }
+
+    public boolean isJdbcPingEnabled() {
+        return jdbcPingEnabled;
+    }
+
+    public AbstractEngineConfiguration setJdbcPingEnabled(boolean jdbcPingEnabled) {
+        this.jdbcPingEnabled = jdbcPingEnabled;
+        return this;
+    }
+
+    public int getJdbcPingConnectionNotUsedFor() {
+        return jdbcPingConnectionNotUsedFor;
+    }
+
+    public AbstractEngineConfiguration setJdbcPingConnectionNotUsedFor(int jdbcPingConnectionNotUsedFor) {
+        this.jdbcPingConnectionNotUsedFor = jdbcPingConnectionNotUsedFor;
+        return this;
+    }
+
+    public int getJdbcDefaultTransactionIsolationLevel() {
+        return jdbcDefaultTransactionIsolationLevel;
+    }
+
+    public AbstractEngineConfiguration setJdbcDefaultTransactionIsolationLevel(int jdbcDefaultTransactionIsolationLevel) {
+        this.jdbcDefaultTransactionIsolationLevel = jdbcDefaultTransactionIsolationLevel;
+        return this;
+    }
+
+    public String getJdbcPingQuery() {
+        return jdbcPingQuery;
+    }
+
+    public AbstractEngineConfiguration setJdbcPingQuery(String jdbcPingQuery) {
+        this.jdbcPingQuery = jdbcPingQuery;
+        return this;
+    }
+
+    public String getDataSourceJndiName() {
+        return dataSourceJndiName;
+    }
+
+    public AbstractEngineConfiguration setDataSourceJndiName(String dataSourceJndiName) {
+        this.dataSourceJndiName = dataSourceJndiName;
+        return this;
+    }
+
+    public CommandConfig getSchemaCommandConfig() {
+        return schemaCommandConfig;
+    }
+
+    public AbstractEngineConfiguration setSchemaCommandConfig(CommandConfig schemaCommandConfig) {
+        this.schemaCommandConfig = schemaCommandConfig;
+        return this;
+    }
+
+    public boolean isTransactionsExternallyManaged() {
+        return transactionsExternallyManaged;
+    }
+
+    public AbstractEngineConfiguration setTransactionsExternallyManaged(boolean transactionsExternallyManaged) {
+        this.transactionsExternallyManaged = transactionsExternallyManaged;
+        return this;
+    }
+
+    public Map<Object, Object> getBeans() {
+        return beans;
+    }
+
+    public AbstractEngineConfiguration setBeans(Map<Object, Object> beans) {
+        this.beans = beans;
+        return this;
+    }
+
+    public IdGenerator getIdGenerator() {
+        return idGenerator;
+    }
+
+    public AbstractEngineConfiguration setIdGenerator(IdGenerator idGenerator) {
+        this.idGenerator = idGenerator;
+        return this;
+    }
+
+    public boolean isUsePrefixId() {
+        return usePrefixId;
+    }
+
+    public AbstractEngineConfiguration setUsePrefixId(boolean usePrefixId) {
+        this.usePrefixId = usePrefixId;
+        return this;
+    }
+
+    public String getXmlEncoding() {
+        return xmlEncoding;
+    }
+
+    public AbstractEngineConfiguration setXmlEncoding(String xmlEncoding) {
+        this.xmlEncoding = xmlEncoding;
+        return this;
+    }
+
+    public CommandConfig getDefaultCommandConfig() {
+        return defaultCommandConfig;
+    }
+
+    public AbstractEngineConfiguration setDefaultCommandConfig(CommandConfig defaultCommandConfig) {
+        this.defaultCommandConfig = defaultCommandConfig;
+        return this;
+    }
+
+    public CommandExecutor getCommandExecutor() {
+        return commandExecutor;
+    }
+
+    public AbstractEngineConfiguration setCommandExecutor(CommandExecutor commandExecutor) {
+        this.commandExecutor = commandExecutor;
+        return this;
+    }
+
+    public CommandContextFactory getCommandContextFactory() {
+        return commandContextFactory;
+    }
+
+    public AbstractEngineConfiguration setCommandContextFactory(CommandContextFactory commandContextFactory) {
+        this.commandContextFactory = commandContextFactory;
+        return this;
+    }
+
+    public CommandInterceptor getCommandInvoker() {
+        return commandInvoker;
+    }
+
+    public AbstractEngineConfiguration setCommandInvoker(CommandInterceptor commandInvoker) {
+        this.commandInvoker = commandInvoker;
+        return this;
+    }
+
+    public AgendaOperationRunner getAgendaOperationRunner() {
+        return agendaOperationRunner;
+    }
+
+    public AbstractEngineConfiguration setAgendaOperationRunner(AgendaOperationRunner agendaOperationRunner) {
+        this.agendaOperationRunner = agendaOperationRunner;
+        return this;
+    }
+
+    public List<CommandInterceptor> getCustomPreCommandInterceptors() {
+        return customPreCommandInterceptors;
+    }
+
+    public AbstractEngineConfiguration setCustomPreCommandInterceptors(List<CommandInterceptor> customPreCommandInterceptors) {
+        this.customPreCommandInterceptors = customPreCommandInterceptors;
+        return this;
+    }
+
+    public List<CommandInterceptor> getCustomPostCommandInterceptors() {
+        return customPostCommandInterceptors;
+    }
+
+    public AbstractEngineConfiguration setCustomPostCommandInterceptors(List<CommandInterceptor> customPostCommandInterceptors) {
+        this.customPostCommandInterceptors = customPostCommandInterceptors;
+        return this;
+    }
+
+    public List<CommandInterceptor> getCommandInterceptors() {
+        return commandInterceptors;
+    }
+
+    public AbstractEngineConfiguration setCommandInterceptors(List<CommandInterceptor> commandInterceptors) {
+        this.commandInterceptors = commandInterceptors;
+        return this;
+    }
+
+    public Map<String, AbstractEngineConfiguration> getEngineConfigurations() {
+        return engineConfigurations;
+    }
+
+    public AbstractEngineConfiguration setEngineConfigurations(Map<String, AbstractEngineConfiguration> engineConfigurations) {
+        this.engineConfigurations = engineConfigurations;
+        return this;
+    }
+
+    public void addEngineConfiguration(String key, String scopeType, AbstractEngineConfiguration engineConfiguration) {
+        if (engineConfigurations == null) {
+            engineConfigurations = new HashMap<>();
+        }
+        engineConfigurations.put(key, engineConfiguration);
+        engineConfigurations.put(scopeType, engineConfiguration);
+    }
+
+    public Map<String, AbstractServiceConfiguration> getServiceConfigurations() {
+        return serviceConfigurations;
+    }
+
+    public AbstractEngineConfiguration setServiceConfigurations(Map<String, AbstractServiceConfiguration> serviceConfigurations) {
+        this.serviceConfigurations = serviceConfigurations;
+        return this;
+    }
+
+    public void addServiceConfiguration(String key, AbstractServiceConfiguration serviceConfiguration) {
+        if (serviceConfigurations == null) {
+            serviceConfigurations = new HashMap<>();
+        }
+        serviceConfigurations.put(key, serviceConfiguration);
+    }
+
+    public Map<String, EventRegistryEventConsumer> getEventRegistryEventConsumers() {
+        return eventRegistryEventConsumers;
+    }
+
+    public AbstractEngineConfiguration setEventRegistryEventConsumers(Map<String, EventRegistryEventConsumer> eventRegistryEventConsumers) {
+        this.eventRegistryEventConsumers = eventRegistryEventConsumers;
+        return this;
+    }
+
+    public void addEventRegistryEventConsumer(String key, EventRegistryEventConsumer eventRegistryEventConsumer) {
+        if (eventRegistryEventConsumers == null) {
+            eventRegistryEventConsumers = new HashMap<>();
+        }
+        eventRegistryEventConsumers.put(key, eventRegistryEventConsumer);
+    }
+
+    public AbstractEngineConfiguration setDefaultCommandInterceptors(Collection<? extends CommandInterceptor> defaultCommandInterceptors) {
+        this.defaultCommandInterceptors = defaultCommandInterceptors;
+        return this;
+    }
+
+    public SqlSessionFactory getSqlSessionFactory() {
+        return sqlSessionFactory;
+    }
+
+    public AbstractEngineConfiguration setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
+        this.sqlSessionFactory = sqlSessionFactory;
+        return this;
+    }
+
+    public boolean isDbHistoryUsed() {
+        return isDbHistoryUsed;
+    }
+
+    public AbstractEngineConfiguration setDbHistoryUsed(boolean isDbHistoryUsed) {
+        this.isDbHistoryUsed = isDbHistoryUsed;
+        return this;
+    }
+
+    public DbSqlSessionFactory getDbSqlSessionFactory() {
+        return dbSqlSessionFactory;
+    }
+
+    public AbstractEngineConfiguration setDbSqlSessionFactory(DbSqlSessionFactory dbSqlSessionFactory) {
+        this.dbSqlSessionFactory = dbSqlSessionFactory;
+        return this;
+    }
+
+    public TransactionFactory getTransactionFactory() {
+        return transactionFactory;
+    }
+
+    public AbstractEngineConfiguration setTransactionFactory(TransactionFactory transactionFactory) {
+        this.transactionFactory = transactionFactory;
+        return this;
+    }
+
+    public TransactionContextFactory getTransactionContextFactory() {
+        return transactionContextFactory;
+    }
+
+    public AbstractEngineConfiguration setTransactionContextFactory(TransactionContextFactory transactionContextFactory) {
+        this.transactionContextFactory = transactionContextFactory;
+        return this;
+    }
+
+    public int getMaxNrOfStatementsInBulkInsert() {
+        return maxNrOfStatementsInBulkInsert;
+    }
+
+    public AbstractEngineConfiguration setMaxNrOfStatementsInBulkInsert(int maxNrOfStatementsInBulkInsert) {
+        this.maxNrOfStatementsInBulkInsert = maxNrOfStatementsInBulkInsert;
+        return this;
+    }
+
+    public boolean isBulkInsertEnabled() {
+        return isBulkInsertEnabled;
+    }
+
+    public AbstractEngineConfiguration setBulkInsertEnabled(boolean isBulkInsertEnabled) {
+        this.isBulkInsertEnabled = isBulkInsertEnabled;
+        return this;
+    }
+
+    public Set<Class<?>> getCustomMybatisMappers() {
+        return customMybatisMappers;
+    }
+
+    public AbstractEngineConfiguration setCustomMybatisMappers(Set<Class<?>> customMybatisMappers) {
+        this.customMybatisMappers = customMybatisMappers;
+        return this;
+    }
+
+    public Set<String> getCustomMybatisXMLMappers() {
+        return customMybatisXMLMappers;
+    }
+
+    public AbstractEngineConfiguration setCustomMybatisXMLMappers(Set<String> customMybatisXMLMappers) {
+        this.customMybatisXMLMappers = customMybatisXMLMappers;
+        return this;
+    }
+
+    public Set<String> getDependentEngineMyBatisXmlMappers() {
+        return dependentEngineMyBatisXmlMappers;
+    }
+
+    public AbstractEngineConfiguration setCustomMybatisInterceptors(List<Interceptor> customMybatisInterceptors) {
+        this.customMybatisInterceptors = customMybatisInterceptors;
+        return  this;
+    }
+
+    public List<Interceptor> getCustomMybatisInterceptors() {
+        return customMybatisInterceptors;
+    }
+
+    public AbstractEngineConfiguration setDependentEngineMyBatisXmlMappers(Set<String> dependentEngineMyBatisXmlMappers) {
+        this.dependentEngineMyBatisXmlMappers = dependentEngineMyBatisXmlMappers;
+        return this;
+    }
+
+    public List<MybatisTypeAliasConfigurator> getDependentEngineMybatisTypeAliasConfigs() {
+        return dependentEngineMybatisTypeAliasConfigs;
+    }
+
+    public AbstractEngineConfiguration setDependentEngineMybatisTypeAliasConfigs(List<MybatisTypeAliasConfigurator> dependentEngineMybatisTypeAliasConfigs) {
+        this.dependentEngineMybatisTypeAliasConfigs = dependentEngineMybatisTypeAliasConfigs;
+        return this;
+    }
+
+    public List<MybatisTypeHandlerConfigurator> getDependentEngineMybatisTypeHandlerConfigs() {
+        return dependentEngineMybatisTypeHandlerConfigs;
+    }
+
+    public AbstractEngineConfiguration setDependentEngineMybatisTypeHandlerConfigs(List<MybatisTypeHandlerConfigurator> dependentEngineMybatisTypeHandlerConfigs) {
+        this.dependentEngineMybatisTypeHandlerConfigs = dependentEngineMybatisTypeHandlerConfigs;
+        return this;
+    }
+
+    public List<SessionFactory> getCustomSessionFactories() {
+        return customSessionFactories;
+    }
+
+    public AbstractEngineConfiguration addCustomSessionFactory(SessionFactory sessionFactory) {
+        if (customSessionFactories == null) {
+            customSessionFactories = new ArrayList<>();
+        }
+        customSessionFactories.add(sessionFactory);
+        return this;
+    }
+
+    public AbstractEngineConfiguration setCustomSessionFactories(List<SessionFactory> customSessionFactories) {
+        this.customSessionFactories = customSessionFactories;
+        return this;
+    }
+
+    public boolean isUsingRelationalDatabase() {
+        return usingRelationalDatabase;
+    }
+
+    public AbstractEngineConfiguration setUsingRelationalDatabase(boolean usingRelationalDatabase) {
+        this.usingRelationalDatabase = usingRelationalDatabase;
+        return this;
+    }
+
+    public boolean isUsingSchemaMgmt() {
+        return usingSchemaMgmt;
+    }
+
+    public AbstractEngineConfiguration setUsingSchemaMgmt(boolean usingSchema) {
+        this.usingSchemaMgmt = usingSchema;
+        return this;
+    }
+
+    public String getDatabaseTablePrefix() {
+        return databaseTablePrefix;
+    }
+
+    public AbstractEngineConfiguration setDatabaseTablePrefix(String databaseTablePrefix) {
+        this.databaseTablePrefix = databaseTablePrefix;
+        return this;
+    }
+
+    public String getDatabaseWildcardEscapeCharacter() {
+        return databaseWildcardEscapeCharacter;
+    }
+
+    public AbstractEngineConfiguration setDatabaseWildcardEscapeCharacter(String databaseWildcardEscapeCharacter) {
+        this.databaseWildcardEscapeCharacter = databaseWildcardEscapeCharacter;
+        return this;
+    }
+
+    public String getDatabaseCatalog() {
+        return databaseCatalog;
+    }
+
+    public AbstractEngineConfiguration setDatabaseCatalog(String databaseCatalog) {
+        this.databaseCatalog = databaseCatalog;
+        return this;
+    }
+
+    public String getDatabaseSchema() {
+        return databaseSchema;
+    }
+
+    public AbstractEngineConfiguration setDatabaseSchema(String databaseSchema) {
+        this.databaseSchema = databaseSchema;
+        return this;
+    }
+
+    public boolean isTablePrefixIsSchema() {
+        return tablePrefixIsSchema;
+    }
+
+    public AbstractEngineConfiguration setTablePrefixIsSchema(boolean tablePrefixIsSchema) {
+        this.tablePrefixIsSchema = tablePrefixIsSchema;
+        return this;
+    }
+
+    public boolean isAlwaysLookupLatestDefinitionVersion() {
+        return alwaysLookupLatestDefinitionVersion;
+    }
+
+    public AbstractEngineConfiguration setAlwaysLookupLatestDefinitionVersion(boolean alwaysLookupLatestDefinitionVersion) {
+        this.alwaysLookupLatestDefinitionVersion = alwaysLookupLatestDefinitionVersion;
+        return this;
+    }
+
+    public boolean isFallbackToDefaultTenant() {
+        return fallbackToDefaultTenant;
+    }
+
+    public AbstractEngineConfiguration setFallbackToDefaultTenant(boolean fallbackToDefaultTenant) {
+        this.fallbackToDefaultTenant = fallbackToDefaultTenant;
+        return this;
+    }
+
+    /**
+     * @return name of the default tenant
+     * @deprecated use {@link AbstractEngineConfiguration#getDefaultTenantProvider()} instead
+     */
+    @Deprecated
+    public String getDefaultTenantValue() {
+        return getDefaultTenantProvider().getDefaultTenant(null, null, null);
+    }
+
+    public AbstractEngineConfiguration setDefaultTenantValue(String defaultTenantValue) {
+        this.defaultTenantProvider = (tenantId, scope, scopeKey) -> defaultTenantValue;
+        return this;
+    }
+
+    public DefaultTenantProvider getDefaultTenantProvider() {
+        return defaultTenantProvider;
+    }
+
+    public AbstractEngineConfiguration setDefaultTenantProvider(DefaultTenantProvider defaultTenantProvider) {
+        this.defaultTenantProvider = defaultTenantProvider;
+        return this;
+    }
+
+    public boolean isEnableLogSqlExecutionTime() {
+        return enableLogSqlExecutionTime;
+    }
+
+    public void setEnableLogSqlExecutionTime(boolean enableLogSqlExecutionTime) {
+        this.enableLogSqlExecutionTime = enableLogSqlExecutionTime;
+    }
+
+    public Map<Class<?>, SessionFactory> getSessionFactories() {
+        return sessionFactories;
+    }
+
+    public AbstractEngineConfiguration setSessionFactories(Map<Class<?>, SessionFactory> sessionFactories) {
+        this.sessionFactories = sessionFactories;
+        return this;
+    }
+
+    public String getDatabaseSchemaUpdate() {
+        return databaseSchemaUpdate;
+    }
+
+    public AbstractEngineConfiguration setDatabaseSchemaUpdate(String databaseSchemaUpdate) {
+        this.databaseSchemaUpdate = databaseSchemaUpdate;
+        return this;
+    }
+
+    public boolean isUseLockForDatabaseSchemaUpdate() {
+        return useLockForDatabaseSchemaUpdate;
+    }
+
+    public AbstractEngineConfiguration setUseLockForDatabaseSchemaUpdate(boolean useLockForDatabaseSchemaUpdate) {
+        this.useLockForDatabaseSchemaUpdate = useLockForDatabaseSchemaUpdate;
+        return this;
+    }
+
+    public boolean isEnableEventDispatcher() {
+        return enableEventDispatcher;
+    }
+
+    public AbstractEngineConfiguration setEnableEventDispatcher(boolean enableEventDispatcher) {
+        this.enableEventDispatcher = enableEventDispatcher;
+        return this;
+    }
+
+    public FlowableEventDispatcher getEventDispatcher() {
+        return eventDispatcher;
+    }
+
+    public AbstractEngineConfiguration setEventDispatcher(FlowableEventDispatcher eventDispatcher) {
+        this.eventDispatcher = eventDispatcher;
+        return this;
+    }
+
+    public List<FlowableEventListener> getEventListeners() {
+        return eventListeners;
+    }
+
+    public AbstractEngineConfiguration setEventListeners(List<FlowableEventListener> eventListeners) {
+        this.eventListeners = eventListeners;
+        return this;
+    }
+
+    public Map<String, List<FlowableEventListener>> getTypedEventListeners() {
+        return typedEventListeners;
+    }
+
+    public AbstractEngineConfiguration setTypedEventListeners(Map<String, List<FlowableEventListener>> typedEventListeners) {
+        this.typedEventListeners = typedEventListeners;
+        return this;
+    }
+
+    public List<EventDispatchAction> getAdditionalEventDispatchActions() {
+        return additionalEventDispatchActions;
+    }
+
+    public AbstractEngineConfiguration setAdditionalEventDispatchActions(List<EventDispatchAction> additionalEventDispatchActions) {
+        this.additionalEventDispatchActions = additionalEventDispatchActions;
+        return this;
+    }
+
+    public void initEventDispatcher() {
+        if (this.eventDispatcher == null) {
+            this.eventDispatcher = new FlowableEventDispatcherImpl();
+        }
+
+        initAdditionalEventDispatchActions();
+
+        this.eventDispatcher.setEnabled(enableEventDispatcher);
+
+        initEventListeners();
+        initTypedEventListeners();
+    }
+
+    protected void initEventListeners() {
+        if (eventListeners != null) {
+            for (FlowableEventListener listenerToAdd : eventListeners) {
+                this.eventDispatcher.addEventListener(listenerToAdd);
+            }
+        }
+    }
+
+    protected void initAdditionalEventDispatchActions() {
+        if (this.additionalEventDispatchActions == null) {
+            this.additionalEventDispatchActions = new ArrayList<>();
+        }
+    }
+
+    protected void initTypedEventListeners() {
+        if (typedEventListeners != null) {
+            for (Map.Entry<String, List<FlowableEventListener>> listenersToAdd : typedEventListeners.entrySet()) {
+                // Extract types from the given string
+                FlowableEngineEventType[] types = FlowableEngineEventType.getTypesFromString(listenersToAdd.getKey());
+
+                for (FlowableEventListener listenerToAdd : listenersToAdd.getValue()) {
+                    this.eventDispatcher.addEventListener(listenerToAdd, types);
+                }
+            }
+        }
+    }
+
+    public boolean isLoggingSessionEnabled() {
+        return loggingListener != null;
+    }
+
+    public LoggingListener getLoggingListener() {
+        return loggingListener;
+    }
+
+    public void setLoggingListener(LoggingListener loggingListener) {
+        this.loggingListener = loggingListener;
+    }
+
+    public Clock getClock() {
+        return clock;
+    }
+
+    public AbstractEngineConfiguration setClock(Clock clock) {
+        this.clock = clock;
+        return this;
+    }
+
+    public ObjectMapper getObjectMapper() {
+        return objectMapper;
+    }
+
+    public AbstractEngineConfiguration setObjectMapper(ObjectMapper objectMapper) {
+        this.objectMapper = objectMapper;
+        return this;
+    }
+
+    public int getMaxLengthString() {
+        if (maxLengthStringVariableType == -1) {
+            if ("oracle".equalsIgnoreCase(databaseType)) {
+                return DEFAULT_ORACLE_MAX_LENGTH_STRING;
+            } else {
+                return DEFAULT_GENERIC_MAX_LENGTH_STRING;
+            }
+        } else {
+            return maxLengthStringVariableType;
+        }
+    }
+
+    public int getMaxLengthStringVariableType() {
+        return maxLengthStringVariableType;
+    }
+
+    public AbstractEngineConfiguration setMaxLengthStringVariableType(int maxLengthStringVariableType) {
+        this.maxLengthStringVariableType = maxLengthStringVariableType;
+        return this;
+    }
+
+    public PropertyDataManager getPropertyDataManager() {
+        return propertyDataManager;
+    }
+
+    public Duration getLockPollRate() {
+        return lockPollRate;
+    }
+
+    public AbstractEngineConfiguration setLockPollRate(Duration lockPollRate) {
+        this.lockPollRate = lockPollRate;
+        return this;
+    }
+
+    public Duration getSchemaLockWaitTime() {
+        return schemaLockWaitTime;
+    }
+
+    public void setSchemaLockWaitTime(Duration schemaLockWaitTime) {
+        this.schemaLockWaitTime = schemaLockWaitTime;
+    }
+
+    public AbstractEngineConfiguration setPropertyDataManager(PropertyDataManager propertyDataManager) {
+        this.propertyDataManager = propertyDataManager;
+        return this;
+    }
+
+    public PropertyEntityManager getPropertyEntityManager() {
+        return propertyEntityManager;
+    }
+
+    public AbstractEngineConfiguration setPropertyEntityManager(PropertyEntityManager propertyEntityManager) {
+        this.propertyEntityManager = propertyEntityManager;
+        return this;
+    }
+
+    public ByteArrayDataManager getByteArrayDataManager() {
+        return byteArrayDataManager;
+    }
+
+    public AbstractEngineConfiguration setByteArrayDataManager(ByteArrayDataManager byteArrayDataManager) {
+        this.byteArrayDataManager = byteArrayDataManager;
+        return this;
+    }
+
+    public ByteArrayEntityManager getByteArrayEntityManager() {
+        return byteArrayEntityManager;
+    }
+
+    public AbstractEngineConfiguration setByteArrayEntityManager(ByteArrayEntityManager byteArrayEntityManager) {
+        this.byteArrayEntityManager = byteArrayEntityManager;
+        return this;
+    }
+
+    public TableDataManager getTableDataManager() {
+        return tableDataManager;
+    }
+
+    public AbstractEngineConfiguration setTableDataManager(TableDataManager tableDataManager) {
+        this.tableDataManager = tableDataManager;
+        return this;
+    }
+
+    public List<EngineDeployer> getDeployers() {
+        return deployers;
+    }
+
+    public AbstractEngineConfiguration setDeployers(List<EngineDeployer> deployers) {
+        this.deployers = deployers;
+        return this;
+    }
+
+    public List<EngineDeployer> getCustomPreDeployers() {
+        return customPreDeployers;
+    }
+
+    public AbstractEngineConfiguration setCustomPreDeployers(List<EngineDeployer> customPreDeployers) {
+        this.customPreDeployers = customPreDeployers;
+        return this;
+    }
+
+    public List<EngineDeployer> getCustomPostDeployers() {
+        return customPostDeployers;
+    }
+
+    public AbstractEngineConfiguration setCustomPostDeployers(List<EngineDeployer> customPostDeployers) {
+        this.customPostDeployers = customPostDeployers;
+        return this;
+    }
+
+    public boolean isEnableConfiguratorServiceLoader() {
+        return enableConfiguratorServiceLoader;
+    }
+
+    public AbstractEngineConfiguration setEnableConfiguratorServiceLoader(boolean enableConfiguratorServiceLoader) {
+        this.enableConfiguratorServiceLoader = enableConfiguratorServiceLoader;
+        return this;
+    }
+
+    public List<EngineConfigurator> getConfigurators() {
+        return configurators;
+    }
+
+    public AbstractEngineConfiguration addConfigurator(EngineConfigurator configurator) {
+        if (configurators == null) {
+            configurators = new ArrayList<>();
+        }
+        configurators.add(configurator);
+        return this;
+    }
+
+    /**
+     * @return All {@link EngineConfigurator} instances. Will only contain values after init of the engine.
+     * Use the {@link #getConfigurators()} or {@link #addConfigurator(EngineConfigurator)} methods otherwise.
+     */
+    public List<EngineConfigurator> getAllConfigurators() {
+        return allConfigurators;
+    }
+
+    public AbstractEngineConfiguration setConfigurators(List<EngineConfigurator> configurators) {
+        this.configurators = configurators;
+        return this;
+    }
+
+    public EngineConfigurator getIdmEngineConfigurator() {
+        return idmEngineConfigurator;
+    }
+
+    public AbstractEngineConfiguration setIdmEngineConfigurator(EngineConfigurator idmEngineConfigurator) {
+        this.idmEngineConfigurator = idmEngineConfigurator;
+        return this;
+    }
+
+    public EngineConfigurator getEventRegistryConfigurator() {
+        return eventRegistryConfigurator;
+    }
+
+    public AbstractEngineConfiguration setEventRegistryConfigurator(EngineConfigurator eventRegistryConfigurator) {
+        this.eventRegistryConfigurator = eventRegistryConfigurator;
+        return this;
+    }
+
+    public AbstractEngineConfiguration setForceCloseMybatisConnectionPool(boolean forceCloseMybatisConnectionPool) {
+        this.forceCloseMybatisConnectionPool = forceCloseMybatisConnectionPool;
+        return this;
+    }
+
+    public boolean isForceCloseMybatisConnectionPool() {
+        return forceCloseMybatisConnectionPool;
+    }
+}

+ 1 - 0
sql/dm/flowable-patch/src/main/resources/META-INF/package-info.md

@@ -0,0 +1 @@
+防止IDEA将`.`和`/`混为一谈

+ 21 - 0
sql/dm/flowable-patch/src/main/resources/META-INF/services/liquibase.database.Database

@@ -0,0 +1,21 @@
+liquibase.database.core.CockroachDatabase
+liquibase.database.core.DB2Database
+liquibase.database.core.Db2zDatabase
+liquibase.database.core.DerbyDatabase
+liquibase.database.core.Firebird3Database
+liquibase.database.core.FirebirdDatabase
+liquibase.database.core.H2Database
+liquibase.database.core.HsqlDatabase
+liquibase.database.core.InformixDatabase
+liquibase.database.core.Ingres9Database
+liquibase.database.core.MSSQLDatabase
+liquibase.database.core.MariaDBDatabase
+liquibase.database.core.MockDatabase
+liquibase.database.core.MySQLDatabase
+liquibase.database.core.OracleDatabase
+liquibase.database.core.PostgresDatabase
+liquibase.database.core.SQLiteDatabase
+liquibase.database.core.SybaseASADatabase
+liquibase.database.core.SybaseDatabase
+liquibase.database.core.DmDatabase
+liquibase.database.core.UnsupportedDatabase

+ 0 - 0
sql/mysql/crm.sql → sql/mysql/optinal/crm.sql


+ 0 - 0
sql/mysql/crm_data.sql → sql/mysql/optinal/crm_data.sql


+ 0 - 0
sql/mysql/crm_menu.sql → sql/mysql/optinal/crm_menu.sql


+ 0 - 0
sql/mysql/mall.sql → sql/mysql/optinal/mall.sql


+ 8 - 0
sql/mysql/pay_wallet.sql → sql/mysql/optinal/pay_wallet.sql

@@ -246,3 +246,11 @@ VALUES (
            '转账订单', '', 2, 3, 1117,
            'transfer', 'ep:credit-card', 'pay/transfer/index', 0, 'PayTransfer'
        );
+
+-- 转账通知脚本
+
+ALTER TABLE `pay_app`
+    ADD COLUMN `transfer_notify_url` varchar(1024) NOT NULL COMMENT '转账结果的回调地址' AFTER `refund_notify_url`;
+ALTER TABLE  `pay_notify_task`
+    MODIFY COLUMN `merchant_order_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '商户订单编号' AFTER `status`,
+    ADD COLUMN `merchant_transfer_id` varchar(64) COMMENT '商户转账单编号' AFTER `merchant_order_id`;

+ 63 - 44
sql/mysql/ruoyi-vue-pro.sql

@@ -3,15 +3,15 @@
 
  Source Server         : 127.0.0.1 MySQL
  Source Server Type    : MySQL
- Source Server Version : 80034
- Source Host           : localhost:3306
+ Source Server Version : 80200 (8.2.0)
+ Source Host           : 127.0.0.1:3306
  Source Schema         : ruoyi-vue-pro
 
  Target Server Type    : MySQL
- Target Server Version : 80034
+ Target Server Version : 80200 (8.2.0)
  File Encoding         : 65001
 
- Date: 18/11/2023 17:48:18
+ Date: 30/11/2023 21:13:06
 */
 
 SET NAMES utf8mb4;
@@ -385,7 +385,7 @@ CREATE TABLE `infra_api_error_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1964 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 2018 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
 
 -- ----------------------------
 -- Records of infra_api_error_log
@@ -423,7 +423,7 @@ CREATE TABLE `infra_codegen_column`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1905 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义';
+) ENGINE = InnoDB AUTO_INCREMENT = 2000 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义';
 
 -- ----------------------------
 -- Records of infra_codegen_column
@@ -461,7 +461,7 @@ CREATE TABLE `infra_codegen_table`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 146 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义';
+) ENGINE = InnoDB AUTO_INCREMENT = 155 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义';
 
 -- ----------------------------
 -- Records of infra_codegen_table
@@ -535,8 +535,8 @@ CREATE TABLE `infra_demo01_contact`  (
   `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字',
   `sex` tinyint(1) NOT NULL COMMENT '性别',
   `birthday` datetime NOT NULL COMMENT '出生年',
-  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '简介',
-  `avatar` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像',
+  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '简介',
+  `avatar` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像',
   `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
@@ -568,7 +568,7 @@ CREATE TABLE `infra_demo02_category`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '示例分类表';
+) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '示例分类表';
 
 -- ----------------------------
 -- Records of infra_demo02_category
@@ -579,6 +579,7 @@ INSERT INTO `infra_demo02_category` (`id`, `name`, `parent_id`, `creator`, `crea
 INSERT INTO `infra_demo02_category` (`id`, `name`, `parent_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, '怪怪', 0, '1', '2023-11-16 20:24:32', '1', '2023-11-16 20:24:32', b'0', 1);
 INSERT INTO `infra_demo02_category` (`id`, `name`, `parent_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, '小番茄', 2, '1', '2023-11-16 20:24:39', '1', '2023-11-16 20:24:39', b'0', 1);
 INSERT INTO `infra_demo02_category` (`id`, `name`, `parent_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5, '大番茄', 2, '1', '2023-11-16 20:24:46', '1', '2023-11-16 20:24:46', b'0', 1);
+INSERT INTO `infra_demo02_category` (`id`, `name`, `parent_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6, '11', 3, '1', '2023-11-24 19:29:34', '1', '2023-11-24 19:29:34', b'0', 1);
 COMMIT;
 
 -- ----------------------------
@@ -651,7 +652,7 @@ CREATE TABLE `infra_demo03_student`  (
   `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字',
   `sex` tinyint NOT NULL COMMENT '性别',
   `birthday` datetime NOT NULL COMMENT '出生日期',
-  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '简介',
+  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '简介',
   `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
@@ -765,7 +766,7 @@ CREATE TABLE `infra_job`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 27 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '定时任务表';
+) ENGINE = InnoDB AUTO_INCREMENT = 28 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '定时任务表';
 
 -- ----------------------------
 -- Records of infra_job
@@ -806,7 +807,7 @@ CREATE TABLE `infra_job_log`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 232 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '定时任务日志表';
+) ENGINE = InnoDB AUTO_INCREMENT = 233 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '定时任务日志表';
 
 -- ----------------------------
 -- Records of infra_job_log
@@ -834,7 +835,7 @@ CREATE TABLE `member_address`  (
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `idx_userId`(`user_id` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 24 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '用户收件地址';
+) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '用户收件地址';
 
 -- ----------------------------
 -- Records of member_address
@@ -893,7 +894,7 @@ CREATE TABLE `member_experience_record`  (
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `idx_user_id`(`user_id` ASC) USING BTREE COMMENT '会员经验记录-用户编号',
   INDEX `idx_user_biz_type`(`user_id` ASC, `biz_type` ASC) USING BTREE COMMENT '会员经验记录-用户业务类型'
-) ENGINE = InnoDB AUTO_INCREMENT = 41 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员经验记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 42 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员经验记录';
 
 -- ----------------------------
 -- Records of member_experience_record
@@ -951,7 +952,7 @@ CREATE TABLE `member_group`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户分组';
+) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户分组';
 
 -- ----------------------------
 -- Records of member_group
@@ -979,7 +980,7 @@ CREATE TABLE `member_level`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员等级';
+) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员等级';
 
 -- ----------------------------
 -- Records of member_level
@@ -1009,7 +1010,7 @@ CREATE TABLE `member_level_record`  (
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `idx_user_id`(`user_id` ASC) USING BTREE COMMENT '会员等级记录-用户编号'
-) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员等级记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 21 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员等级记录';
 
 -- ----------------------------
 -- Records of member_level_record
@@ -1039,7 +1040,7 @@ CREATE TABLE `member_point_record`  (
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `index_userId`(`user_id` ASC) USING BTREE,
   INDEX `index_title`(`title` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 60 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户积分记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 61 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户积分记录';
 
 -- ----------------------------
 -- Records of member_point_record
@@ -1179,7 +1180,7 @@ CREATE TABLE `member_tag`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员标签';
+) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会员标签';
 
 -- ----------------------------
 -- Records of member_tag
@@ -1221,7 +1222,7 @@ CREATE TABLE `member_user`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 249 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '会员用户';
+) ENGINE = InnoDB AUTO_INCREMENT = 250 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '会员用户';
 
 -- ----------------------------
 -- Records of member_user
@@ -1289,7 +1290,7 @@ CREATE TABLE `system_dict_data`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1447 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
+) ENGINE = InnoDB AUTO_INCREMENT = 1455 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
 
 -- ----------------------------
 -- Records of system_dict_data
@@ -1604,6 +1605,14 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1444, 10, '主表(标准模式)', '10', 'infra_codegen_template_type', 0, 'default', '', '', '1', '2023-11-14 12:32:49', '1', '2023-11-14 12:32:49', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1445, 11, '主表(ERP 模式)', '11', 'infra_codegen_template_type', 0, 'default', '', '', '1', '2023-11-14 12:33:05', '1', '2023-11-14 12:33:05', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1446, 12, '主表(内嵌模式)', '12', 'infra_codegen_template_type', 0, '', '', '', '1', '2023-11-14 12:33:31', '1', '2023-11-14 12:33:31', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1447, 1, '负责人', '1', 'crm_permission_level', 0, 'default', '', '', '1', '2023-11-30 09:53:12', '1', '2023-11-30 09:53:12', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1448, 2, '只读', '2', 'crm_permission_level', 0, '', '', '', '1', '2023-11-30 09:53:29', '1', '2023-11-30 09:53:29', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1449, 3, '读写', '3', 'crm_permission_level', 0, '', '', '', '1', '2023-11-30 09:53:36', '1', '2023-11-30 09:53:36', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1450, 0, '未提交', '0', 'crm_audit_status', 0, '', '', '', '1', '2023-11-30 18:56:59', '1', '2023-11-30 18:56:59', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1451, 10, '审批中', '10', 'crm_audit_status', 0, '', '', '', '1', '2023-11-30 18:57:10', '1', '2023-11-30 18:57:10', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1452, 20, '审核通过', '20', 'crm_audit_status', 0, '', '', '', '1', '2023-11-30 18:57:24', '1', '2023-11-30 18:57:24', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1453, 30, '审核不通过', '30', 'crm_audit_status', 0, '', '', '', '1', '2023-11-30 18:57:32', '1', '2023-11-30 18:57:32', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1454, 40, '已取消', '40', 'crm_audit_status', 0, '', '', '', '1', '2023-11-30 18:57:42', '1', '2023-11-30 18:57:42', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -1624,7 +1633,7 @@ CREATE TABLE `system_dict_type`  (
   `deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间',
   PRIMARY KEY (`id`) USING BTREE,
   UNIQUE INDEX `dict_type`(`type` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 605 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
+) ENGINE = InnoDB AUTO_INCREMENT = 607 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
 
 -- ----------------------------
 -- Records of system_dict_type
@@ -1704,6 +1713,8 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (600, 'Banner 位置', 'promotion_banner_position', 0, '', '1', '2023-10-08 07:24:25', '1', '2023-11-04 13:04:02', b'0', '1970-01-01 00:00:00');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (601, '社交类型', 'system_social_type', 0, '', '1', '2023-11-04 13:03:54', '1', '2023-11-04 13:03:54', b'0', '1970-01-01 00:00:00');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (604, '产品状态', 'crm_product_status', 0, '', '1', '2023-10-30 21:47:59', '1', '2023-10-30 21:48:45', b'0', '1970-01-01 00:00:00');
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (605, 'CRM 数据权限的级别', 'crm_permission_level', 0, '', '1', '2023-11-30 09:51:59', '1', '2023-11-30 09:51:59', b'0', '1970-01-01 00:00:00');
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (606, 'CRM 审批状态', 'crm_audit_status', 0, '', '1', '2023-11-30 18:56:23', '1', '2023-11-30 18:56:23', b'0', '1970-01-01 00:00:00');
 COMMIT;
 
 -- ----------------------------
@@ -1723,7 +1734,7 @@ CREATE TABLE `system_error_code`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 5932 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '错误码表';
+) ENGINE = InnoDB AUTO_INCREMENT = 6039 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '错误码表';
 
 -- ----------------------------
 -- Records of system_error_code
@@ -1752,7 +1763,7 @@ CREATE TABLE `system_login_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 2647 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 2667 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
 
 -- ----------------------------
 -- Records of system_login_log
@@ -1817,7 +1828,7 @@ CREATE TABLE `system_mail_log`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 355 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '邮件日志表';
+) ENGINE = InnoDB AUTO_INCREMENT = 356 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '邮件日志表';
 
 -- ----------------------------
 -- Records of system_mail_log
@@ -1882,7 +1893,7 @@ CREATE TABLE `system_menu`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 2504 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表';
+) ENGINE = InnoDB AUTO_INCREMENT = 2526 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表';
 
 -- ----------------------------
 -- Records of system_menu
@@ -2292,7 +2303,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2302, '支付通知查询', 'pay:notify:query', 3, 1, 2301, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-07-20 04:41:32', '', '2023-07-20 04:41:32', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2303, '拼团活动', '', 2, 3, 2030, 'combination', 'fa:group', '', '', 0, b'1', b'1', b'1', '1', '2023-08-12 17:19:54', '1', '2023-08-12 17:20:05', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2304, '拼团商品', '', 2, 1, 2303, 'acitivity', 'ep:apple', 'mall/promotion/combination/activity/index', 'PromotionCombinationActivity', 0, b'1', b'1', b'1', '1', '2023-08-12 17:22:03', '1', '2023-08-12 17:22:29', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2305, '拼团活动查询', 'promotion:combination-activity:query	', 3, 1, 2304, '', '', '', '', 0, b'1', b'1', b'1', '1', '2023-08-12 17:54:32', '1', '2023-08-12 17:54:32', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2305, '拼团活动查询', 'promotion:combination-activity:query', 3, 1, 2304, '', '', '', '', 0, b'1', b'1', b'1', '1', '2023-08-12 17:54:32', '1', '2023-11-24 11:57:40', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2306, '拼团活动创建', 'promotion:combination-activity:create', 3, 2, 2304, '', '', '', '', 0, b'1', b'1', b'1', '1', '2023-08-12 17:54:49', '1', '2023-08-12 17:54:49', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2307, '拼团活动更新', 'promotion:combination-activity:update', 3, 3, 2304, '', '', '', '', 0, b'1', b'1', b'1', '1', '2023-08-12 17:55:04', '1', '2023-08-12 17:55:04', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2308, '拼团活动删除', 'promotion:combination-activity:delete', 3, 4, 2304, '', '', '', '', 0, b'1', b'1', b'1', '1', '2023-08-12 17:55:23', '1', '2023-08-12 17:55:23', b'0');
@@ -2457,6 +2468,16 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2494, '学生删除', 'infra:demo03-student:delete', 3, 4, 2490, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '', '2023-11-16 12:53:37', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2495, '学生导出', 'infra:demo03-student:export', 3, 5, 2490, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '', '2023-11-16 12:53:37', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2497, '主子表(ERP)', '', 2, 11, 1070, 'demo03-erp', 'ep:calendar', 'infra/demo/demo03/erp/index', 'Demo03StudentERP', 0, b'1', b'1', b'1', '', '2023-11-16 15:50:59', '1', '2023-11-17 13:19:56', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2516, '客户公海配置', '', 2, 0, 2524, 'customer-pool-config', 'ep:data-analysis', 'crm/config/customerPoolConfig/index', 'CrmCustomerPoolConfig', 0, b'1', b'1', b'1', '', '2023-11-18 13:33:31', '1', '2023-11-26 20:08:14', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2517, '客户公海配置保存', 'crm:customer-pool-config:update', 3, 1, 2516, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-18 13:33:31', '', '2023-11-18 13:33:31', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2518, '客户限制配置', '', 2, 0, 2524, 'customer-limit-config', 'ep:avatar', 'crm/config/customerLimitConfig/index', 'CrmCustomerLimitConfig', 0, b'1', b'1', b'1', '', '2023-11-18 13:33:53', '1', '2023-11-26 20:07:04', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2519, '客户限制配置查询', 'crm:customer-limit-config:query', 3, 1, 2518, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-18 13:33:53', '', '2023-11-18 13:33:53', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2520, '客户限制配置创建', 'crm:customer-limit-config:create', 3, 2, 2518, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-18 13:33:53', '', '2023-11-18 13:33:53', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2521, '客户限制配置更新', 'crm:customer-limit-config:update', 3, 3, 2518, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-18 13:33:53', '', '2023-11-18 13:33:53', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2522, '客户限制配置删除', 'crm:customer-limit-config:delete', 3, 4, 2518, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-18 13:33:53', '', '2023-11-18 13:33:53', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2523, '客户限制配置导出', 'crm:customer-limit-config:export', 3, 5, 2518, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-18 13:33:53', '', '2023-11-18 13:33:53', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2524, '系统配置', '', 1, 99, 2397, 'config', 'ep:connection', '', '', 0, b'1', b'1', b'1', '1', '2023-11-18 21:58:00', '1', '2023-11-18 21:58:00', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2525, 'WebSocket 测试', '', 2, 7, 2, 'websocket', 'ep:connection', 'infra/webSocket/index', 'InfraWebSocket', 0, b'1', b'1', b'1', '1', '2023-11-23 19:41:55', '1', '2023-11-24 19:22:30', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -2483,7 +2504,7 @@ CREATE TABLE `system_notice`  (
 -- ----------------------------
 BEGIN;
 INSERT INTO `system_notice` (`id`, `title`, `content`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, '芋道的公众', '<p>新版本内容133</p>', 1, 0, 'admin', '2021-01-05 17:03:48', '1', '2022-05-04 21:00:20', b'0', 1);
-INSERT INTO `system_notice` (`id`, `title`, `content`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '维护通知:2018-07-01 若依系统凌晨维护', '<p><img src=\"http://test.yudao.iocoder.cn/b7cb3cf49b4b3258bf7309a09dd2f4e5.jpg\" alt=\"\" data-href=\"\" style=\"\"/>1111</p>', 2, 1, 'admin', '2021-01-05 17:03:48', '1', '2023-11-11 12:51:11', b'0', 1);
+INSERT INTO `system_notice` (`id`, `title`, `content`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '维护通知:2018-07-01 系统凌晨维护', '<p><img src=\"http://test.yudao.iocoder.cn/b7cb3cf49b4b3258bf7309a09dd2f4e5.jpg\" alt=\"\" data-href=\"\" style=\"\"/>1111</p>', 2, 1, 'admin', '2021-01-05 17:03:48', '1', '2023-11-23 23:37:41', b'0', 1);
 INSERT INTO `system_notice` (`id`, `title`, `content`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, '我是测试标题', '<p>哈哈哈哈123</p>', 1, 0, '110', '2022-02-22 01:01:25', '110', '2022-02-22 01:01:46', b'0', 121);
 COMMIT;
 
@@ -2547,7 +2568,7 @@ CREATE TABLE `system_notify_template`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '站内信模板表';
+) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '站内信模板表';
 
 -- ----------------------------
 -- Records of system_notify_template
@@ -2577,7 +2598,7 @@ CREATE TABLE `system_oauth2_access_token`  (
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `idx_access_token`(`access_token` ASC) USING BTREE,
   INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 3467 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 3587 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_access_token
@@ -2699,7 +2720,7 @@ CREATE TABLE `system_oauth2_refresh_token`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1115 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 1132 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_refresh_token
@@ -2739,7 +2760,7 @@ CREATE TABLE `system_operate_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 9090 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 9175 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录';
 
 -- ----------------------------
 -- Records of system_operate_log
@@ -2765,7 +2786,7 @@ CREATE TABLE `system_post`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '岗位信息表';
+) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '岗位信息表';
 
 -- ----------------------------
 -- Records of system_post
@@ -3739,7 +3760,7 @@ CREATE TABLE `system_sms_code`  (
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `idx_mobile`(`mobile` ASC) USING BTREE COMMENT '手机号'
-) ENGINE = InnoDB AUTO_INCREMENT = 535 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
+) ENGINE = InnoDB AUTO_INCREMENT = 536 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码';
 
 -- ----------------------------
 -- Records of system_sms_code
@@ -3766,8 +3787,6 @@ CREATE TABLE `system_sms_log`  (
   `user_type` tinyint NULL DEFAULT NULL COMMENT '用户类型',
   `send_status` tinyint NOT NULL DEFAULT 0 COMMENT '发送状态',
   `send_time` datetime NULL DEFAULT NULL COMMENT '发送时间',
-  `send_code` int NULL DEFAULT NULL COMMENT '发送结果的编码',
-  `send_msg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '发送结果的提示',
   `api_send_code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '短信 API 发送结果的编码',
   `api_send_msg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '短信 API 发送失败的提示',
   `api_request_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '短信 API 发送返回的唯一请求 ID',
@@ -3782,7 +3801,7 @@ CREATE TABLE `system_sms_log`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 502 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 503 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
 
 -- ----------------------------
 -- Records of system_sms_log
@@ -3812,7 +3831,7 @@ CREATE TABLE `system_sms_template`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板';
+) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板';
 
 -- ----------------------------
 -- Records of system_sms_template
@@ -3882,7 +3901,7 @@ CREATE TABLE `system_social_user`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 24 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交用户表';
+) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交用户表';
 
 -- ----------------------------
 -- Records of system_social_user
@@ -3907,7 +3926,7 @@ CREATE TABLE `system_social_user_bind`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 80 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交绑定表';
+) ENGINE = InnoDB AUTO_INCREMENT = 81 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交绑定表';
 
 -- ----------------------------
 -- Records of system_social_user_bind
@@ -4077,13 +4096,13 @@ CREATE TABLE `system_users`  (
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE,
   UNIQUE INDEX `idx_username`(`username` ASC, `update_time` ASC, `tenant_id` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 127 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表';
+) ENGINE = InnoDB AUTO_INCREMENT = 126 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表';
 
 -- ----------------------------
 -- Records of system_users
 -- ----------------------------
 BEGIN;
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://127.0.0.1:48080/admin-api/infra/file/4/get/37e56010ecbee472cdd821ac4b608e151e62a74d9633f15d085aee026eedeb60.png', 0, '0:0:0:0:0:0:0:1', '2023-11-18 17:19:30', 'admin', '2021-01-05 17:03:47', NULL, '2023-11-18 17:19:30', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://127.0.0.1:48080/admin-api/infra/file/4/get/37e56010ecbee472cdd821ac4b608e151e62a74d9633f15d085aee026eedeb60.png', 0, '127.0.0.1', '2023-11-30 09:16:00', 'admin', '2021-01-05 17:03:47', NULL, '2023-11-30 09:16:00', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 1, '127.0.0.1', '2022-07-09 23:03:33', '', '2021-01-07 09:07:17', NULL, '2022-07-09 23:03:33', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$10$YMpimV4T6BtDhIaA8jSW.u8UTGBeGhc/qwXP4oxoMr4mOw9.qttt6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '127.0.0.1', '2022-07-08 01:26:27', '', '2021-01-13 23:50:35', NULL, '2022-07-08 01:26:27', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$10$GP8zvqHB//TekuzYZSBYAuBQJiNq1.fxQVDYJ.uBCOnWCtDVKE4H6', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2023-09-24 18:21:19', '', '2021-01-21 02:13:53', NULL, '2023-09-24 18:21:19', b'0', 1);
@@ -4098,7 +4117,7 @@ INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`,
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (115, 'aotemane', '$2a$10$/WCwGHu1eq0wOVDd/u8HweJ0gJCHyLS6T7ndCqI8UXZAQom1etk2e', '1', '11', 101, '[]', '', '', 1, '', 0, '', NULL, '1', '2022-04-30 02:55:43', '1', '2022-06-22 13:34:58', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (116, '15601691302', '$2a$10$L5C4S0U6adBWMvFv1Wwl4.DI/NwYS3WIfLj5Q.Naqr5II8CmqsDZ6', '小豆', NULL, NULL, NULL, '', '', 0, '', 0, '', NULL, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (117, 'admin123', '$2a$10$WI8Gg/lpZQIrOEZMHqka7OdFaD4Nx.B/qY8ZGTTUKrOJwaHFqibaC', '测试号', '1111', 100, '[2]', '', '15601691234', 1, '', 0, '', NULL, '1', '2022-07-09 17:40:26', '1', '2022-07-09 17:40:26', b'0', 1);
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (118, 'goudan', '$2a$10$Lrb71muL.s5/AFjQ2IHkzOFlAFwUToH.zQL7bnghvTDt/QptjGgF6', '狗蛋', NULL, 103, '[1]', '', '', 2, '', 0, '', NULL, '1', '2022-07-09 17:44:43', '1', '2022-12-31 17:29:13', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (118, 'goudan', '$2a$10$Lrb71muL.s5/AFjQ2IHkzOFlAFwUToH.zQL7bnghvTDt/QptjGgF6', '狗蛋', NULL, 103, '[1]', '', '', 2, '', 0, '', NULL, '1', '2022-07-09 17:44:43', '1', '2023-11-18 19:02:13', b'0', 1);
 COMMIT;
 
 SET FOREIGN_KEY_CHECKS = 1;

+ 15 - 13
yudao-dependencies/pom.xml

@@ -14,7 +14,7 @@
     <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 
     <properties>
-        <revision>1.8.3-snapshot</revision>
+        <revision>1.9.0-snapshot</revision>
         <flatten-maven-plugin.version>1.5.0</flatten-maven-plugin.version>
         <!-- 统一依赖管理 -->
         <spring.boot.version>2.7.17</spring.boot.version>
@@ -70,10 +70,10 @@
         <aliyun-java-sdk-core.version>4.6.4</aliyun-java-sdk-core.version>
         <aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
         <tencentcloud-sdk-java.version>3.1.880</tencentcloud-sdk-java.version>
-        <justauth.version>1.0.7</justauth.version>
+        <justauth.version>1.0.8</justauth.version>
         <jimureport.version>1.6.1</jimureport.version>
         <xercesImpl.version>2.12.2</xercesImpl.version>
-        <weixin-java.version>4.5.0</weixin-java.version>
+        <weixin-java.version>4.5.7.B</weixin-java.version>
     </properties>
 
     <dependencyManagement>
@@ -113,11 +113,6 @@
                 <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>
             <dependency>
                 <groupId>cn.iocoder.boot</groupId>
                 <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
@@ -175,6 +170,12 @@
                 <version>${revision}</version>
             </dependency>
 
+            <dependency>
+                <groupId>cn.iocoder.boot</groupId>
+                <artifactId>yudao-spring-boot-starter-websocket</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
             <dependency>
                 <groupId>com.github.xiaoymin</groupId>
                 <artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
@@ -605,6 +606,12 @@
                 <groupId>com.xingyuv</groupId>
                 <artifactId>spring-boot-starter-justauth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
                 <version>${justauth.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>cn.hutool</groupId>
+                        <artifactId>hutool-core</artifactId>
+                    </exclusion>
+                </exclusions>
             </dependency>
 
             <dependency>
@@ -612,11 +619,6 @@
                 <artifactId>weixin-java-pay</artifactId>
                 <version>${weixin-java.version}</version>
             </dependency>
-            <dependency>
-                <groupId>com.github.binarywang</groupId>
-                <artifactId>weixin-java-mp</artifactId>
-                <version>${weixin-java.version}</version>
-            </dependency>
             <dependency>
                 <groupId>com.github.binarywang</groupId>
                 <artifactId>wx-java-mp-spring-boot-starter</artifactId>

+ 0 - 2
yudao-framework/pom.xml

@@ -31,8 +31,6 @@
         <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-biz-social</module>
         <module>yudao-spring-boot-starter-biz-tenant</module>
         <module>yudao-spring-boot-starter-biz-data-permission</module>
         <module>yudao-spring-boot-starter-biz-error-code</module>

+ 6 - 2
yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java

@@ -49,8 +49,12 @@ public class ErrorCodeAutoGeneratorImpl implements ErrorCodeAutoGenerator {
         log.info("[execute][解析到错误码数量为 ({}) 个]", autoGenerateDTOs.size());
 
         // 第二步,写入到 system 服务
-        errorCodeApi.autoGenerateErrorCodeList(autoGenerateDTOs);
-        log.info("[execute][写入到 system 组件完成]");
+        try {
+            errorCodeApi.autoGenerateErrorCodeList(autoGenerateDTOs);
+            log.info("[execute][写入到 system 组件完成]");
+        } catch (Exception ex) {
+            log.error("[execute][写入到 system 组件失败({})]", ExceptionUtil.getRootCauseMessage(ex));
+        }
     }
 
     /**

+ 10 - 0
yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/loader/ErrorCodeLoader.java

@@ -21,4 +21,14 @@ public interface ErrorCodeLoader {
         ServiceExceptionUtil.put(code, msg);
     }
 
+    /**
+     * 刷新错误码
+     */
+    void refreshErrorCodes();
+
+    /**
+     * 加载错误码
+     */
+    void loadErrorCodes();
+
 }

+ 24 - 15
yudao-framework/yudao-spring-boot-starter-biz-error-code/src/main/java/cn/iocoder/yudao/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java

@@ -1,6 +1,7 @@
 package cn.iocoder.yudao.framework.errorcode.core.loader;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.exceptions.ExceptionUtil;
 import cn.iocoder.yudao.framework.common.util.date.DateUtils;
 import cn.iocoder.yudao.module.system.api.errorcode.ErrorCodeApi;
 import cn.iocoder.yudao.module.system.api.errorcode.dto.ErrorCodeRespDTO;
@@ -8,6 +9,7 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.boot.context.event.ApplicationReadyEvent;
 import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
 import org.springframework.scheduling.annotation.Scheduled;
 
 import java.time.LocalDateTime;
@@ -43,31 +45,38 @@ public class ErrorCodeLoaderImpl implements ErrorCodeLoader {
      */
     private LocalDateTime maxUpdateTime;
 
+    @Override
     @EventListener(ApplicationReadyEvent.class)
+    @Async // 异步,保证项目的启动过程,毕竟非关键流程
     public void loadErrorCodes() {
-        this.loadErrorCodes0();
+        loadErrorCodes0();
     }
 
+    @Override
     @Scheduled(fixedDelay = REFRESH_ERROR_CODE_PERIOD, initialDelay = REFRESH_ERROR_CODE_PERIOD)
     public void refreshErrorCodes() {
-        this.loadErrorCodes0();
+        loadErrorCodes0();
     }
 
     private void loadErrorCodes0() {
-        // 加载错误码
-        List<ErrorCodeRespDTO> errorCodeRespDTOs = errorCodeApi.getErrorCodeList(applicationName, maxUpdateTime);
-        if (CollUtil.isEmpty(errorCodeRespDTOs)) {
-            return;
-        }
-        log.info("[loadErrorCodes0][加载到 ({}) 个错误码]", errorCodeRespDTOs.size());
+        try {
+            // 加载错误码
+            List<ErrorCodeRespDTO> errorCodeRespDTOs = errorCodeApi.getErrorCodeList(applicationName, maxUpdateTime);
+            if (CollUtil.isEmpty(errorCodeRespDTOs)) {
+                return;
+            }
+            log.info("[loadErrorCodes0][加载到 ({}) 个错误码]", errorCodeRespDTOs.size());
 
-        // 刷新错误码的缓存
-        errorCodeRespDTOs.forEach(errorCodeRespDTO -> {
-            // 写入到错误码的缓存
-            putErrorCode(errorCodeRespDTO.getCode(), errorCodeRespDTO.getMessage());
-            // 记录下更新时间,方便增量更新
-            maxUpdateTime = DateUtils.max(maxUpdateTime, errorCodeRespDTO.getUpdateTime());
-        });
+            // 刷新错误码的缓存
+            errorCodeRespDTOs.forEach(errorCodeRespDTO -> {
+                // 写入到错误码的缓存
+                putErrorCode(errorCodeRespDTO.getCode(), errorCodeRespDTO.getMessage());
+                // 记录下更新时间,方便增量更新
+                maxUpdateTime = DateUtils.max(maxUpdateTime, errorCodeRespDTO.getUpdateTime());
+            });
+        } catch (Exception ex) {
+            log.error("[loadErrorCodes0][加载错误码失败({})]", ExceptionUtil.getRootCauseMessage(ex));
+        }
     }
 
 }

+ 1 - 1
yudao-framework/yudao-spring-boot-starter-biz-operatelog/src/main/java/cn/iocoder/yudao/framework/operatelog/core/service/OperateLogFrameworkServiceImpl.java

@@ -21,7 +21,7 @@ public class OperateLogFrameworkServiceImpl implements OperateLogFrameworkServic
     @Override
     @Async
     public void createOperateLog(OperateLog operateLog) {
-        OperateLogCreateReqDTO reqDTO = BeanUtil.copyProperties(operateLog, OperateLogCreateReqDTO.class);
+        OperateLogCreateReqDTO reqDTO = BeanUtil.toBean(operateLog, OperateLogCreateReqDTO.class);
         operateLogApi.createOperateLog(reqDTO);
     }
 

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

@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDTO;
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
 
 import java.util.Map;
 
@@ -86,4 +87,12 @@ public interface PayClient {
      */
     PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO);
 
+    /**
+     * 获得转账订单信息
+     *
+     * @param outTradeNo 外部订单号
+     * @param type 转账类型
+     * @return 转账信息
+     */
+    PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type);
 }

+ 15 - 2
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/dto/transfer/PayTransferRespDTO.java

@@ -53,11 +53,24 @@ public class PayTransferRespDTO {
     /**
      * 创建【WAITING】状态的转账返回
      */
-    public static PayTransferRespDTO waitingOf(String channelOrderNo,
+    public static PayTransferRespDTO waitingOf(String channelTransferNo,
                                              String outTransferNo, Object rawData) {
         PayTransferRespDTO respDTO = new PayTransferRespDTO();
         respDTO.status = PayTransferStatusRespEnum.WAITING.getStatus();
-        respDTO.channelTransferNo = channelOrderNo;
+        respDTO.channelTransferNo = channelTransferNo;
+        respDTO.outTransferNo = outTransferNo;
+        respDTO.rawData = rawData;
+        return respDTO;
+    }
+
+    /**
+     * 创建【IN_PROGRESS】状态的转账返回
+     */
+    public static PayTransferRespDTO dealingOf(String channelTransferNo,
+                                               String outTransferNo, Object rawData) {
+        PayTransferRespDTO respDTO = new PayTransferRespDTO();
+        respDTO.status = PayTransferStatusRespEnum.IN_PROGRESS.getStatus();
+        respDTO.channelTransferNo = channelTransferNo;
         respDTO.outTransferNo = outTransferNo;
         respDTO.rawData = rawData;
         return respDTO;

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

@@ -188,11 +188,11 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
 
     @Override
     public final PayTransferRespDTO unifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
+        validatePayTransferReqDTO(reqDTO);
         PayTransferRespDTO resp;
-        try{
-            validatePayTransferReqDTO(reqDTO);
+        try {
             resp = doUnifiedTransfer(reqDTO);
-        }catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
+        } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
             throw ex;
         } catch (Throwable ex) {
             // 系统异常,则包装成 PayException 异常抛出
@@ -219,9 +219,25 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
         }
     }
 
+    @Override
+    public final PayTransferRespDTO getTransfer(String outTradeNo, PayTransferTypeEnum type) {
+        try {
+            return doGetTransfer(outTradeNo, type);
+        } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可
+            throw ex;
+        } catch (Throwable ex) {
+            log.error("[getTransfer][客户端({}) outTradeNo({}) type({}) 查询转账单异常]",
+                    getId(), outTradeNo, type, ex);
+            throw buildPayException(ex);
+        }
+    }
+
     protected abstract PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO)
             throws Throwable;
 
+    protected abstract PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type)
+            throws Throwable;
+
     // ========== 各种工具方法 ==========
 
     private PayException buildPayException(Throwable ex) {

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

@@ -23,14 +23,8 @@ import com.alipay.api.AlipayResponse;
 import com.alipay.api.DefaultAlipayClient;
 import com.alipay.api.domain.*;
 import com.alipay.api.internal.util.AlipaySignature;
-import com.alipay.api.request.AlipayFundTransUniTransferRequest;
-import com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest;
-import com.alipay.api.request.AlipayTradeQueryRequest;
-import com.alipay.api.request.AlipayTradeRefundRequest;
-import com.alipay.api.response.AlipayFundTransUniTransferResponse;
-import com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse;
-import com.alipay.api.response.AlipayTradeQueryResponse;
-import com.alipay.api.response.AlipayTradeRefundResponse;
+import com.alipay.api.request.*;
+import com.alipay.api.response.*;
 import lombok.Getter;
 import lombok.SneakyThrows;
 import lombok.extern.slf4j.Slf4j;
@@ -126,7 +120,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
         }
         // 2.2 解析订单的状态
         Integer status = parseStatus(response.getTradeStatus());
-        Assert.notNull(status,  () -> {
+        Assert.notNull(status, () -> {
             throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody()));
         });
         return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()),
@@ -228,7 +222,7 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
     protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) throws AlipayApiException {
         // 1.1 校验公钥类型 必须使用公钥证书模式
         if (!Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
-            throw exception0(ERROR_CONFIGURATION.getCode(),"支付宝单笔转账必须使用公钥证书模式");
+            throw exception0(ERROR_CONFIGURATION.getCode(), "支付宝单笔转账必须使用公钥证书模式");
         }
         // 1.2 构建 AlipayFundTransUniTransferModel
         AlipayFundTransUniTransferModel model = new AlipayFundTransUniTransferModel();
@@ -238,44 +232,96 @@ public abstract class AbstractAlipayPayClient extends AbstractPayClient<AlipayPa
         model.setOutBizNo(reqDTO.getOutTransferNo());
         model.setProductCode("TRANS_ACCOUNT_NO_PWD");    // 销售产品码。单笔无密转账固定为 TRANS_ACCOUNT_NO_PWD
         model.setBizScene("DIRECT_TRANSFER");           // 业务场景 单笔无密转账固定为 DIRECT_TRANSFER
-        model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
+        if (reqDTO.getChannelExtras() != null) {
+            model.setBusinessParams(JsonUtils.toJsonString(reqDTO.getChannelExtras()));
+        }
+        // ② 个性化的参数
+        Participant payeeInfo = new Participant();
         PayTransferTypeEnum transferType = PayTransferTypeEnum.typeOf(reqDTO.getType());
         switch (transferType) {
             // TODO @jason:是不是不用传递 transferType 参数哈?因为应该已经明确是支付宝啦?
             // @芋艿。 是不是还要考虑转账到银行卡。所以传 transferType 但是转账到银行卡不知道要如何测试??
             case ALIPAY_BALANCE: {
-                // ② 个性化的参数
-                Participant payeeInfo = new Participant();
                 payeeInfo.setIdentityType("ALIPAY_LOGON_ID");
                 payeeInfo.setIdentity(reqDTO.getAlipayLogonId()); // 支付宝登录号
                 payeeInfo.setName(reqDTO.getUserName()); // 支付宝账号姓名
                 model.setPayeeInfo(payeeInfo);
-                // 1.3 构建 AlipayFundTransUniTransferRequest
-                AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
-                request.setBizModel(model);
-                // 执行请求
-                AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
-                // 处理结果
-                if (!response.isSuccess()) {
-                    // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询
-                    if (ObjectUtils.equalsAny(response.getSubCode(), "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
-                        return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
-                    }
-                    return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
-                            reqDTO.getOutTransferNo(), response);
-                }
-                return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
-                        response.getOutBizNo(), response);
+                break;
             }
             case BANK_CARD: {
-                Participant payeeInfo = new Participant();
                 payeeInfo.setIdentityType("BANKCARD_ACCOUNT");
                 // TODO 待实现
                 throw exception(NOT_IMPLEMENTED);
             }
             default: {
-                throw exception0(BAD_REQUEST.getCode(),"不正确的转账类型: {}",transferType);
+                throw exception0(BAD_REQUEST.getCode(), "不正确的转账类型: {}", transferType);
+            }
+        }
+        // 1.3 构建 AlipayFundTransUniTransferRequest
+        AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest();
+        request.setBizModel(model);
+        // 执行请求
+        AlipayFundTransUniTransferResponse response = client.certificateExecute(request);
+        // 处理结果
+        if (!response.isSuccess()) {
+            // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询,或相同 outBizNo 重新发起转账
+            // 发现 outBizNo 相同 两次请求参数相同. 会返回 "PAYMENT_INFO_INCONSISTENCY", 不知道哪里的问题. 暂时返回 WAIT. 后续job 会轮询
+            if (ObjectUtils.equalsAny(response.getSubCode(),"PAYMENT_INFO_INCONSISTENCY", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
+                return PayTransferRespDTO.waitingOf(null, reqDTO.getOutTransferNo(), response);
+            }
+            return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                    reqDTO.getOutTransferNo(), response);
+        } else {
+            if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
+                return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                        reqDTO.getOutTransferNo(), response);
+            }
+            if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING"  处理中
+                return PayTransferRespDTO.dealingOf(response.getOrderId(), reqDTO.getOutTransferNo(), response);
+            }
+            return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getTransDate()),
+                    response.getOutBizNo(), response);
+        }
+
+    }
+
+    @Override
+    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) throws Throwable {
+        // 1.1 构建 AlipayFundTransCommonQueryModel
+        AlipayFundTransCommonQueryModel model = new AlipayFundTransCommonQueryModel();
+        model.setProductCode(type == PayTransferTypeEnum.BANK_CARD ? "TRANS_BANKCARD_NO_PWD" : "TRANS_ACCOUNT_NO_PWD");
+        model.setBizScene("DIRECT_TRANSFER"); //业务场景
+        model.setOutBizNo(outTradeNo);
+        // 1.2 构建 AlipayFundTransCommonQueryRequest
+        AlipayFundTransCommonQueryRequest request = new AlipayFundTransCommonQueryRequest();
+        request.setBizModel(model);
+
+        // 2.1 执行请求
+        AlipayFundTransCommonQueryResponse response;
+        if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) { // 证书模式
+            response = client.certificateExecute(request);
+        } else {
+            response = client.execute(request);
+        }
+        // 2.2 处理返回结果
+        if (response.isSuccess()) {
+            if (ObjectUtils.equalsAny(response.getStatus(), "REFUND", "FAIL")) { // 转账到银行卡会出现 "REFUND" "FAIL"
+                return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                        outTradeNo, response);
+            }
+            if (Objects.equals(response.getStatus(), "DEALING")) { // 转账到银行卡会出现 "DEALING" 处理中
+                return PayTransferRespDTO.dealingOf(response.getOrderId(), outTradeNo, response);
             }
+            return PayTransferRespDTO.successOf(response.getOrderId(), parseTime(response.getPayDate()),
+                    response.getOutBizNo(), response);
+        } else {
+            // 当出现 SYSTEM_ERROR, 转账可能成功也可能失败。 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
+            // 当出现 ORDER_NOT_EXIST 可能是转账还在处理中,也可能是转账处理失败. 返回 WAIT 状态. 后续 job 会轮询, 或相同 outBizNo 重新发起转账
+            if (ObjectUtils.equalsAny(response.getSubCode(), "ORDER_NOT_EXIST", "SYSTEM_ERROR", "ACQ.SYSTEM_ERROR")) {
+                return PayTransferRespDTO.waitingOf(null, outTradeNo, response);
+            }
+            return PayTransferRespDTO.closedOf(response.getSubCode(), response.getSubMsg(),
+                    outTradeNo, response);
         }
     }
 

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

@@ -9,6 +9,7 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifie
 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.client.impl.NonePayClientConfig;
 import cn.iocoder.yudao.framework.pay.core.enums.channel.PayChannelEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
 
 import java.time.LocalDateTime;
 import java.util.Map;
@@ -71,4 +72,9 @@ public class MockPayClient extends AbstractPayClient<NonePayClientConfig> {
         throw new UnsupportedOperationException("待实现");
     }
 
+    @Override
+    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
+        throw new UnsupportedOperationException("待实现");
+    }
+
 }

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

@@ -16,8 +16,9 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferRespDT
 import cn.iocoder.yudao.framework.pay.core.client.dto.transfer.PayTransferUnifiedReqDTO;
 import cn.iocoder.yudao.framework.pay.core.client.impl.AbstractPayClient;
 import cn.iocoder.yudao.framework.pay.core.enums.order.PayOrderStatusRespEnum;
+import cn.iocoder.yudao.framework.pay.core.enums.transfer.PayTransferTypeEnum;
+import com.github.binarywang.wxpay.bean.notify.WxPayNotifyV3Result;
 import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
-import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result;
 import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult;
 import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result;
 import com.github.binarywang.wxpay.bean.request.*;
@@ -175,8 +176,8 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
 
     private PayOrderRespDTO doParseOrderNotifyV3(String body) throws WxPayException {
         // 1. 解析回调
-        WxPayOrderNotifyV3Result response = client.parseOrderNotifyV3Result(body, null);
-        WxPayOrderNotifyV3Result.DecryptNotifyResult result = response.getResult();
+        WxPayNotifyV3Result response = client.parseOrderNotifyV3Result(body, null);
+        WxPayNotifyV3Result.DecryptNotifyResult result = response.getResult();
         // 2. 构建结果
         Integer status = parseStatus(result.getTradeState());
         String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null;
@@ -431,6 +432,12 @@ public abstract class AbstractWxPayClient extends AbstractPayClient<WxPayClientC
     protected PayTransferRespDTO doUnifiedTransfer(PayTransferUnifiedReqDTO reqDTO) {
        throw new UnsupportedOperationException("待实现");
     }
+
+    @Override
+    protected PayTransferRespDTO doGetTransfer(String outTradeNo, PayTransferTypeEnum type) {
+        throw new UnsupportedOperationException("待实现");
+    }
+
     // ========== 各种工具方法 ==========
 
     static String formatDateV2(LocalDateTime time) {

+ 4 - 0
yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/enums/transfer/PayTransferStatusRespEnum.java

@@ -38,4 +38,8 @@ public enum PayTransferStatusRespEnum {
     public static boolean isClosed(Integer status) {
         return Objects.equals(status, CLOSED.getStatus());
     }
+
+    public static boolean isInProgress(Integer status) {
+        return Objects.equals(status, IN_PROGRESS.getStatus());
+    }
 }

+ 3 - 3
yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsClient.java

@@ -31,8 +31,8 @@ public interface SmsClient {
      * @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序
      * @return 短信发送结果
      */
-    SmsCommonResult<SmsSendRespDTO> sendSms(Long logId, String mobile, String apiTemplateId,
-                                            List<KeyValue<String, Object>> templateParams);
+    SmsSendRespDTO sendSms(Long logId, String mobile, String apiTemplateId,
+                           List<KeyValue<String, Object>> templateParams) throws Throwable;
 
     /**
      * 解析接收短信的接收结果
@@ -49,6 +49,6 @@ public interface SmsClient {
      * @param apiTemplateId 短信 API 的模板编号
      * @return 短信模板
      */
-    SmsCommonResult<SmsTemplateRespDTO> getSmsTemplate(String apiTemplateId);
+    SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable;
 
 }

+ 0 - 17
yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCodeMapping.java

@@ -1,17 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client;
-
-import cn.iocoder.yudao.framework.common.exception.ErrorCode;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
-
-import java.util.function.Function;
-
-/**
- * 将 API 的错误码,转换为通用的错误码
- *
- * @see SmsCommonResult
- * @see SmsFrameworkErrorCodeConstants
- *
- * @author 芋道源码
- */
-public interface SmsCodeMapping extends Function<String, ErrorCode> {
-}

+ 0 - 68
yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/SmsCommonResult.java

@@ -1,68 +0,0 @@
-package cn.iocoder.yudao.framework.sms.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.sms.core.enums.SmsFrameworkErrorCodeConstants;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.ToString;
-
-/**
- * 短信的 CommonResult 拓展类
- *
- * 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段
- *
- * 另外,一些短信平台(例如说阿里云、腾讯云)会返回一个请求编号,用于排查请求失败的问题,我们设置到 {@link #apiRequestId} 字段
- *
- * @author 芋道源码
- */
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class SmsCommonResult<T> extends CommonResult<T> {
-
-    /**
-     * API 返回错误码
-     *
-     * 由于第三方的错误码可能是字符串,所以使用 String 类型
-     */
-    private String apiCode;
-    /**
-     * API 返回提示
-     */
-    private String apiMsg;
-
-    /**
-     * API 请求编号
-     */
-    private String apiRequestId;
-
-    private SmsCommonResult() {
-    }
-
-    public static <T> SmsCommonResult<T> build(String apiCode, String apiMsg, String apiRequestId,
-                                               T data, SmsCodeMapping codeMapping) {
-        Assert.notNull(codeMapping, "参数 codeMapping 不能为空");
-        SmsCommonResult<T> result = new SmsCommonResult<T>().setApiCode(apiCode).setApiMsg(apiMsg).setApiRequestId(apiRequestId);
-        result.setData(data);
-        // 翻译错误码
-        if (codeMapping != null) {
-            ErrorCode errorCode = codeMapping.apply(apiCode);
-            if (errorCode == null) {
-                errorCode = SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
-            }
-            result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg());
-        }
-        return result;
-    }
-
-    public static <T> SmsCommonResult<T> error(Throwable ex) {
-        SmsCommonResult<T> result = new SmsCommonResult<>();
-        result.setCode(SmsFrameworkErrorCodeConstants.EXCEPTION.getCode());
-        result.setMsg(ExceptionUtil.getRootCauseMessage(ex));
-        return result;
-    }
-
-}

+ 25 - 0
yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/dto/SmsSendRespDTO.java

@@ -10,9 +10,34 @@ import lombok.Data;
 @Data
 public class SmsSendRespDTO {
 
+    /**
+     * 是否成功
+     */
+    private Boolean success;
+
+    /**
+     * API 请求编号
+     */
+    private String apiRequestId;
+
+    // ==================== 成功时字段 ====================
+
     /**
      * 短信 API 发送返回的序号
      */
     private String serialNo;
 
+    // ==================== 失败时字段 ====================
+
+    /**
+     * API 返回错误码
+     *
+     * 由于第三方的错误码可能是字符串,所以使用 String 类型
+     */
+    private String apiCode;
+    /**
+     * API 返回提示
+     */
+    private String apiMsg;
+
 }

+ 2 - 76
yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/AbstractSmsClient.java

@@ -1,17 +1,9 @@
 package cn.iocoder.yudao.framework.sms.core.client.impl;
 
-import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.framework.sms.core.client.SmsClient;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCodeMapping;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
-import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
-import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
-import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
 import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
 import lombok.extern.slf4j.Slf4j;
 
-import java.util.List;
-
 /**
  * 短信客户端的抽象类,提供模板方法,减少子类的冗余代码
  *
@@ -25,14 +17,9 @@ public abstract class AbstractSmsClient implements SmsClient {
      * 短信渠道配置
      */
     protected volatile SmsChannelProperties properties;
-    /**
-     * 错误码枚举类
-     */
-    protected final SmsCodeMapping codeMapping;
 
-    public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) {
-        this.properties = prepareProperties(properties);
-        this.codeMapping = codeMapping;
+    public AbstractSmsClient(SmsChannelProperties properties) {
+        this.properties = properties;
     }
 
     /**
@@ -54,74 +41,13 @@ public abstract class AbstractSmsClient implements SmsClient {
             return;
         }
         log.info("[refresh][配置({})发生变化,重新初始化]", properties);
-        this.properties = prepareProperties(properties);
         // 初始化
         this.init();
     }
 
-    /**
-     * 在赋值给{@link this#properties}前,子类可根据需要预处理短信渠道配置
-     *
-     * @param properties 数据库中存储的短信渠道配置
-     * @return 满足子类实现的短信渠道配置
-     */
-    protected SmsChannelProperties prepareProperties(SmsChannelProperties properties) {
-        return properties;
-    }
-
     @Override
     public Long getId() {
         return properties.getId();
     }
 
-    @Override
-    public final SmsCommonResult<SmsSendRespDTO> sendSms(Long logId, String mobile,
-                                                         String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
-        // 执行短信发送
-        SmsCommonResult<SmsSendRespDTO> result;
-        try {
-            result = doSendSms(logId, mobile, apiTemplateId, templateParams);
-        } catch (Throwable ex) {
-            // 打印异常日志
-            log.error("[sendSms][发送短信异常,sendLogId({}) mobile({}) apiTemplateId({}) templateParams({})]",
-                    logId, mobile, apiTemplateId, templateParams, ex);
-            // 封装返回
-            return SmsCommonResult.error(ex);
-        }
-        return result;
-    }
-
-    protected abstract SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
-                                                                 String apiTemplateId, List<KeyValue<String, Object>> templateParams)
-            throws Throwable;
-
-    @Override
-    public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) throws Throwable {
-        try {
-            return doParseSmsReceiveStatus(text);
-        } catch (Throwable ex) {
-            log.error("[parseSmsReceiveStatus][text({}) 解析发生异常]", text, ex);
-            throw ex;
-        }
-    }
-
-    protected abstract List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable;
-
-    @Override
-    public SmsCommonResult<SmsTemplateRespDTO> getSmsTemplate(String apiTemplateId) {
-        // 执行短信发送
-        SmsCommonResult<SmsTemplateRespDTO> result;
-        try {
-            result = doGetSmsTemplate(apiTemplateId);
-        } catch (Throwable ex) {
-            // 打印异常日志
-            log.error("[getSmsTemplate][获得短信模板({}) 发生异常]", apiTemplateId, ex);
-            // 封装返回
-            return SmsCommonResult.error(ex);
-        }
-        return result;
-    }
-
-    protected abstract SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) throws Throwable;
-
 }

+ 31 - 60
yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java

@@ -1,25 +1,21 @@
 package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun;
 
 import cn.hutool.core.lang.Assert;
-import cn.hutool.core.util.ReflectUtil;
-import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
 import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
 import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
 import cn.iocoder.yudao.framework.sms.core.client.impl.AbstractSmsClient;
 import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
 import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
-import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
-import com.aliyuncs.AcsRequest;
-import com.aliyuncs.AcsResponse;
 import com.aliyuncs.DefaultAcsClient;
 import com.aliyuncs.IAcsClient;
 import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest;
+import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse;
 import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
-import com.aliyuncs.exceptions.ClientException;
+import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
 import com.aliyuncs.profile.DefaultProfile;
 import com.aliyuncs.profile.IClientProfile;
 import com.fasterxml.jackson.annotation.JsonFormat;
@@ -31,9 +27,8 @@ import lombok.extern.slf4j.Slf4j;
 import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Objects;
-import java.util.function.Function;
-import java.util.stream.Collectors;
 
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
 
@@ -46,6 +41,11 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
 @Slf4j
 public class AliyunSmsClient extends AbstractSmsClient {
 
+    /**
+     * 调用成功 code
+     */
+    public static final String API_CODE_SUCCESS = "OK";
+
     /**
      * REGION, 使用杭州
      */
@@ -57,7 +57,7 @@ public class AliyunSmsClient extends AbstractSmsClient {
     private volatile IAcsClient client;
 
     public AliyunSmsClient(SmsChannelProperties properties) {
-        super(properties, new AliyunSmsCodeMapping());
+        super(properties);
         Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
         Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
     }
@@ -69,9 +69,9 @@ public class AliyunSmsClient extends AbstractSmsClient {
     }
 
     @Override
-    protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
-                                                        String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
-        // 构建参数
+    public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId,
+                                  List<KeyValue<String, Object>> templateParams) throws Throwable {
+        // 构建请求
         SendSmsRequest request = new SendSmsRequest();
         request.setPhoneNumbers(mobile);
         request.setSignName(properties.getSignature());
@@ -79,34 +79,32 @@ public class AliyunSmsClient extends AbstractSmsClient {
         request.setTemplateParam(JsonUtils.toJsonString(MapUtils.convertMap(templateParams)));
         request.setOutId(String.valueOf(sendLogId));
         // 执行请求
-        return invoke(request, response -> new SmsSendRespDTO().setSerialNo(response.getBizId()));
+        SendSmsResponse response = client.getAcsResponse(request);
+        return new SmsSendRespDTO().setSuccess(Objects.equals(response.getCode(), API_CODE_SUCCESS)).setSerialNo(response.getBizId())
+                .setApiRequestId(response.getRequestId()).setApiCode(response.getCode()).setApiMsg(response.getMessage());
     }
 
     @Override
-    protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
+    public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
         List<SmsReceiveStatus> statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class);
-        return statuses.stream().map(status -> {
-            SmsReceiveRespDTO resp = new SmsReceiveRespDTO();
-            resp.setSuccess(status.getSuccess());
-            resp.setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg());
-            resp.setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime());
-            resp.setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId()));
-            return resp;
-        }).collect(Collectors.toList());
+        return convertList(statuses, status -> new SmsReceiveRespDTO().setSuccess(status.getSuccess())
+                .setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg())
+                .setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime())
+                .setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId())));
     }
 
     @Override
-    protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) {
-        // 构建参数
+    public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
+        // 构建请求
         QuerySmsTemplateRequest request = new QuerySmsTemplateRequest();
         request.setTemplateCode(apiTemplateId);
         // 执行请求
-        return invoke(request, response -> {
-            SmsTemplateRespDTO data = new SmsTemplateRespDTO();
-            data.setId(response.getTemplateCode()).setContent(response.getTemplateContent());
-            data.setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason());
-            return data;
-        });
+        QuerySmsTemplateResponse response = client.getAcsResponse(request);
+        if (response.getTemplateStatus() == null) {
+            return null;
+        }
+        return new SmsTemplateRespDTO().setId(response.getTemplateCode()).setContent(response.getTemplateContent())
+                .setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason());
     }
 
     @VisibleForTesting
@@ -119,37 +117,10 @@ public class AliyunSmsClient extends AbstractSmsClient {
         }
     }
 
-    @VisibleForTesting
-    <T extends AcsResponse, R> SmsCommonResult<R> invoke(AcsRequest<T> request, Function<T, R> responseConsumer) {
-        try {
-            // 执行发送. 由于阿里云 sms 短信没有统一的 Response,但是有统一的 code、message、requestId 属性,所以只好反射
-            T sendResult = client.getAcsResponse(request);
-            String code = (String) ReflectUtil.getFieldValue(sendResult, "code");
-            String message = (String) ReflectUtil.getFieldValue(sendResult, "message");
-            String requestId = (String) ReflectUtil.getFieldValue(sendResult, "requestId");
-            // 解析结果
-            R data = null;
-            if (Objects.equals(code, "OK")) { // 请求成功的情况下
-                data = responseConsumer.apply(sendResult);
-            }
-            // 拼接结果
-            return SmsCommonResult.build(code, message, requestId, data, codeMapping);
-        } catch (ClientException ex) {
-            return SmsCommonResult.build(ex.getErrCode(), formatResultMsg(ex), ex.getRequestId(), null, codeMapping);
-        }
-    }
-
-    private static String formatResultMsg(ClientException ex) {
-        if (StrUtil.isEmpty(ex.getErrorDescription())) {
-            return ex.getErrMsg();
-        }
-        return ex.getErrMsg() + " => " + ex.getErrorDescription();
-    }
-
     /**
      * 短信接收状态
      *
-     * 参见 https://help.aliyun.com/document_detail/101867.html 文档
+     * 参见 <a href="https://help.aliyun.com/document_detail/101867.html">文档</a>
      *
      * @author 芋道源码
      */

+ 0 - 42
yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java

@@ -1,42 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun;
-
-import cn.iocoder.yudao.framework.common.exception.ErrorCode;
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCodeMapping;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
-
-/**
- * 阿里云的 SmsCodeMapping 实现类
- *
- * 参见 https://help.aliyun.com/document_detail/101346.htm 文档
- *
- * @author 芋道源码
- */
-public class AliyunSmsCodeMapping implements SmsCodeMapping {
-
-    @Override
-    public ErrorCode apply(String apiCode) {
-        switch (apiCode) {
-            case "OK": return GlobalErrorCodeConstants.SUCCESS;
-            case "isv.ACCOUNT_NOT_EXISTS":
-            case "isv.ACCOUNT_ABNORMAL":
-            case "MissingAccessKeyId": return SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID;
-            case "isp.RAM_PERMISSION_DENY": return SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY;
-            case "isv.INVALID_JSON_PARAM":
-            case "isv.INVALID_PARAMETERS": return SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR;
-            case "isv.BUSINESS_LIMIT_CONTROL": return SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL;
-            case "isv.DAY_LIMIT_CONTROL": return SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL;
-            case "isv.SMS_CONTENT_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID;
-            case "isv.SMS_TEMPLATE_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID;
-            case "isv.SMS_SIGNATURE_ILLEGAL":
-            case "isv.SIGN_NAME_ILLEGAL":
-            case "isv.SMS_SIGN_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID;
-            case "isv.AMOUNT_NOT_ENOUGH":
-            case "isv.OUT_OF_SERVICE": return SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH;
-            case "isv.MOBILE_NUMBER_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID;
-            case "isv.TEMPLATE_MISSING_PARAMETERS": return SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR;
-            default: return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
-        }
-    }
-
-}

+ 0 - 22
yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java

@@ -1,22 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.debug;
-
-import cn.iocoder.yudao.framework.common.exception.ErrorCode;
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCodeMapping;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
-
-import java.util.Objects;
-
-/**
- * 钉钉的 SmsCodeMapping 实现类
- *
- * @author 芋道源码
- */
-public class DebugDingTalkCodeMapping implements SmsCodeMapping {
-
-    @Override
-    public ErrorCode apply(String apiCode) {
-        return Objects.equals(apiCode, "0") ? GlobalErrorCodeConstants.SUCCESS : SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
-    }
-
-}

+ 13 - 13
yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java

@@ -8,19 +8,19 @@ import cn.hutool.crypto.digest.DigestUtil;
 import cn.hutool.crypto.digest.HmacAlgorithm;
 import cn.hutool.http.HttpUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
 import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
 import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
 import cn.iocoder.yudao.framework.sms.core.client.impl.AbstractSmsClient;
 import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
 import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
-import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 
 /**
  * 基于钉钉 WebHook 实现的调试的短信客户端实现类
@@ -32,7 +32,7 @@ import java.util.Map;
 public class DebugDingTalkSmsClient extends AbstractSmsClient {
 
     public DebugDingTalkSmsClient(SmsChannelProperties properties) {
-        super(properties, new DebugDingTalkCodeMapping());
+        super(properties);
         Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
         Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
     }
@@ -42,8 +42,8 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient {
     }
 
     @Override
-    protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId, String mobile,
-                                                        String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
+    public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
+                                  String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
         // 构建请求
         String url = buildUrl("robot/send");
         Map<String, Object> params = new HashMap<>();
@@ -55,14 +55,15 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient {
         String responseText = HttpUtil.post(url, JsonUtils.toJsonString(params));
         // 解析结果
         Map<?, ?> responseObj = JsonUtils.parseObject(responseText, Map.class);
-        return SmsCommonResult.build(MapUtil.getStr(responseObj, "errcode"), MapUtil.getStr(responseObj, "errorMsg"),
-                null, new SmsSendRespDTO().setSerialNo(StrUtil.uuid()), codeMapping);
+        String errorCode = MapUtil.getStr(responseObj, "errcode");
+        return new SmsSendRespDTO().setSuccess(Objects.equals(errorCode, "0")).setSerialNo(StrUtil.uuid())
+                .setApiCode(errorCode).setApiMsg(MapUtil.getStr(responseObj, "errorMsg"));
     }
 
     /**
      * 构建请求地址
      *
-     * 参见 https://developers.dingtalk.com/document/app/custom-robot-access/title-nfv-794-g71 文档
+     * 参见 <a href="https://developers.dingtalk.com/document/app/custom-robot-access/title-nfv-794-g71">文档</a>
      *
      * @param path 请求路径
      * @return 请求地址
@@ -82,15 +83,14 @@ public class DebugDingTalkSmsClient extends AbstractSmsClient {
     }
 
     @Override
-    protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
+    public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
         throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调");
     }
 
     @Override
-    protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) {
-        SmsTemplateRespDTO data = new SmsTemplateRespDTO().setId(apiTemplateId).setContent("")
+    public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) {
+        return new SmsTemplateRespDTO().setId(apiTemplateId).setContent("")
                 .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason("");
-        return SmsCommonResult.build("0", "success", null, data, codeMapping);
     }
 
 }

+ 0 - 41
yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsChannelProperties.java

@@ -1,41 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.tencent;
-
-import cn.hutool.core.bean.BeanUtil;
-import cn.hutool.core.lang.Assert;
-import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
-import lombok.Data;
-
-/**
- * 腾讯云短信配置实现类
- * 腾讯云发送短信时,需要额外的参数 sdkAppId,
- *
- * @author shiwp
- */
-@Data
-public class TencentSmsChannelProperties extends SmsChannelProperties {
-
-    /**
-     * 应用 id
-     */
-    private String sdkAppId;
-
-    /**
-     * 考虑到不破坏原有的 apiKey + apiSecret 的结构,
-     * 所以腾讯云短信存储时,将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。
-     * 因此在使用时,需要将 secretId 和 sdkAppId 解析出来,分别存储到对应字段中。
-     */
-    public static TencentSmsChannelProperties build(SmsChannelProperties properties) {
-        if (properties instanceof TencentSmsChannelProperties) {
-            return (TencentSmsChannelProperties) properties;
-        }
-        TencentSmsChannelProperties result = BeanUtil.toBean(properties, TencentSmsChannelProperties.class);
-        String combineKey = properties.getApiKey();
-        Assert.notEmpty(combineKey, "apiKey 不能为空");
-        String[] keys = combineKey.trim().split(" ");
-        Assert.isTrue(keys.length == 2, "腾讯云短信 apiKey 配置格式错误,请配置 为[secretId sdkAppId]");
-        Assert.notBlank(keys[0], "腾讯云短信 secretId 不能为空");
-        Assert.notBlank(keys[1], "腾讯云短信 sdkAppId 不能为空");
-        result.setSdkAppId(keys[1]).setApiKey(keys[0]);
-        return result;
-    }
-}

+ 62 - 145
yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClient.java

@@ -4,9 +4,7 @@ import cn.hutool.core.lang.Assert;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
 import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
-import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
 import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
 import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
 import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
@@ -17,23 +15,22 @@ import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.annotations.VisibleForTesting;
 import com.tencentcloudapi.common.Credential;
-import com.tencentcloudapi.common.exception.TencentCloudSDKException;
 import com.tencentcloudapi.sms.v20210111.SmsClient;
 import com.tencentcloudapi.sms.v20210111.models.*;
 import lombok.Data;
 
 import java.time.LocalDateTime;
 import java.util.List;
-import java.util.function.Function;
-import java.util.function.Supplier;
+import java.util.Objects;
 
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT;
 
 /**
  * 腾讯云短信功能实现
- * <p>
- * 参见 https://cloud.tencent.com/document/product/382/52077
+ *
+ * 参见 <a href="https://cloud.tencent.com/document/product/382/52077">文档</a>
  *
  * @author shiwp
  */
@@ -42,7 +39,7 @@ public class TencentSmsClient extends AbstractSmsClient {
     /**
      * 调用成功 code
      */
-    public static final String API_SUCCESS_CODE = "Ok";
+    public static final String API_CODE_SUCCESS = "Ok";
 
     /**
      * REGION,使用南京
@@ -51,180 +48,103 @@ public class TencentSmsClient extends AbstractSmsClient {
 
     /**
      * 是否国际/港澳台短信:
+     *
      * 0:表示国内短信。
      * 1:表示国际/港澳台短信。
      */
-    private static final long INTERNATIONAL = 0L;
+    private static final long INTERNATIONAL_CHINA = 0L;
 
     private SmsClient client;
 
     public TencentSmsClient(SmsChannelProperties properties) {
-        super(properties, new TencentSmsCodeMapping());
+        super(properties);
         Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
+        validateSdkAppId(properties);
     }
 
     @Override
     protected void doInit() {
         // 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId,secretKey
-        Credential credential = new Credential(properties.getApiKey(), properties.getApiSecret());
+        Credential credential = new Credential(getApiKey(), properties.getApiSecret());
         client = new SmsClient(credential, ENDPOINT);
     }
 
-    @Override
-    protected SmsCommonResult<SmsSendRespDTO> doSendSms(Long sendLogId,
-                                                        String mobile,
-                                                        String apiTemplateId,
-                                                        List<KeyValue<String, Object>> templateParams) throws Throwable {
-        return invoke(() -> buildSendSmsRequest(sendLogId, mobile, apiTemplateId, templateParams),
-                this::doSendSms0,
-                response -> {
-                    SendStatus sendStatus = response.getSendStatusSet()[0];
-                    return SmsCommonResult.build(sendStatus.getCode(), sendStatus.getMessage(), response.getRequestId(),
-                            new SmsSendRespDTO().setSerialNo(sendStatus.getSerialNo()), codeMapping);
-                });
-    }
-
-
     /**
-     * 腾讯云发放短信的时候,需要额外的参数 sdkAppId。
-     * 考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。
-     * 因此,这边需要使用 TencentSmsChannelProperties 做拆分,重新封装到 properties 内。
+     * 参数校验腾讯云的 SDK AppId
+     *
+     * 原因是:腾讯云发放短信的时候,需要额外的参数 sdkAppId
+     *
+     * 解决方案:考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。
      *
-     * @param properties 数据库中存储的短信渠道配置
-     * @return TencentSmsChannelProperties
+     * @param properties 配置
      */
-    @Override
-    protected SmsChannelProperties prepareProperties(SmsChannelProperties properties) {
-        return TencentSmsChannelProperties.build(properties);
+    private static void validateSdkAppId(SmsChannelProperties properties) {
+        String combineKey = properties.getApiKey();
+        Assert.notEmpty(combineKey, "apiKey 不能为空");
+        String[] keys = combineKey.trim().split(" ");
+        Assert.isTrue(keys.length == 2, "腾讯云短信 apiKey 配置格式错误,请配置 为[secretId sdkAppId]");
     }
 
-    /**
-     * 调用腾讯云 SDK 发送短信
-     *
-     * @param request 发送短信请求
-     * @return 发送短信响应
-     * @throws TencentCloudSDKException SDK 用来封装发送短信失败
-     */
-    private SendSmsResponse doSendSms0(SendSmsRequest request) throws TencentCloudSDKException {
-        return client.SendSms(request);
+    private String getSdkAppId() {
+        return StrUtil.subAfter(properties.getApiKey(), " ", true);
     }
 
-    /**
-     * 封装腾讯云发送短信请求
-     *
-     * @param sendLogId      日志编号
-     * @param mobile         手机号
-     * @param apiTemplateId  短信 API 的模板编号
-     * @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序
-     * @return 腾讯云发送短信请求
-     */
-    private SendSmsRequest buildSendSmsRequest(Long sendLogId,
-                                               String mobile,
-                                               String apiTemplateId,
-                                               List<KeyValue<String, Object>> templateParams) {
+    private String getApiKey() {
+        return StrUtil.subBefore(properties.getApiKey(), " ", true);
+    }
+
+    @Override
+    public SmsSendRespDTO sendSms(Long sendLogId, String mobile,
+                                  String apiTemplateId, List<KeyValue<String, Object>> templateParams) throws Throwable {
+        // 构建请求
         SendSmsRequest request = new SendSmsRequest();
-        request.setSmsSdkAppId(((TencentSmsChannelProperties) properties).getSdkAppId());
+        request.setSmsSdkAppId(getSdkAppId());
         request.setPhoneNumberSet(new String[]{mobile});
         request.setSignName(properties.getSignature());
         request.setTemplateId(apiTemplateId);
         request.setTemplateParamSet(ArrayUtils.toArray(templateParams, e -> String.valueOf(e.getValue())));
         request.setSessionContext(JsonUtils.toJsonString(new SessionContext().setLogId(sendLogId)));
-        return request;
+        // 执行请求
+        SendSmsResponse response = client.SendSms(request);
+        SendStatus status = response.getSendStatusSet()[0];
+        return new SmsSendRespDTO().setSuccess(Objects.equals(status.getCode(), API_CODE_SUCCESS)).setSerialNo(status.getSerialNo())
+                .setApiRequestId(response.getRequestId()).setApiCode(status.getCode()).setApiMsg(status.getMessage());
     }
 
     @Override
-    protected List<SmsReceiveRespDTO> doParseSmsReceiveStatus(String text) throws Throwable {
+    public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
         List<SmsReceiveStatus> callback = JsonUtils.parseArray(text, SmsReceiveStatus.class);
-        return CollectionUtils.convertList(callback, status -> {
-            SmsReceiveRespDTO data = new SmsReceiveRespDTO();
-            data.setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription());
-            data.setReceiveTime(status.getReceiveTime()).setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus()));
-            data.setMobile(status.getMobile()).setSerialNo(status.getSerialNo());
-            SessionContext context;
-            Long logId;
-            Assert.notNull(context = status.getSessionContext(), "回执信息中未解析出 context,请联系腾讯云小助手");
-            Assert.notNull(logId = context.getLogId(), "回执信息中未解析出 logId,请联系腾讯云小助手");
-            data.setLogId(logId);
-            return data;
-        });
+        return convertList(callback, status -> new SmsReceiveRespDTO()
+                .setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus()))
+                .setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription())
+                .setMobile(status.getMobile()).setReceiveTime(status.getReceiveTime())
+                .setSerialNo(status.getSerialNo()).setLogId(status.getSessionContext().getLogId()));
     }
 
     @Override
-    protected SmsCommonResult<SmsTemplateRespDTO> doGetSmsTemplate(String apiTemplateId) throws Throwable {
-        return invoke(() -> this.buildSmsTemplateStatusRequest(apiTemplateId),
-                this::doGetSmsTemplate0,
-                response -> {
-                    SmsTemplateRespDTO data = convertTemplateStatusDTO(response.getDescribeTemplateStatusSet()[0]);
-                    return SmsCommonResult.build(API_SUCCESS_CODE, null, response.getRequestId(), data, codeMapping);
-                });
-    }
-
-    @VisibleForTesting
-    SmsTemplateRespDTO convertTemplateStatusDTO(DescribeTemplateListStatus templateStatus) {
-        if (templateStatus == null) {
-            return null;
-        }
-        SmsTemplateAuditStatusEnum auditStatus;
-        Assert.notNull(templateStatus.getStatusCode(),
-                StrUtil.format("短信模版审核状态为 null,模版 id{}", templateStatus.getTemplateId()));
-        switch (templateStatus.getStatusCode().intValue()) {
-            case -1:
-                auditStatus = SmsTemplateAuditStatusEnum.FAIL;
-                break;
-            case 0:
-                auditStatus = SmsTemplateAuditStatusEnum.SUCCESS;
-                break;
-            case 1:
-                auditStatus = SmsTemplateAuditStatusEnum.CHECKING;
-                break;
-            default:
-                throw new IllegalStateException(StrUtil.format("不能解析短信模版审核状态{},模版 id{}",
-                        templateStatus.getStatusCode(), templateStatus.getTemplateId()));
-        }
-        SmsTemplateRespDTO data = new SmsTemplateRespDTO();
-        data.setId(String.valueOf(templateStatus.getTemplateId())).setContent(templateStatus.getTemplateContent());
-        data.setAuditStatus(auditStatus.getStatus()).setAuditReason(templateStatus.getReviewReply());
-        return data;
-    }
-
-    /**
-     * 封装查询模版审核状态请求
-     * @param apiTemplateId api 的模版 id
-     * @return 查询模版审核状态请求
-     */
-    private DescribeSmsTemplateListRequest buildSmsTemplateStatusRequest(String apiTemplateId) {
+    public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
+        // 构建请求
         DescribeSmsTemplateListRequest request = new DescribeSmsTemplateListRequest();
         request.setTemplateIdSet(new Long[]{Long.parseLong(apiTemplateId)});
-        // 地区 0:表示国内短信。1:表示国际/港澳台短信。
-        request.setInternational(INTERNATIONAL);
-        return request;
-    }
-
-    /**
-     * 调用腾讯云 SDK 查询短信模版状态
-     *
-     * @param request 查询短信模版状态请求
-     * @return 查询短信模版状态响应
-     * @throws TencentCloudSDKException SDK 用来封装查询短信模版状态失败
-     */
-    private DescribeSmsTemplateListResponse doGetSmsTemplate0(DescribeSmsTemplateListRequest request) throws TencentCloudSDKException {
-        return client.DescribeSmsTemplateList(request);
+        request.setInternational(INTERNATIONAL_CHINA);
+        // 执行请求
+        DescribeSmsTemplateListResponse response = client.DescribeSmsTemplateList(request);
+        DescribeTemplateListStatus status = response.getDescribeTemplateStatusSet()[0];
+        if (status == null || status.getStatusCode() == null) {
+            return null;
+        }
+        return new SmsTemplateRespDTO().setId(status.getTemplateId().toString()).setContent(status.getTemplateContent())
+                .setAuditStatus(convertSmsTemplateAuditStatus(status.getStatusCode().intValue())).setAuditReason(status.getReviewReply());
     }
 
-    <Q, P, R> SmsCommonResult<R> invoke(Supplier<Q> requestSupplier,
-                                        SdkFunction<Q, P> responseSupplier,
-                                        Function<P, SmsCommonResult<R>> resultGen) {
-        // 构建请求body
-        Q request = requestSupplier.get();
-        P response;
-        // 调用腾讯云发送短信
-        try {
-            response = responseSupplier.apply(request);
-        } catch (TencentCloudSDKException e) {
-            // 调用异常,封装结果
-            return SmsCommonResult.build(e.getErrorCode(), e.getMessage(), e.getRequestId(), null, codeMapping);
+    @VisibleForTesting
+    Integer convertSmsTemplateAuditStatus(int templateStatus) {
+        switch (templateStatus) {
+            case 1: return SmsTemplateAuditStatusEnum.CHECKING.getStatus();
+            case 0: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
+            case -1: return SmsTemplateAuditStatusEnum.FAIL.getStatus();
+            default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
         }
-        return resultGen.apply(response);
     }
 
     @Data
@@ -278,7 +198,7 @@ public class TencentSmsClient extends AbstractSmsClient {
         private String serialNo;
 
         /**
-         * 用户的 session 内容(与发送接口的请求参数SessionContext一致)
+         * 用户的 session 内容(与发送接口的请求参数 SessionContext 一致)
          */
         @JsonProperty("ext")
         private SessionContext sessionContext;
@@ -293,10 +213,7 @@ public class TencentSmsClient extends AbstractSmsClient {
          * 发送短信记录id
          */
         private Long logId;
-    }
 
-    private interface SdkFunction<T, R> {
-        R apply(T t) throws TencentCloudSDKException;
     }
 
 }

+ 0 - 50
yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMapping.java

@@ -1,50 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.tencent;
-
-import cn.iocoder.yudao.framework.common.exception.ErrorCode;
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCodeMapping;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
-
-import static cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*;
-
-/**
- * 腾讯云的 SmsCodeMapping 实现类
- *
- * 参见 https://cloud.tencent.com/document/api/382/52075#.E5.85.AC.E5.85.B1.E9.94.99.E8.AF.AF.E7.A0.81
- *
- * @author : shiwp
- */
-public class TencentSmsCodeMapping implements SmsCodeMapping {
-
-    @Override
-    public ErrorCode apply(String apiCode) {
-        switch (apiCode) {
-            case TencentSmsClient.API_SUCCESS_CODE: return GlobalErrorCodeConstants.SUCCESS;
-            case "FailedOperation.ContainSensitiveWord": return SMS_SEND_CONTENT_INVALID;
-            case "FailedOperation.JsonParseFail":
-            case "MissingParameter.EmptyPhoneNumberSet":
-            case "LimitExceeded.PhoneNumberCountLimit":
-            case "FailedOperation.FailResolvePacket": return GlobalErrorCodeConstants.BAD_REQUEST;
-            case "FailedOperation.InsufficientBalanceInSmsPackage": return SMS_ACCOUNT_MONEY_NOT_ENOUGH;
-            case "FailedOperation.MarketingSendTimeConstraint": return SMS_SEND_MARKET_LIMIT_CONTROL;
-            case "FailedOperation.PhoneNumberInBlacklist": return SMS_MOBILE_BLACK;
-            case "FailedOperation.SignatureIncorrectOrUnapproved": return SMS_SIGN_INVALID;
-            case "FailedOperation.MissingTemplateToModify":
-            case "FailedOperation.TemplateIncorrectOrUnapproved": return SMS_TEMPLATE_INVALID;
-            case "InvalidParameterValue.IncorrectPhoneNumber": return SMS_MOBILE_INVALID;
-            case "InvalidParameterValue.SdkAppIdNotExist": return SMS_APP_ID_INVALID;
-            case "InvalidParameterValue.TemplateParameterLengthLimit":
-            case "InvalidParameterValue.TemplateParameterFormatError": return SMS_TEMPLATE_PARAM_ERROR;
-            case "LimitExceeded.PhoneNumberDailyLimit": return SMS_SEND_DAY_LIMIT_CONTROL;
-            case "LimitExceeded.PhoneNumberThirtySecondLimit":
-            case "LimitExceeded.PhoneNumberOneHourLimit": return SMS_SEND_BUSINESS_LIMIT_CONTROL;
-            case "UnauthorizedOperation.RequestPermissionDeny":
-            case "FailedOperation.ForbidAddMarketingTemplates":
-            case "FailedOperation.NotEnterpriseCertification":
-            case "UnauthorizedOperation.IndividualUserMarketingSmsPermissionDeny": return SMS_PERMISSION_DENY;
-            case "UnauthorizedOperation.RequestIpNotInWhitelist": return SMS_IP_DENY;
-            case "AuthFailure.SecretIdNotFound": return SMS_ACCOUNT_INVALID;
-        }
-        return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN;
-    }
-}

+ 0 - 55
yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientIntegrationTest.java

@@ -1,55 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun;
-
-import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
-import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
-import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
-import cn.iocoder.yudao.framework.sms.core.client.impl.aliyun.AliyunSmsClient;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum;
-import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * {@link AliyunSmsClient} 的集成测试
- */
-public class AliyunSmsClientIntegrationTest {
-
-    private static AliyunSmsClient smsClient;
-
-    @BeforeAll
-    public static void before() {
-        // 创建配置类
-        SmsChannelProperties properties = new SmsChannelProperties();
-        properties.setId(1L);
-        properties.setSignature("Ballcat");
-        properties.setCode(SmsChannelEnum.ALIYUN.getCode());
-        properties.setApiKey(System.getenv("ALIYUN_ACCESS_KEY"));
-        properties.setApiSecret(System.getenv("ALIYUN_SECRET_KEY"));
-        // 创建客户端
-        smsClient = new AliyunSmsClient(properties);
-        smsClient.init();
-    }
-
-    @Test
-    public void testSendSms() {
-        List<KeyValue<String, Object>> templateParams = new ArrayList<>();
-        templateParams.add(new KeyValue<>("code", "1024"));
-//        templateParams.put("operation", "嘿嘿");
-//        SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams);
-        SmsCommonResult<SmsSendRespDTO> result = smsClient.sendSms(1L, "15601691399",
-                "SMS_207945135", templateParams);
-        System.out.println(result);
-    }
-
-    @Test
-    public void testGetSmsTemplate() {
-        String apiTemplateId = "SMS_2079451351";
-        SmsCommonResult<SmsTemplateRespDTO> result = smsClient.getSmsTemplate(apiTemplateId);
-        System.out.println(result);
-    }
-
-}

+ 0 - 46
yudao-framework/yudao-spring-boot-starter-biz-sms/src/test-integration/java/cn/iocoder/yudao/framework/sms/core/client/impl/debug/DebugDingTalkSmsClientIntegrationTest.java

@@ -1,46 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.debug;
-
-import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
-import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
-import cn.iocoder.yudao.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsChannelEnum;
-import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.Test;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * {@link DebugDingTalkSmsClient} 的集成测试
- */
-public class DebugDingTalkSmsClientIntegrationTest {
-
-    private static DebugDingTalkSmsClient smsClient;
-
-    @BeforeAll
-    public static void init() {
-        // 创建配置类
-        SmsChannelProperties properties = new SmsChannelProperties();
-        properties.setId(1L);
-        properties.setSignature("芋道");
-        properties.setCode(SmsChannelEnum.DEBUG_DING_TALK.getCode());
-        properties.setApiKey("696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859");
-        properties.setApiSecret("SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67");
-        // 创建客户端
-        smsClient = new DebugDingTalkSmsClient(properties);
-        smsClient.init();
-    }
-
-    @Test
-    public void testSendSms() {
-        List<KeyValue<String, Object>> templateParams = new ArrayList<>();
-        templateParams.add(new KeyValue<>("code", "1024"));
-        templateParams.add(new KeyValue<>("operation", "嘿嘿"));
-//        SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams);
-        SmsCommonResult<SmsSendRespDTO> result = smsClient.sendSms(1L, "15601691399", "4383920", templateParams);
-        System.out.println(result);
-    }
-
-}

+ 45 - 83
yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java

@@ -1,26 +1,20 @@
 package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun;
 
 import cn.hutool.core.util.ReflectUtil;
-import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
 import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
 import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
 import cn.iocoder.yudao.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
 import cn.iocoder.yudao.framework.sms.core.property.SmsChannelProperties;
-import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
-import com.aliyuncs.AcsRequest;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
 import com.aliyuncs.IAcsClient;
 import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest;
 import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse;
 import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
 import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
-import com.aliyuncs.exceptions.ClientException;
 import com.google.common.collect.Lists;
-import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.mockito.ArgumentMatcher;
 import org.mockito.InjectMocks;
@@ -28,12 +22,10 @@ import org.mockito.Mock;
 
 import java.time.LocalDateTime;
 import java.util.List;
-import java.util.function.Function;
 
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
 import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.when;
 
@@ -67,8 +59,7 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
     }
 
     @Test
-    @SuppressWarnings("unchecked")
-    public void testDoSendSms() throws ClientException {
+    public void tesSendSms_success() throws Throwable {
         // 准备参数
         Long sendLogId = randomLongId();
         String mobile = randomString();
@@ -87,20 +78,47 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
         }))).thenReturn(response);
 
         // 调用
-        SmsCommonResult<SmsSendRespDTO> result = smsClient.doSendSms(sendLogId, mobile,
+        SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile,
                 apiTemplateId, templateParams);
         // 断言
+        assertTrue(result.getSuccess());
+        assertEquals(response.getRequestId(), result.getApiRequestId());
         assertEquals(response.getCode(), result.getApiCode());
         assertEquals(response.getMessage(), result.getApiMsg());
-        assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
-        assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
+        assertEquals(response.getBizId(), result.getSerialNo());
+    }
+
+    @Test
+    public void tesSendSms_fail() throws Throwable {
+        // 准备参数
+        Long sendLogId = randomLongId();
+        String mobile = randomString();
+        String apiTemplateId = randomString();
+        List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
+                new KeyValue<>("code", 1234), new KeyValue<>("op", "login"));
+        // mock 方法
+        SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("ERROR"));
+        when(client.getAcsResponse(argThat((ArgumentMatcher<SendSmsRequest>) acsRequest -> {
+            assertEquals(mobile, acsRequest.getPhoneNumbers());
+            assertEquals(properties.getSignature(), acsRequest.getSignName());
+            assertEquals(apiTemplateId, acsRequest.getTemplateCode());
+            assertEquals(toJsonString(MapUtils.convertMap(templateParams)), acsRequest.getTemplateParam());
+            assertEquals(sendLogId.toString(), acsRequest.getOutId());
+            return true;
+        }))).thenReturn(response);
+
+        // 调用
+        SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
+        // 断言
+        assertFalse(result.getSuccess());
         assertEquals(response.getRequestId(), result.getApiRequestId());
-        // 断言结果
-        assertEquals(response.getBizId(), result.getData().getSerialNo());
+        assertEquals(response.getCode(), result.getApiCode());
+        assertEquals(response.getMessage(), result.getApiMsg());
+        assertEquals(response.getBizId(), result.getSerialNo());
     }
 
     @Test
-    public void testDoTParseSmsReceiveStatus() throws Throwable {
+    public void testParseSmsReceiveStatus() {
         // 准备参数
         String text = "[\n" +
                 "  {\n" +
@@ -118,20 +136,21 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
         // mock 方法
 
         // 调用
-        List<SmsReceiveRespDTO> statuses = smsClient.doParseSmsReceiveStatus(text);
+        List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
         // 断言
         assertEquals(1, statuses.size());
         assertTrue(statuses.get(0).getSuccess());
         assertEquals("DELIVERED", statuses.get(0).getErrorCode());
         assertEquals("用户接收成功", statuses.get(0).getErrorMsg());
         assertEquals("13900000001", statuses.get(0).getMobile());
-        assertEquals(LocalDateTime.of(2017, 2, 2, 22, 23, 24), statuses.get(0).getReceiveTime());
+        assertEquals(LocalDateTime.of(2017, 2, 2, 22, 23, 24),
+                statuses.get(0).getReceiveTime());
         assertEquals("12345", statuses.get(0).getSerialNo());
         assertEquals(67890L, statuses.get(0).getLogId());
     }
 
     @Test
-    public void testDoGetSmsTemplate() throws ClientException {
+    public void testGetSmsTemplate() throws Throwable {
         // 准备参数
         String apiTemplateId = randomString();
         // mock 方法
@@ -145,18 +164,12 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
         }))).thenReturn(response);
 
         // 调用
-        SmsCommonResult<SmsTemplateRespDTO> result = smsClient.doGetSmsTemplate(apiTemplateId);
+        SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId);
         // 断言
-        assertEquals(response.getCode(), result.getApiCode());
-        assertEquals(response.getMessage(), result.getApiMsg());
-        assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
-        assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
-        assertEquals(response.getRequestId(), result.getApiRequestId());
-        // 断言结果
-        assertEquals(response.getTemplateCode(), result.getData().getId());
-        assertEquals(response.getTemplateContent(), result.getData().getContent());
-        assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus());
-        assertEquals(response.getReason(), result.getData().getAuditReason());
+        assertEquals(response.getTemplateCode(), result.getId());
+        assertEquals(response.getTemplateContent(), result.getContent());
+        assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
+        assertEquals(response.getReason(), result.getAuditReason());
     }
 
     @Test
@@ -171,55 +184,4 @@ public class AliyunSmsClientTest extends BaseMockitoUnitTest {
                 "未知审核状态(3)");
     }
 
-    @Test
-    @SuppressWarnings("unchecked")
-    public void testInvoke_throwable() throws ClientException {
-        // 准备参数
-        QuerySmsTemplateRequest request = new QuerySmsTemplateRequest();
-        // mock 方法
-        ClientException ex = new ClientException("isv.INVALID_PARAMETERS", "参数不正确", randomString());
-        when(client.getAcsResponse(any(AcsRequest.class))).thenThrow(ex);
-
-        // 调用,并断言异常
-        SmsCommonResult<?> result = smsClient.invoke(request, null);
-        // 断言
-        assertEquals(ex.getErrCode(), result.getApiCode());
-        assertEquals(ex.getErrMsg(), result.getApiMsg());
-        Assertions.assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR.getCode(), result.getCode());
-        Assertions.assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR.getMsg(), result.getMsg());
-        assertEquals(ex.getRequestId(), result.getApiRequestId());
-    }
-
-    @Test
-    public void testInvoke_success() throws ClientException {
-        // 准备参数
-        QuerySmsTemplateRequest request = new QuerySmsTemplateRequest();
-        Function<QuerySmsTemplateResponse, SmsTemplateRespDTO> responseConsumer = response -> {
-            SmsTemplateRespDTO data = new SmsTemplateRespDTO();
-            data.setId(response.getTemplateCode()).setContent(response.getTemplateContent());
-            data.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(response.getReason());
-            return data;
-        };
-        // mock 方法
-        QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> {
-            o.setCode("OK");
-            o.setTemplateStatus(1); // 设置模板通过
-        });
-        when(client.getAcsResponse(any(AcsRequest.class))).thenReturn(response);
-
-        // 调用
-        SmsCommonResult<SmsTemplateRespDTO> result = smsClient.invoke(request, responseConsumer);
-        // 断言
-        assertEquals(response.getCode(), result.getApiCode());
-        assertEquals(response.getMessage(), result.getApiMsg());
-        assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
-        assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
-        assertEquals(response.getRequestId(), result.getApiRequestId());
-        // 断言结果
-        assertEquals(response.getTemplateCode(), result.getData().getId());
-        assertEquals(response.getTemplateContent(), result.getData().getContent());
-        assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus());
-        assertEquals(response.getReason(), result.getData().getAuditReason());
-    }
-
 }

+ 0 - 43
yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java

@@ -1,43 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.aliyun;
-
-import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
-import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * {@link AliyunSmsCodeMapping} 的单元测试
- *
- * @author 芋道源码
- */
-public class AliyunSmsCodeMappingTest extends BaseMockitoUnitTest {
-
-    @InjectMocks
-    private AliyunSmsCodeMapping codeMapping;
-
-    @Test
-    public void testApply() {
-        assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply("OK"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("MissingAccessKeyId"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_NOT_EXISTS"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_ABNORMAL"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL, codeMapping.apply("isv.DAY_LIMIT_CONTROL"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID, codeMapping.apply("isv.SMS_CONTENT_ILLEGAL"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGN_ILLEGAL"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SIGN_NAME_ILLEGAL"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("isp.RAM_PERMISSION_DENY"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.OUT_OF_SERVICE"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.AMOUNT_NOT_ENOUGH"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("isv.SMS_TEMPLATE_ILLEGAL"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGNATURE_ILLEGAL"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_PARAMETERS"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_JSON_PARAM"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID, codeMapping.apply("isv.MOBILE_NUMBER_ILLEGAL"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("isv.TEMPLATE_MISSING_PARAMETERS"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("isv.BUSINESS_LIMIT_CONTROL"));
-    }
-
-}

+ 63 - 55
yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java

@@ -1,13 +1,10 @@
 package cn.iocoder.yudao.framework.sms.core.client.impl.tencent;
 
 import cn.hutool.core.util.ReflectUtil;
-import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.core.KeyValue;
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
 import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
-import cn.iocoder.yudao.framework.sms.core.client.SmsCommonResult;
 import cn.iocoder.yudao.framework.sms.core.client.dto.SmsReceiveRespDTO;
 import cn.iocoder.yudao.framework.sms.core.client.dto.SmsSendRespDTO;
 import cn.iocoder.yudao.framework.sms.core.client.dto.SmsTemplateRespDTO;
@@ -31,7 +28,6 @@ import java.util.List;
 import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
 import static org.junit.jupiter.api.Assertions.*;
-import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.Mockito.when;
 
@@ -78,7 +74,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
     }
 
     @Test
-    public void testDoSendSms() throws Throwable {
+    public void testDoSendSms_success() throws Throwable {
         // 准备参数
         Long sendLogId = randomLongId();
         String mobile = randomString();
@@ -94,7 +90,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
             o.setSendStatusSet(sendStatuses);
             SendStatus sendStatus = new SendStatus();
             sendStatuses[0] = sendStatus;
-            sendStatus.setCode(TencentSmsClient.API_SUCCESS_CODE);
+            sendStatus.setCode(TencentSmsClient.API_CODE_SUCCESS);
             sendStatus.setMessage("send success");
             sendStatus.setSerialNo(serialNo);
         });
@@ -109,20 +105,58 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
         }))).thenReturn(response);
 
         // 调用
-        SmsCommonResult<SmsSendRespDTO> result = smsClient.doSendSms(sendLogId, mobile,
-                apiTemplateId, templateParams);
+        SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
         // 断言
+        assertTrue(result.getSuccess());
+        assertEquals(response.getRequestId(), result.getApiRequestId());
         assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode());
         assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg());
-        assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
-        assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
+        assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo());
+    }
+
+    @Test
+    public void testDoSendSms_fail() throws Throwable {
+        // 准备参数
+        Long sendLogId = randomLongId();
+        String mobile = randomString();
+        String apiTemplateId = randomString();
+        List<KeyValue<String, Object>> templateParams = Lists.newArrayList(
+                new KeyValue<>("1", 1234), new KeyValue<>("2", "login"));
+        String requestId = randomString();
+        String serialNo = randomString();
+        // mock 方法
+        SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> {
+            o.setRequestId(requestId);
+            SendStatus[] sendStatuses = new SendStatus[1];
+            o.setSendStatusSet(sendStatuses);
+            SendStatus sendStatus = new SendStatus();
+            sendStatuses[0] = sendStatus;
+            sendStatus.setCode("ERROR");
+            sendStatus.setMessage("send success");
+            sendStatus.setSerialNo(serialNo);
+        });
+        when(client.SendSms(argThat(request -> {
+            assertEquals(mobile, request.getPhoneNumberSet()[0]);
+            assertEquals(properties.getSignature(), request.getSignName());
+            assertEquals(apiTemplateId, request.getTemplateId());
+            assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)),
+                    toJsonString(request.getTemplateParamSet()));
+            assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId"));
+            return true;
+        }))).thenReturn(response);
+
+        // 调用
+        SmsSendRespDTO result = smsClient.sendSms(sendLogId, mobile, apiTemplateId, templateParams);
+        // 断言
+        assertFalse(result.getSuccess());
         assertEquals(response.getRequestId(), result.getApiRequestId());
-        // 断言结果
-        assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getData().getSerialNo());
+        assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode());
+        assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg());
+        assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getSerialNo());
     }
 
     @Test
-    public void testDoTParseSmsReceiveStatus() throws Throwable {
+    public void testParseSmsReceiveStatus() {
         // 准备参数
         String text = "[\n" +
                 "    {\n" +
@@ -139,7 +173,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
         // mock 方法
 
         // 调用
-        List<SmsReceiveRespDTO> statuses = smsClient.doParseSmsReceiveStatus(text);
+        List<SmsReceiveRespDTO> statuses = smsClient.parseSmsReceiveStatus(text);
         // 断言
         assertEquals(1, statuses.size());
         assertTrue(statuses.get(0).getSuccess());
@@ -152,7 +186,7 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
     }
 
     @Test
-    public void testDoGetSmsTemplate() throws Throwable {
+    public void testGetSmsTemplate() throws Throwable {
         // 准备参数
         Long apiTemplateId = randomLongId();
         String requestId = randomString();
@@ -173,50 +207,24 @@ public class TencentSmsClientTest extends BaseMockitoUnitTest {
         }))).thenReturn(response);
 
         // 调用
-        SmsCommonResult<SmsTemplateRespDTO> result = smsClient.doGetSmsTemplate(apiTemplateId.toString());
+        SmsTemplateRespDTO result = smsClient.getSmsTemplate(apiTemplateId.toString());
         // 断言
-        assertEquals(TencentSmsClient.API_SUCCESS_CODE, result.getApiCode());
-        assertNull(result.getApiMsg());
-        assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode());
-        assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg());
-        assertEquals(response.getRequestId(), result.getApiRequestId());
-        // 断言结果
-        assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getData().getId());
-        assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getData().getContent());
-        assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus());
-        assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getData().getAuditReason());
-    }
-
-    @Test
-    public void testConvertSuccessTemplateStatus() {
-        testTemplateStatus(SmsTemplateAuditStatusEnum.SUCCESS, 0L);
-    }
-
-    @Test
-    public void testConvertCheckingTemplateStatus() {
-        testTemplateStatus(SmsTemplateAuditStatusEnum.CHECKING, 1L);
-    }
-
-    @Test
-    public void testConvertFailTemplateStatus() {
-        testTemplateStatus(SmsTemplateAuditStatusEnum.FAIL, -1L);
+        assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getId());
+        assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getContent());
+        assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getAuditStatus());
+        assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getAuditReason());
     }
 
     @Test
-    public void testConvertUnknownTemplateStatus() {
-        DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus();
-        templateStatus.setStatusCode(3L);
-        Long templateId = randomLongId();
-        // 调用,并断言结果
-        assertThrows(IllegalStateException.class, () -> smsClient.convertTemplateStatusDTO(templateStatus),
-                StrUtil.format("不能解析短信模版审核状态[3],模版id[{}]", templateId));
-    }
-
-    private void testTemplateStatus(SmsTemplateAuditStatusEnum expected, Long value) {
-        DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus();
-        templateStatus.setStatusCode(value);
-        SmsTemplateRespDTO result = smsClient.convertTemplateStatusDTO(templateStatus);
-        assertEquals(expected.getStatus(), result.getAuditStatus());
+    public void testConvertSmsTemplateAuditStatus() {
+        assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(),
+                smsClient.convertSmsTemplateAuditStatus(0));
+        assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(),
+                smsClient.convertSmsTemplateAuditStatus(1));
+        assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(),
+                smsClient.convertSmsTemplateAuditStatus(-1));
+        assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus(3),
+                "未知审核状态(3)");
     }
 
 }

+ 0 - 50
yudao-framework/yudao-spring-boot-starter-biz-sms/src/test/java/cn/iocoder/yudao/framework/sms/core/client/impl/tencent/TencentSmsCodeMappingTest.java

@@ -1,50 +0,0 @@
-package cn.iocoder.yudao.framework.sms.core.client.impl.tencent;
-
-import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
-import cn.iocoder.yudao.framework.sms.core.enums.SmsFrameworkErrorCodeConstants;
-import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
-import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * {@link TencentSmsCodeMapping} 的单元测试
- *
- * @author : shiwp
- */
-public class TencentSmsCodeMappingTest extends BaseMockitoUnitTest {
-
-    @InjectMocks
-    private TencentSmsCodeMapping codeMapping;
-
-    @Test
-    public void testApply() {
-        assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply(TencentSmsClient.API_SUCCESS_CODE));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID, codeMapping.apply("FailedOperation.ContainSensitiveWord"));
-        assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("FailedOperation.JsonParseFail"));
-        assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("MissingParameter.EmptyPhoneNumberSet"));
-        assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("LimitExceeded.PhoneNumberCountLimit"));
-        assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("FailedOperation.FailResolvePacket"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("FailedOperation.InsufficientBalanceInSmsPackage"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_MARKET_LIMIT_CONTROL, codeMapping.apply("FailedOperation.MarketingSendTimeConstraint"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_BLACK, codeMapping.apply("FailedOperation.PhoneNumberInBlacklist"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("FailedOperation.SignatureIncorrectOrUnapproved"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("FailedOperation.MissingTemplateToModify"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("FailedOperation.TemplateIncorrectOrUnapproved"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID, codeMapping.apply("InvalidParameterValue.IncorrectPhoneNumber"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_APP_ID_INVALID, codeMapping.apply("InvalidParameterValue.SdkAppIdNotExist"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("InvalidParameterValue.TemplateParameterLengthLimit"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("InvalidParameterValue.TemplateParameterFormatError"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberDailyLimit"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberThirtySecondLimit"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberOneHourLimit"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("UnauthorizedOperation.RequestPermissionDeny"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("FailedOperation.ForbidAddMarketingTemplates"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("FailedOperation.NotEnterpriseCertification"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("UnauthorizedOperation.IndividualUserMarketingSmsPermissionDeny"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_IP_DENY, codeMapping.apply("UnauthorizedOperation.RequestIpNotInWhitelist"));
-        assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("AuthFailure.SecretIdNotFound"));
-    }
-
-}

+ 0 - 56
yudao-framework/yudao-spring-boot-starter-biz-social/pom.xml

@@ -1,56 +0,0 @@
-<?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>
-        <groupId>cn.iocoder.boot</groupId>
-        <artifactId>yudao-framework</artifactId>
-        <version>${revision}</version>
-    </parent>
-    <packaging>jar</packaging>
-    <modelVersion>4.0.0</modelVersion>
-
-    <artifactId>yudao-spring-boot-starter-biz-social</artifactId>
-    <name>${project.artifactId}</name>
-
-    <dependencies>
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-common</artifactId>
-        </dependency>
-        <!-- Spring 核心 -->
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-aop</artifactId>
-        </dependency>
-        <!-- Web 相关 -->
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-spring-boot-starter-web</artifactId>
-        </dependency>
-        <!-- spring boot 配置所需依赖 -->
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-configuration-processor</artifactId>
-            <optional>true</optional>
-        </dependency>
-        <!-- 三方云服务相关 -->
-        <dependency>
-            <groupId>com.xingyuv</groupId>
-            <artifactId>spring-boot-starter-justauth</artifactId>
-            <exclusions>
-                <exclusion>
-                    <groupId>cn.hutool</groupId>
-                    <artifactId>hutool-core</artifactId>
-                </exclusion>
-            </exclusions>
-        </dependency>
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-spring-boot-starter-redis</artifactId>
-        </dependency>
-
-    </dependencies>
-
-
-</project>

+ 0 - 36
yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/config/YudaoSocialAutoConfiguration.java

@@ -1,36 +0,0 @@
-package cn.iocoder.yudao.framework.social.config;
-
-import cn.iocoder.yudao.framework.social.core.YudaoAuthRequestFactory;
-import com.xingyuv.http.HttpUtil;
-import com.xingyuv.http.support.hutool.HutoolImpl;
-import com.xingyuv.jushauth.cache.AuthStateCache;
-import com.xingyuv.justauth.autoconfigure.JustAuthProperties;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.boot.autoconfigure.AutoConfiguration;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Primary;
-
-/**
- * 社交自动装配类
- *
- * @author timfruit
- * @date 2021-10-30
- */
-@Slf4j
-@AutoConfiguration
-@EnableConfigurationProperties(JustAuthProperties.class)
-public class YudaoSocialAutoConfiguration {
-
-    @Bean
-    @Primary
-    @ConditionalOnProperty(prefix = "justauth", value = "enabled", havingValue = "true", matchIfMissing = true)
-    public YudaoAuthRequestFactory yudaoAuthRequestFactory(JustAuthProperties properties, AuthStateCache authStateCache) {
-        // 需要修改 HttpUtil 使用的实现,避免类报错
-        HttpUtil.setHttp(new HutoolImpl());
-        // 创建 YudaoAuthRequestFactory
-        return new YudaoAuthRequestFactory(properties, authStateCache);
-    }
-
-}

+ 0 - 94
yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/YudaoAuthRequestFactory.java

@@ -1,94 +0,0 @@
-package cn.iocoder.yudao.framework.social.core;
-
-import cn.hutool.core.util.EnumUtil;
-import cn.hutool.core.util.ReflectUtil;
-import cn.iocoder.yudao.framework.social.core.enums.AuthExtendSource;
-import cn.iocoder.yudao.framework.social.core.request.AuthWeChatMiniAppRequest;
-import cn.iocoder.yudao.framework.social.core.request.AuthWeChatMpRequest;
-import com.xingyuv.jushauth.cache.AuthStateCache;
-import com.xingyuv.jushauth.config.AuthConfig;
-import com.xingyuv.jushauth.config.AuthSource;
-import com.xingyuv.jushauth.request.AuthRequest;
-import com.xingyuv.justauth.AuthRequestFactory;
-import com.xingyuv.justauth.autoconfigure.JustAuthProperties;
-
-import java.lang.reflect.Method;
-
-import static com.xingyuv.jushauth.config.AuthDefaultSource.WECHAT_MP;
-
-/**
- * 第三方授权拓展 request 工厂类
- * 为使得拓展配置 {@link AuthConfig} 和默认配置齐平,所以自定义本工厂类
- *
- * @author timfruit
- * @date 2021-10-31
- */
-public class YudaoAuthRequestFactory extends AuthRequestFactory {
-
-    protected JustAuthProperties properties;
-    protected AuthStateCache authStateCache;
-
-    /**
-     * 由于父类 configureHttpConfig 方法是 private 修饰,所以获取后,进行反射调用
-     */
-    private final Method configureHttpConfigMethod = ReflectUtil.getMethod(AuthRequestFactory.class,
-            "configureHttpConfig", String.class, AuthConfig.class, JustAuthProperties.JustAuthHttpConfig.class);
-
-    public YudaoAuthRequestFactory(JustAuthProperties properties, AuthStateCache authStateCache) {
-        super(properties, authStateCache);
-        this.properties = properties;
-        this.authStateCache = authStateCache;
-    }
-
-    /**
-     * 返回 AuthRequest 对象
-     *
-     * @param source {@link AuthSource}
-     * @return {@link AuthRequest}
-     */
-    @Override
-    public AuthRequest get(String source) {
-        // 先尝试获取自定义扩展的
-        AuthRequest authRequest = getExtendRequest(source);
-        // 找不到,使用默认拓展
-        if (authRequest == null) {
-            authRequest = super.get(source);
-        }
-        return authRequest;
-    }
-
-    protected AuthRequest getExtendRequest(String source) {
-        // TODO 芋艿:临时兼容 justauth 迁移的类型不对问题;
-        if (WECHAT_MP.name().equalsIgnoreCase(source)) {
-            AuthConfig config = properties.getType().get(WECHAT_MP.name());
-            return new AuthWeChatMpRequest(config, authStateCache);
-        }
-
-        AuthExtendSource authExtendSource;
-        try {
-            authExtendSource = EnumUtil.fromString(AuthExtendSource.class, source.toUpperCase());
-        } catch (IllegalArgumentException e) {
-            // 无自定义匹配
-            return null;
-        }
-
-        // 拓展配置和默认配置齐平,properties 放在一起
-        AuthConfig config = properties.getType().get(authExtendSource.name());
-        // 找不到对应关系,直接返回空
-        if (config == null) {
-            return null;
-        }
-        // 反射调用,配置 http config
-        ReflectUtil.invoke(this, configureHttpConfigMethod, authExtendSource.name(), config, properties.getHttpConfig());
-
-        // 获得拓展的 Request
-        // noinspection SwitchStatementWithTooFewBranches
-        switch (authExtendSource) {
-            case WECHAT_MINI_APP:
-                return new AuthWeChatMiniAppRequest(config, authStateCache);
-            default:
-                return null;
-        }
-    }
-
-}

+ 0 - 45
yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/enums/AuthExtendSource.java

@@ -1,45 +0,0 @@
-package cn.iocoder.yudao.framework.social.core.enums;
-
-import com.xingyuv.jushauth.config.AuthSource;
-import com.xingyuv.jushauth.request.AuthDefaultRequest;
-
-/**
- * 拓展 JustAuth 各 api 需要的 url, 用枚举类分平台类型管理
- *
- * 默认配置 {@link com.xingyuv.jushauth.config.AuthDefaultSource}
- *
- * @author timfruit
- */
-public enum AuthExtendSource implements AuthSource {
-
-    /**
-     * 微信小程序授权登录
-     */
-    WECHAT_MINI_APP {
-
-        @Override
-        public String authorize() {
-            // 参见 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html 文档
-            throw new UnsupportedOperationException("不支持获取授权 url,请使用小程序内置函数 wx.login() 登录获取 code");
-        }
-
-        @Override
-        public String accessToken() {
-            // 参见 https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html 文档
-            // 获取 openid, unionId , session_key 等字段
-            return "https://api.weixin.qq.com/sns/jscode2session";
-        }
-
-        @Override
-        public String userInfo() {
-            // 参见 https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserProfile.html 文档
-            throw new UnsupportedOperationException("不支持获取用户信息 url,请使用小程序内置函数 wx.getUserProfile() 获取用户信息");
-        }
-
-        @Override
-        public Class<? extends AuthDefaultRequest> getTargetClass() {
-            return null;
-        }
-    }
-
-}

+ 0 - 97
yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/request/AuthWeChatMiniAppRequest.java

@@ -1,97 +0,0 @@
-package cn.iocoder.yudao.framework.social.core.request;
-
-import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
-import cn.iocoder.yudao.framework.social.core.enums.AuthExtendSource;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.xingyuv.jushauth.cache.AuthStateCache;
-import com.xingyuv.jushauth.config.AuthConfig;
-import com.xingyuv.jushauth.exception.AuthException;
-import com.xingyuv.jushauth.model.AuthCallback;
-import com.xingyuv.jushauth.model.AuthToken;
-import com.xingyuv.jushauth.model.AuthUser;
-import com.xingyuv.jushauth.request.AuthDefaultRequest;
-import com.xingyuv.jushauth.utils.HttpUtils;
-import com.xingyuv.jushauth.utils.UrlBuilder;
-import lombok.Data;
-
-/**
- * 微信小程序登陆 Request 请求
- *
- * 由于 JustAuth 定位是面向 Web 为主的三方登录,所以微信小程序只能自己封装
- *
- * @author timfruit
- * @date 2021-10-29
- */
-public class AuthWeChatMiniAppRequest extends AuthDefaultRequest {
-
-    public AuthWeChatMiniAppRequest(AuthConfig config, AuthStateCache authStateCache) {
-        super(config, AuthExtendSource.WECHAT_MINI_APP, authStateCache);
-    }
-
-    @Override
-    protected AuthToken getAccessToken(AuthCallback authCallback) {
-        // 参见 https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html 文档
-        // 使用 code 获取对应的 openId、unionId 等字段
-        String response = new HttpUtils(config.getHttpConfig()).get(accessTokenUrl(authCallback.getCode())).getBody();
-        JSCode2SessionResponse accessTokenObject = JsonUtils.parseObject(response, JSCode2SessionResponse.class);
-        assert accessTokenObject != null;
-        checkResponse(accessTokenObject);
-        // 拼装结果
-        return AuthToken.builder()
-                .openId(accessTokenObject.getOpenid())
-                .unionId(accessTokenObject.getUnionId())
-                .build();
-    }
-
-    @Override
-    protected AuthUser getUserInfo(AuthToken authToken) {
-        // 参见 https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserProfile.html 文档
-        // 如果需要用户信息,需要在小程序调用函数后传给后端
-        return AuthUser.builder()
-                .username("")
-                .nickname("")
-                .avatar("")
-                .uuid(authToken.getOpenId())
-                .token(authToken)
-                .source(source.toString())
-                .build();
-    }
-
-    /**
-     * 检查响应内容是否正确
-     *
-     * @param response 请求响应内容
-     */
-    private void checkResponse(JSCode2SessionResponse response) {
-        if (response.getErrorCode() != 0) {
-            throw new AuthException(response.getErrorCode(), response.getErrorMsg());
-        }
-    }
-
-    @Override
-    protected String accessTokenUrl(String code) {
-        return UrlBuilder.fromBaseUrl(source.accessToken())
-                .queryParam("appid", config.getClientId())
-                .queryParam("secret", config.getClientSecret())
-                .queryParam("js_code", code)
-                .queryParam("grant_type", "authorization_code")
-                .build();
-    }
-
-    @Data
-    @SuppressWarnings("SpellCheckingInspection")
-    private static class JSCode2SessionResponse {
-
-        @JsonProperty("errcode")
-        private int errorCode;
-        @JsonProperty("errmsg")
-        private String errorMsg;
-        @JsonProperty("session_key")
-        private String sessionKey;
-        private String openid;
-        @JsonProperty("unionid")
-        private String unionId;
-
-    }
-
-}

+ 0 - 178
yudao-framework/yudao-spring-boot-starter-biz-social/src/main/java/cn/iocoder/yudao/framework/social/core/request/AuthWeChatMpRequest.java

@@ -1,178 +0,0 @@
-package cn.iocoder.yudao.framework.social.core.request;
-
-import com.alibaba.fastjson.JSONObject;
-import com.xingyuv.jushauth.cache.AuthStateCache;
-import com.xingyuv.jushauth.config.AuthConfig;
-import com.xingyuv.jushauth.config.AuthDefaultSource;
-import com.xingyuv.jushauth.enums.AuthResponseStatus;
-import com.xingyuv.jushauth.enums.AuthUserGender;
-import com.xingyuv.jushauth.enums.scope.AuthWechatMpScope;
-import com.xingyuv.jushauth.exception.AuthException;
-import com.xingyuv.jushauth.model.AuthCallback;
-import com.xingyuv.jushauth.model.AuthResponse;
-import com.xingyuv.jushauth.model.AuthToken;
-import com.xingyuv.jushauth.model.AuthUser;
-import com.xingyuv.jushauth.request.AuthDefaultRequest;
-import com.xingyuv.jushauth.utils.AuthScopeUtils;
-import com.xingyuv.jushauth.utils.GlobalAuthUtils;
-import com.xingyuv.jushauth.utils.HttpUtils;
-import com.xingyuv.jushauth.utils.UrlBuilder;
-
-/**
- * 微信公众平台登录
- *
- * @author yangkai.shen (https://xkcoding.com)
- * @since 1.1.0
- */
-public class AuthWeChatMpRequest extends AuthDefaultRequest {
-    public AuthWeChatMpRequest(AuthConfig config) {
-        super(config, AuthDefaultSource.WECHAT_MP);
-    }
-
-    public AuthWeChatMpRequest(AuthConfig config, AuthStateCache authStateCache) {
-        super(config, AuthDefaultSource.WECHAT_MP, authStateCache);
-    }
-
-    /**
-     * 微信的特殊性,此时返回的信息同时包含 openid 和 access_token
-     *
-     * @param authCallback 回调返回的参数
-     * @return 所有信息
-     */
-    @Override
-    protected AuthToken getAccessToken(AuthCallback authCallback) {
-        return this.getToken(accessTokenUrl(authCallback.getCode()));
-    }
-
-    @Override
-    protected AuthUser getUserInfo(AuthToken authToken) {
-        String openId = authToken.getOpenId();
-
-        String response = doGetUserInfo(authToken);
-        JSONObject object = JSONObject.parseObject(response);
-
-        this.checkResponse(object);
-
-        String location = String.format("%s-%s-%s", object.getString("country"), object.getString("province"), object.getString("city"));
-
-        if (object.containsKey("unionid")) {
-            authToken.setUnionId(object.getString("unionid"));
-        }
-
-        return AuthUser.builder()
-                .rawUserInfo(object)
-                .username(object.getString("nickname"))
-                .nickname(object.getString("nickname"))
-                .avatar(object.getString("headimgurl"))
-                .location(location)
-                .uuid(openId)
-                .gender(AuthUserGender.getWechatRealGender(object.getString("sex")))
-                .token(authToken)
-                .source(source.toString())
-                .build();
-    }
-
-    @Override
-    public AuthResponse refresh(AuthToken oldToken) {
-        return AuthResponse.builder()
-                .code(AuthResponseStatus.SUCCESS.getCode())
-                .data(this.getToken(refreshTokenUrl(oldToken.getRefreshToken())))
-                .build();
-    }
-
-    /**
-     * 检查响应内容是否正确
-     *
-     * @param object 请求响应内容
-     */
-    private void checkResponse(JSONObject object) {
-        if (object.containsKey("errcode")) {
-            throw new AuthException(object.getIntValue("errcode"), object.getString("errmsg"));
-        }
-    }
-
-    /**
-     * 获取token,适用于获取access_token和刷新token
-     *
-     * @param accessTokenUrl 实际请求token的地址
-     * @return token对象
-     */
-    private AuthToken getToken(String accessTokenUrl) {
-        String response = new HttpUtils(config.getHttpConfig()).get(accessTokenUrl).getBody();
-        JSONObject accessTokenObject = JSONObject.parseObject(response);
-
-        this.checkResponse(accessTokenObject);
-
-        return AuthToken.builder()
-                .accessToken(accessTokenObject.getString("access_token"))
-                .refreshToken(accessTokenObject.getString("refresh_token"))
-                .expireIn(accessTokenObject.getIntValue("expires_in"))
-                .openId(accessTokenObject.getString("openid"))
-                .scope(accessTokenObject.getString("scope"))
-                .build();
-    }
-
-    /**
-     * 返回带{@code state}参数的授权url,授权回调时会带上这个{@code state}
-     *
-     * @param state state 验证授权流程的参数,可以防止csrf
-     * @return 返回授权地址
-     * @since 1.9.3
-     */
-    @Override
-    public String authorize(String state) {
-        return UrlBuilder.fromBaseUrl(source.authorize())
-                .queryParam("appid", config.getClientId())
-                .queryParam("redirect_uri", GlobalAuthUtils.urlEncode(config.getRedirectUri()))
-                .queryParam("response_type", "code")
-                .queryParam("scope", this.getScopes(",", false, AuthScopeUtils.getDefaultScopes(AuthWechatMpScope.values())))
-                .queryParam("state", getRealState(state).concat("#wechat_redirect"))
-                .build();
-    }
-
-    /**
-     * 返回获取accessToken的url
-     *
-     * @param code 授权码
-     * @return 返回获取accessToken的url
-     */
-    @Override
-    protected String accessTokenUrl(String code) {
-        return UrlBuilder.fromBaseUrl(source.accessToken())
-                .queryParam("appid", config.getClientId())
-                .queryParam("secret", config.getClientSecret())
-                .queryParam("code", code)
-                .queryParam("grant_type", "authorization_code")
-                .build();
-    }
-
-    /**
-     * 返回获取userInfo的url
-     *
-     * @param authToken 用户授权后的token
-     * @return 返回获取userInfo的url
-     */
-    @Override
-    protected String userInfoUrl(AuthToken authToken) {
-        return UrlBuilder.fromBaseUrl(source.userInfo())
-                .queryParam("access_token", authToken.getAccessToken())
-                .queryParam("openid", authToken.getOpenId())
-                .queryParam("lang", "zh_CN")
-                .build();
-    }
-
-    /**
-     * 返回获取userInfo的url
-     *
-     * @param refreshToken getAccessToken方法返回的refreshToken
-     * @return 返回获取userInfo的url
-     */
-    @Override
-    protected String refreshTokenUrl(String refreshToken) {
-        return UrlBuilder.fromBaseUrl(source.refresh())
-                .queryParam("appid", config.getClientId())
-                .queryParam("grant_type", "refresh_token")
-                .queryParam("refresh_token", refreshToken)
-                .build();
-    }
-}

+ 0 - 1
yudao-framework/yudao-spring-boot-starter-biz-social/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -1 +0,0 @@
-cn.iocoder.yudao.framework.social.config.YudaoSocialAutoConfiguration

+ 0 - 45
yudao-framework/yudao-spring-boot-starter-biz-weixin/pom.xml

@@ -1,45 +0,0 @@
-<?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>
-        <groupId>cn.iocoder.boot</groupId>
-        <artifactId>yudao-framework</artifactId>
-        <version>${revision}</version>
-    </parent>
-    <modelVersion>4.0.0</modelVersion>
-    <artifactId>yudao-spring-boot-starter-biz-weixin</artifactId>
-    <packaging>jar</packaging>
-
-    <name>${project.artifactId}</name>
-    <description>微信拓展
-        1. 基于 weixin-java-mp 库,对接微信公众号平台。目前主要解决微信公众号的支付场景。
-        2. 基于 weixin-java-miniapp 库,对接微信小程序。目前主要解决微信小程序的一键登录场景。
-    </description>
-    <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
-
-    <dependencies>
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-common</artifactId>
-        </dependency>
-
-        <!-- Test 测试相关 -->
-        <dependency>
-            <groupId>cn.iocoder.boot</groupId>
-            <artifactId>yudao-spring-boot-starter-test</artifactId>
-            <scope>test</scope>
-        </dependency>
-
-        <!-- 三方云服务相关 -->
-        <dependency>
-            <groupId>com.github.binarywang</groupId>
-            <artifactId>wx-java-mp-spring-boot-starter</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>com.github.binarywang</groupId>
-            <artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
-        </dependency>
-    </dependencies>
-
-</project>

+ 0 - 7
yudao-framework/yudao-spring-boot-starter-biz-weixin/src/main/java/cn/iocoder/yudao/framework/weixin/package-info.java

@@ -1,7 +0,0 @@
-/**
- * 微信拓展
- * 1. 基于 weixin-java-mp 库,对接微信公众号平台。目前主要解决微信公众号的支付场景。
- * 2. 基于 weixin-java-miniapp 库,对接微信小程序。目前主要解决微信小程序的一键登录场景。
- */
-package cn.iocoder.yudao.framework.weixin;
-

+ 0 - 34
yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/java/cn/iocoder/yudao/framework/weixin/WxMpServiceTest.java

@@ -1,34 +0,0 @@
-package cn.iocoder.yudao.framework.weixin;
-
-import me.chanjar.weixin.common.error.WxErrorException;
-import me.chanjar.weixin.mp.api.WxMpService;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.test.context.SpringBootTest;
-
-import javax.annotation.Resource;
-
-@SpringBootTest(classes = WxMpServiceTest.Application.class)
-public class WxMpServiceTest {
-
-    @Resource
-    private WxMpService wxMpService;
-
-    @Test
-    public void testGetAccessToken() throws WxErrorException {
-        String accessToken = wxMpService.getAccessToken();
-        System.out.println(accessToken);
-    }
-
-    @Test
-    public void testGet() throws WxErrorException {
-        String jsapiTicket = wxMpService.getJsapiTicket();
-        System.out.println(jsapiTicket);
-    }
-
-    @SpringBootApplication
-    public static class Application {
-
-    }
-
-}

+ 0 - 11
yudao-framework/yudao-spring-boot-starter-biz-weixin/src/test-integration/resources/application.yml

@@ -1,11 +0,0 @@
---- #################### 微信公众号相关配置 ####################
-wx: # 参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档
-  mp:
-    # 公众号配置(必填)
-    app-id: wx041349c6f39b268b
-    secret: 5abee519483bc9f8cb37ce280e814bd0
-    # 存储配置,解决 AccessToken 的跨节点的共享
-#    config-storage:
-#      type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取
-#      key-prefix: wx # Redis Key 的前缀 TODO 芋艿:解决下 Redis key 管理的配置
-#      http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台

+ 40 - 0
yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/context/FlowableContextHolder.java

@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.framework.flowable.core.context;
+
+import cn.hutool.core.collection.CollUtil;
+import com.alibaba.ttl.TransmittableThreadLocal;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 工作流--用户用到的上下文相关信息
+ */
+public class FlowableContextHolder {
+
+    private static final ThreadLocal<Map<String, List<Long>>> ASSIGNEE = new TransmittableThreadLocal<>();
+
+    /**
+     * 通过流程任务的定义 key ,拿到提前选好的审批人
+     * 此方法目的:首次创建流程实例时,数据库中还查询不到 assignee 字段,所以存入上下文中获取
+     *
+     * @param taskDefinitionKey 流程任务 key
+     * @return 审批人 ID 集合
+     */
+    public static List<Long> getAssigneeByTaskDefinitionKey(String taskDefinitionKey) {
+        if (CollUtil.isNotEmpty(ASSIGNEE.get())) {
+            return ASSIGNEE.get().get(taskDefinitionKey);
+        }
+        return Collections.emptyList();
+    }
+
+    /**
+     * 存入提前选好的审批人到上下文线程变量中
+     *
+     * @param assignee 流程任务 key -> 审批人 ID 炅和
+     */
+    public static void setAssignee(Map<String, List<Long>> assignee) {
+        ASSIGNEE.set(assignee);
+    }
+
+}

+ 3 - 16
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQAutoConfiguration.java → yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java

@@ -5,7 +5,6 @@ import cn.hutool.core.util.StrUtil;
 import cn.hutool.system.SystemUtil;
 import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
 import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
-import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
 import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob;
 import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
 import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
@@ -23,7 +22,6 @@ import org.springframework.data.redis.connection.stream.ReadOffset;
 import org.springframework.data.redis.connection.stream.StreamOffset;
 import org.springframework.data.redis.core.RedisCallback;
 import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.data.redis.listener.ChannelTopic;
 import org.springframework.data.redis.listener.RedisMessageListenerContainer;
 import org.springframework.data.redis.stream.StreamMessageListenerContainer;
@@ -33,30 +31,19 @@ import java.util.List;
 import java.util.Properties;
 
 /**
- * 消息队列配置类
+ * Redis 消息队列 Consumer 配置类
  *
  * @author 芋道源码
  */
 @Slf4j
 @EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息
 @AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
-public class YudaoRedisMQAutoConfiguration {
-
-    @Bean
-    public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate,
-                                           List<RedisMessageInterceptor> interceptors) {
-        RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate);
-        // 添加拦截器
-        interceptors.forEach(redisMQTemplate::addInterceptor);
-        return redisMQTemplate;
-    }
-
-    // ========== 消费者相关 ==========
+public class YudaoRedisMQConsumerAutoConfiguration {
 
     /**
      * 创建 Redis Pub/Sub 广播消费的容器
      */
-    @Bean(initMethod = "start", destroyMethod = "stop")
+    @Bean
     @ConditionalOnBean(AbstractRedisChannelMessageListener.class) // 只有 AbstractChannelMessageListener 存在的时候,才需要注册 Redis pubsub 监听
     public RedisMessageListenerContainer redisMessageListenerContainer(
             RedisMQTemplate redisMQTemplate, List<AbstractRedisChannelMessageListener<?>> listeners) {

+ 31 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQProducerAutoConfiguration.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.framework.mq.redis.config;
+
+import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
+import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
+import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.core.StringRedisTemplate;
+
+import java.util.List;
+
+/**
+ * Redis 消息队列 Producer 配置类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+@AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
+public class YudaoRedisMQProducerAutoConfiguration {
+
+    @Bean
+    public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate,
+                                           List<RedisMessageInterceptor> interceptors) {
+        RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate);
+        // 添加拦截器
+        interceptors.forEach(redisMQTemplate::addInterceptor);
+        return redisMQTemplate;
+    }
+
+}

+ 2 - 1
yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -1,2 +1,3 @@
-cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQAutoConfiguration
+cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQProducerAutoConfiguration
+cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration
 cn.iocoder.yudao.framework.mq.rabbitmq.config.YudaoRabbitMQAutoConfiguration

+ 9 - 1
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java

@@ -12,6 +12,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
 import com.baomidou.mybatisplus.extension.toolkit.Db;
 import com.github.yulichang.base.MPJBaseMapper;
+import com.github.yulichang.interfaces.MPJBaseJoin;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.Collection;
@@ -27,7 +28,7 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
 
     default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
         // 特殊:不分页,直接查询全部
-        if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageNo())) {
+        if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
             List<T> list = selectList(queryWrapper);
             return new PageResult<>(list, (long) list.size());
         }
@@ -39,6 +40,13 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
         return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
     }
 
+    default <DTO> PageResult<DTO> selectJoinPage(PageParam pageParam, Class<DTO> resultTypeClass, MPJBaseJoin<T> joinQueryWrapper) {
+        IPage<DTO> mpPage = MyBatisUtils.buildPage(pageParam);
+        selectJoinPage(mpPage, resultTypeClass, joinQueryWrapper);
+        // 转换返回
+        return new PageResult<>(mpPage.getRecords(), mpPage.getTotal());
+    }
+
     default T selectOne(String field, Object value) {
         return selectOne(new QueryWrapper<T>().eq(field, value));
     }

+ 7 - 0
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java

@@ -19,6 +19,13 @@ public class SecurityProperties {
      */
     @NotEmpty(message = "Token Header 不能为空")
     private String tokenHeader = "Authorization";
+    /**
+     * HTTP 请求时,访问令牌的请求参数
+     *
+     * 初始目的:解决 WebSocket 无法通过 header 传参,只能通过 token 参数拼接
+     */
+    @NotEmpty(message = "Token Parameter 不能为空")
+    private String tokenParameter = "token";
 
     /**
      * mock 模式的开关

+ 11 - 2
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.framework.security.config;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
 import cn.iocoder.yudao.framework.web.config.WebProperties;
 import com.google.common.collect.HashMultimap;
@@ -17,6 +18,7 @@ import org.springframework.security.web.AuthenticationEntryPoint;
 import org.springframework.security.web.SecurityFilterChain;
 import org.springframework.security.web.access.AccessDeniedHandler;
 import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.method.HandlerMethod;
 import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
 import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
@@ -129,8 +131,6 @@ public class YudaoWebSecurityConfigurerAdapter {
                 .antMatchers(buildAppApi("/**")).permitAll()
                 // 1.5 验证码captcha 允许匿名访问
                 .antMatchers("/captcha/get", "/captcha/check").permitAll()
-                // 1.6 webSocket 允许匿名访问
-                .antMatchers("/websocket/message").permitAll()
                 // ②:每个项目的自定义规则
                 .and().authorizeRequests(registry -> // 下面,循环设置自定义规则
                         authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry)))
@@ -164,6 +164,15 @@ public class YudaoWebSecurityConfigurerAdapter {
                 continue;
             }
             Set<String> urls = entry.getKey().getPatternsCondition().getPatterns();
+            // 特殊:使用 @RequestMapping 注解,并且未写 method 属性,此时认为都需要免登录
+            Set<RequestMethod> methods = entry.getKey().getMethodsCondition().getMethods();
+            if (CollUtil.isEmpty(methods)) { //
+                result.putAll(HttpMethod.GET, urls);
+                result.putAll(HttpMethod.POST, urls);
+                result.putAll(HttpMethod.PUT, urls);
+                result.putAll(HttpMethod.DELETE, urls);
+                continue;
+            }
             // 根据请求方法,添加到 result 结果
             entry.getKey().getMethodsCondition().getMethods().forEach(requestMethod -> {
                 switch (requestMethod) {

+ 6 - 2
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java

@@ -41,7 +41,8 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
     @SuppressWarnings("NullableProblems")
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
             throws ServletException, IOException {
-        String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader());
+        String token = SecurityFrameworkUtils.obtainAuthorization(request,
+                securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
         if (StrUtil.isNotEmpty(token)) {
             Integer userType = WebFrameworkUtils.getLoginUserType(request);
             try {
@@ -74,7 +75,10 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
                 return null;
             }
             // 用户类型不匹配,无权限
-            if (ObjectUtil.notEqual(accessToken.getUserType(), userType)) {
+            // 注意:只有 /admin-api/* 和 /app-api/* 有 userType,才需要比对用户类型
+            // 类似 WebSocket 的 /ws/* 连接地址,是不需要比对用户类型的
+            if (userType != null
+                    && ObjectUtil.notEqual(accessToken.getUserType(), userType)) {
                 throw new AccessDeniedException("错误的用户类型");
             }
             // 构建登录用户

+ 16 - 8
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.framework.security.core.util;
 
+import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.security.core.LoginUser;
 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
 import org.springframework.lang.Nullable;
@@ -20,6 +21,9 @@ import java.util.Collections;
  */
 public class SecurityFrameworkUtils {
 
+    /**
+     * HEADER 认证头 value 的前缀
+     */
     public static final String AUTHORIZATION_BEARER = "Bearer";
 
     private SecurityFrameworkUtils() {}
@@ -28,19 +32,23 @@ public class SecurityFrameworkUtils {
      * 从请求中,获得认证 Token
      *
      * @param request 请求
-     * @param header 认证 Token 对应的 Header 名字
+     * @param headerName 认证 Token 对应的 Header 名字
+     * @param parameterName 认证 Token 对应的 Parameter 名字
      * @return 认证 Token
      */
-    public static String obtainAuthorization(HttpServletRequest request, String header) {
-        String authorization = request.getHeader(header);
-        if (!StringUtils.hasText(authorization)) {
-            return null;
+    public static String obtainAuthorization(HttpServletRequest request,
+                                             String headerName, String parameterName) {
+        // 1. 获得 Token。优先级:Header > Parameter
+        String token = request.getHeader(headerName);
+        if (StrUtil.isEmpty(token)) {
+            token = request.getParameter(parameterName);
         }
-        int index = authorization.indexOf(AUTHORIZATION_BEARER + " ");
-        if (index == -1) { // 未找到
+        if (!StringUtils.hasText(token)) {
             return null;
         }
-        return authorization.substring(index + 7).trim();
+        // 2. 去除 Token 中带的 Bearer
+        int index = token.indexOf(AUTHORIZATION_BEARER + " ");
+        return index >= 0 ? token.substring(index + 7).trim() : token;
     }
 
     /**

+ 49 - 2
yudao-framework/yudao-spring-boot-starter-websocket/pom.xml

@@ -12,26 +12,73 @@
     <packaging>jar</packaging>
 
     <name>${project.artifactId}</name>
-    <description>WebSocket</description>
+    <description>WebSocket 框架,支持多节点的广播</description>
     <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
 
 
     <dependencies>
-
         <dependency>
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-common</artifactId>
         </dependency>
 
+        <!-- Web 相关 -->
         <dependency>
+            <!-- 为什么是 websocket 依赖 security 呢?而不是 security 拓展 websocket 呢?
+                 因为 websocket 和 LoginUser 当前登录的用户有一定的相关性,具体可见 WebSocketSessionManagerImpl 逻辑。
+                 如果让 security 拓展 websocket 的话,会导致 websocket 组件的封装很散,进而增大理解成本。
+            -->
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-security</artifactId>
+            <scope>provided</scope>
         </dependency>
 
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-websocket</artifactId>
         </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <!-- 为什么是 websocket 依赖 security 呢?而不是 security 拓展 websocket 呢?
+                 因为 websocket 和 LoginUser 当前登录的用户有一定的相关性,具体可见 WebSocketSessionManagerImpl 逻辑。
+                 如果让 security 拓展 websocket 的话,会导致 websocket 组件的封装很散,进而增大理解成本。
+            -->
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- 消息队列相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-mq</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.kafka</groupId>
+            <artifactId>spring-kafka</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.amqp</groupId>
+            <artifactId>spring-rabbit</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.rocketmq</groupId>
+            <artifactId>rocketmq-spring-boot-starter</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <!-- 业务组件 -->
+        <dependency>
+            <!-- 为什么要依赖 tenant 组件?
+                因为广播某个类型的用户时候,需要根据租户过滤下,避免广播到别的租户!
+            -->
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
 
 </project>

+ 0 - 14
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java

@@ -1,14 +0,0 @@
-package cn.iocoder.yudao.framework.websocket.config;
-
-import cn.iocoder.yudao.framework.websocket.core.UserHandshakeInterceptor;
-import org.springframework.boot.context.properties.EnableConfigurationProperties;
-import org.springframework.context.annotation.Bean;
-import org.springframework.web.socket.server.HandshakeInterceptor;
-
-@EnableConfigurationProperties(WebSocketProperties.class)
-public class WebSocketHandlerConfig {
-    @Bean
-    public HandshakeInterceptor handshakeInterceptor() {
-        return new UserHandshakeInterceptor();
-    }
-}

+ 13 - 8
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java

@@ -4,6 +4,9 @@ import lombok.Data;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.validation.annotation.Validated;
 
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
 /**
  * WebSocket 配置项
  *
@@ -15,15 +18,17 @@ import org.springframework.validation.annotation.Validated;
 public class WebSocketProperties {
 
     /**
-     * 路径
-     */
-    private String path = "";
-    /**
-     * 默认最多允许同时在线用户数
+     * WebSocket 的连接路径
      */
-    private int maxOnlineCount = 0;
+    @NotEmpty(message = "WebSocket 的连接路径不能为空")
+    private String path = "/ws";
+
     /**
-     * 是否保存session
+     * 消息发送器的类型
+     *
+     * 可选值:local、redis、rocketmq、kafka、rabbitmq
      */
-    private boolean sessionMap = true;
+    @NotNull(message = "WebSocket 的消息发送者不能为空")
+    private String senderType = "local";
+
 }

+ 152 - 9
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java

@@ -1,11 +1,34 @@
 package cn.iocoder.yudao.framework.websocket.config;
 
+import cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration;
+import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
+import cn.iocoder.yudao.framework.websocket.core.handler.JsonWebSocketMessageHandler;
+import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
+import cn.iocoder.yudao.framework.websocket.core.security.LoginUserHandshakeInterceptor;
+import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer;
+import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.local.LocalWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageConsumer;
+import cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageConsumer;
+import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageConsumer;
+import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionHandlerDecorator;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManagerImpl;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.amqp.core.TopicExchange;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.autoconfigure.AutoConfiguration;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.core.KafkaTemplate;
 import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
 import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
 import org.springframework.web.socket.server.HandshakeInterceptor;
 
@@ -16,19 +39,139 @@ import java.util.List;
  *
  * @author xingyu4j
  */
-@AutoConfiguration
-// 允许使用 yudao.websocket.enable=false 禁用websocket
-@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true)
+@AutoConfiguration(before = YudaoRedisMQConsumerAutoConfiguration.class) // before YudaoRedisMQConsumerAutoConfiguration 的原因是,需要保证 RedisWebSocketMessageConsumer 先创建,才能创建 RedisMessageListenerContainer
+@EnableWebSocket // 开启 websocket
+@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true) // 允许使用 yudao.websocket.enable=false 禁用 websocket
 @EnableConfigurationProperties(WebSocketProperties.class)
 public class YudaoWebSocketAutoConfiguration {
+
     @Bean
-    @ConditionalOnMissingBean
-    public WebSocketConfigurer webSocketConfigurer(List<HandshakeInterceptor> handshakeInterceptor,
+    public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors,
                                                    WebSocketHandler webSocketHandler,
                                                    WebSocketProperties webSocketProperties) {
-
         return registry -> registry
+                // 添加 WebSocketHandler
                 .addHandler(webSocketHandler, webSocketProperties.getPath())
-                .addInterceptors(handshakeInterceptor.toArray(new HandshakeInterceptor[0]));
+                .addInterceptors(handshakeInterceptors)
+                // 允许跨域,否则前端连接会直接断开
+                .setAllowedOriginPatterns("*");
+    }
+
+    @Bean
+    public HandshakeInterceptor handshakeInterceptor() {
+        return new LoginUserHandshakeInterceptor();
+    }
+
+    @Bean
+    public WebSocketHandler webSocketHandler(WebSocketSessionManager sessionManager,
+                                             List<? extends WebSocketMessageListener<?>> messageListeners) {
+        // 1. 创建 JsonWebSocketMessageHandler 对象,处理消息
+        JsonWebSocketMessageHandler messageHandler = new JsonWebSocketMessageHandler(messageListeners);
+        // 2. 创建 WebSocketSessionHandlerDecorator 对象,处理连接
+        return new WebSocketSessionHandlerDecorator(messageHandler, sessionManager);
+    }
+
+    @Bean
+    public WebSocketSessionManager webSocketSessionManager() {
+        return new WebSocketSessionManagerImpl();
+    }
+
+    // ==================== Sender 相关 ====================
+
+    @Configuration
+    @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "local", matchIfMissing = true)
+    public class LocalWebSocketMessageSenderConfiguration {
+
+        @Bean
+        public LocalWebSocketMessageSender localWebSocketMessageSender(WebSocketSessionManager sessionManager) {
+            return new LocalWebSocketMessageSender(sessionManager);
+        }
+
+    }
+
+    @Configuration
+    @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "redis", matchIfMissing = true)
+    public class RedisWebSocketMessageSenderConfiguration {
+
+        @Bean
+        public RedisWebSocketMessageSender redisWebSocketMessageSender(WebSocketSessionManager sessionManager,
+                                                                       RedisMQTemplate redisMQTemplate) {
+            return new RedisWebSocketMessageSender(sessionManager, redisMQTemplate);
+        }
+
+        @Bean
+        public RedisWebSocketMessageConsumer redisWebSocketMessageConsumer(
+                RedisWebSocketMessageSender redisWebSocketMessageSender) {
+            return new RedisWebSocketMessageConsumer(redisWebSocketMessageSender);
+        }
+
+    }
+
+    @Configuration
+    @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rocketmq", matchIfMissing = true)
+    public class RocketMQWebSocketMessageSenderConfiguration {
+
+        @Bean
+        public RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender(
+                WebSocketSessionManager sessionManager, RocketMQTemplate rocketMQTemplate,
+                @Value("${yudao.websocket.sender-rocketmq.topic}") String topic) {
+            return new RocketMQWebSocketMessageSender(sessionManager, rocketMQTemplate, topic);
+        }
+
+        @Bean
+        public RocketMQWebSocketMessageConsumer rocketMQWebSocketMessageConsumer(
+                RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender) {
+            return new RocketMQWebSocketMessageConsumer(rocketMQWebSocketMessageSender);
+        }
+
     }
-}
+
+    @Configuration
+    @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rabbitmq", matchIfMissing = true)
+    public class RabbitMQWebSocketMessageSenderConfiguration {
+
+        @Bean
+        public RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender(
+                WebSocketSessionManager sessionManager, RabbitTemplate rabbitTemplate,
+                TopicExchange websocketTopicExchange) {
+            return new RabbitMQWebSocketMessageSender(sessionManager, rabbitTemplate, websocketTopicExchange);
+        }
+
+        @Bean
+        public RabbitMQWebSocketMessageConsumer rabbitMQWebSocketMessageConsumer(
+                RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender) {
+            return new RabbitMQWebSocketMessageConsumer(rabbitMQWebSocketMessageSender);
+        }
+
+        /**
+         * 创建 Topic Exchange
+         */
+        @Bean
+        public TopicExchange websocketTopicExchange(@Value("${yudao.websocket.sender-rabbitmq.exchange}") String exchange) {
+            return new TopicExchange(exchange,
+                    true,  // durable: 是否持久化
+                    false);  // exclusive: 是否排它
+        }
+
+    }
+
+    @Configuration
+    @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "kafka", matchIfMissing = true)
+    public class KafkaWebSocketMessageSenderConfiguration {
+
+        @Bean
+        public KafkaWebSocketMessageSender kafkaWebSocketMessageSender(
+                WebSocketSessionManager sessionManager, KafkaTemplate<Object, Object> kafkaTemplate,
+                @Value("${yudao.websocket.sender-kafka.topic}") String topic) {
+            return new KafkaWebSocketMessageSender(sessionManager, kafkaTemplate, topic);
+        }
+
+        @Bean
+        public KafkaWebSocketMessageConsumer kafkaWebSocketMessageConsumer(
+                KafkaWebSocketMessageSender kafkaWebSocketMessageSender) {
+            return new KafkaWebSocketMessageConsumer(kafkaWebSocketMessageSender);
+        }
+
+    }
+
+}

+ 0 - 24
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java

@@ -1,24 +0,0 @@
-package cn.iocoder.yudao.framework.websocket.core;
-
-import cn.iocoder.yudao.framework.security.core.LoginUser;
-import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
-import org.springframework.http.server.ServerHttpRequest;
-import org.springframework.http.server.ServerHttpResponse;
-import org.springframework.web.socket.WebSocketHandler;
-import org.springframework.web.socket.server.HandshakeInterceptor;
-
-import java.util.Map;
-
-public class UserHandshakeInterceptor implements HandshakeInterceptor {
-    @Override
-    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
-        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
-        attributes.put(WebSocketKeyDefine.LOGIN_USER, loginUser);
-        return true;
-    }
-
-    @Override
-    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
-
-    }
-}

+ 0 - 9
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java

@@ -1,9 +0,0 @@
-package cn.iocoder.yudao.framework.websocket.core;
-
-
-import lombok.Data;
-
-@Data
-public class WebSocketKeyDefine {
-    public static final String LOGIN_USER ="LOGIN_USER";
-}

+ 0 - 24
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java

@@ -1,24 +0,0 @@
-package cn.iocoder.yudao.framework.websocket.core;
-
-import lombok.Data;
-import lombok.experimental.Accessors;
-
-import java.util.List;
-
-@Data
-@Accessors(chain = true)
-public class WebSocketMessageDO {
-    /**
-     * 接收消息的seesion
-     */
-    private List<Object> seesionKeyList;
-    /**
-     * 发送消息
-     */
-    private String msgText;
-
-    public static WebSocketMessageDO build(List<Object> seesionKeyList, String msgText) {
-        return new WebSocketMessageDO().setMsgText(msgText).setSeesionKeyList(seesionKeyList);
-    }
-
-}

+ 0 - 36
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java

@@ -1,36 +0,0 @@
-package cn.iocoder.yudao.framework.websocket.core;
-
-import org.springframework.web.socket.WebSocketSession;
-
-import java.util.Collection;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-public final class WebSocketSessionHandler {
-    private WebSocketSessionHandler() {
-    }
-
-    private static final Map<String, WebSocketSession> SESSION_MAP = new ConcurrentHashMap<>();
-
-    public static void addSession(Object sessionKey, WebSocketSession session) {
-        SESSION_MAP.put(sessionKey.toString(), session);
-    }
-
-    public static void removeSession(Object sessionKey) {
-        SESSION_MAP.remove(sessionKey.toString());
-    }
-
-    public static WebSocketSession getSession(Object sessionKey) {
-        return SESSION_MAP.get(sessionKey.toString());
-    }
-
-    public static Collection<WebSocketSession> getSessions() {
-        return SESSION_MAP.values();
-    }
-
-    public static Set<String> getSessionKeys() {
-        return SESSION_MAP.keySet();
-    }
-
-}

+ 0 - 31
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java

@@ -1,31 +0,0 @@
-package cn.iocoder.yudao.framework.websocket.core;
-
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.web.socket.TextMessage;
-import org.springframework.web.socket.WebSocketSession;
-
-import java.io.IOException;
-
-@Slf4j
-public class WebSocketUtils {
-    public static boolean sendMessage(WebSocketSession seesion, String message) {
-        if (seesion == null) {
-            log.error("seesion 不存在");
-            return false;
-        }
-        if (seesion.isOpen()) {
-            try {
-                seesion.sendMessage(new TextMessage(message));
-            } catch (IOException e) {
-                log.error("WebSocket 消息发送异常 Session={} | msg= {} | exception={}", seesion, message, e);
-                return false;
-            }
-        }
-        return true;
-    }
-
-    public static boolean sendMessage(Object sessionKey, String message) {
-        WebSocketSession session = WebSocketSessionHandler.getSession(sessionKey);
-        return sendMessage(session, message);
-    }
-}

+ 0 - 49
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java

@@ -1,49 +0,0 @@
-package cn.iocoder.yudao.framework.websocket.core;
-
-import cn.iocoder.yudao.framework.security.core.LoginUser;
-import org.springframework.web.socket.CloseStatus;
-import org.springframework.web.socket.WebSocketHandler;
-import org.springframework.web.socket.WebSocketSession;
-import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
-
-public class YudaoWebSocketHandlerDecorator extends WebSocketHandlerDecorator {
-    public YudaoWebSocketHandlerDecorator(WebSocketHandler delegate) {
-        super(delegate);
-    }
-
-    /**
-     * websocket 连接时执行的动作
-     * @param session websocket session 对象
-     * @throws Exception 异常对象
-     */
-    @Override
-    public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
-        Object sessionKey = sessionKeyGen(session);
-        WebSocketSessionHandler.addSession(sessionKey, session);
-    }
-
-    /**
-     * websocket 关闭连接时执行的动作
-     * @param session websocket session 对象
-     * @param closeStatus 关闭状态对象
-     * @throws Exception 异常对象
-     */
-    @Override
-    public void afterConnectionClosed(final WebSocketSession session, CloseStatus closeStatus) throws Exception {
-        Object sessionKey = sessionKeyGen(session);
-        WebSocketSessionHandler.removeSession(sessionKey);
-    }
-
-    public Object sessionKeyGen(WebSocketSession webSocketSession) {
-
-        Object obj = webSocketSession.getAttributes().get(WebSocketKeyDefine.LOGIN_USER);
-
-        if (obj instanceof LoginUser) {
-            LoginUser loginUser = (LoginUser) obj;
-            // userId 作为唯一区分
-            return String.valueOf(loginUser.getId());
-        }
-
-        return null;
-    }
-}

+ 83 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java

@@ -0,0 +1,83 @@
+package cn.iocoder.yudao.framework.websocket.core.handler;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.TypeUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
+import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
+import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage;
+import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.TextWebSocketHandler;
+
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * JSON 格式 {@link WebSocketHandler} 实现类
+ *
+ * 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
+
+    /**
+     * type 与 WebSocketMessageListener 的映射
+     */
+    private final Map<String, WebSocketMessageListener<Object>> listeners = new HashMap<>();
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    public JsonWebSocketMessageHandler(List<? extends WebSocketMessageListener> listenersList) {
+        listenersList.forEach((Consumer<WebSocketMessageListener>)
+                listener -> listeners.put(listener.getType(), listener));
+    }
+
+    @Override
+    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
+        // 1.1 空消息,跳过
+        if (message.getPayloadLength() == 0) {
+            return;
+        }
+        // 1.2 ping 心跳消息,直接返回 pong 消息。
+        if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) {
+            session.sendMessage(new TextMessage("pong"));
+            return;
+        }
+
+        // 2.1 解析消息
+        try {
+            JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class);
+            if (jsonMessage == null) {
+                log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload());
+                return;
+            }
+            if (StrUtil.isEmpty(jsonMessage.getType())) {
+                log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload());
+                return;
+            }
+            // 2.2 获得对应的 WebSocketMessageListener
+            WebSocketMessageListener<Object> messageListener = listeners.get(jsonMessage.getType());
+            if (messageListener == null) {
+                log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload());
+                return;
+            }
+            // 2.3 处理消息
+            Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0);
+            Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type);
+            Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
+            TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
+        } catch (Throwable ex) {
+            log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
+        }
+    }
+
+}

+ 31 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/listener/WebSocketMessageListener.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.framework.websocket.core.listener;
+
+import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+/**
+ * WebSocket 消息监听器接口
+ *
+ * 目的:前端发送消息给后端后,处理对应 {@link #getType()} 类型的消息
+ *
+ * @param <T> 泛型,消息类型
+ */
+public interface WebSocketMessageListener<T> {
+
+    /**
+     * 处理消息
+     *
+     * @param session Session
+     * @param message 消息
+     */
+    void onMessage(WebSocketSession session, T message);
+
+    /**
+     * 获得消息类型
+     *
+     * @see JsonWebSocketMessage#getType()
+     * @return 消息类型
+     */
+    String getType();
+
+}

+ 29 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/message/JsonWebSocketMessage.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.framework.websocket.core.message;
+
+import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * JSON 格式的 WebSocket 消息帧
+ *
+ * @author 芋道源码
+ */
+@Data
+public class JsonWebSocketMessage implements Serializable {
+
+    /**
+     * 消息类型
+     *
+     * 目的:用于分发到对应的 {@link WebSocketMessageListener} 实现类
+     */
+    private String type;
+    /**
+     * 消息内容
+     *
+     * 要求 JSON 对象
+     */
+    private String content;
+
+}

+ 42 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/LoginUserHandshakeInterceptor.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.framework.websocket.core.security;
+
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import java.util.Map;
+
+/**
+ * 登录用户的 {@link HandshakeInterceptor} 实现类
+ *
+ * 流程如下:
+ * 1. 前端连接 websocket 时,会通过拼接 ?token={token} 到 ws:// 连接后,这样它可以被 {@link TokenAuthenticationFilter} 所认证通过
+ * 2. {@link LoginUserHandshakeInterceptor} 负责把 {@link LoginUser} 添加到 {@link WebSocketSession} 中
+ *
+ * @author 芋道源码
+ */
+public class LoginUserHandshakeInterceptor implements HandshakeInterceptor {
+
+    @Override
+    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
+                                   WebSocketHandler wsHandler, Map<String, Object> attributes) {
+        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+        if (loginUser != null) {
+            WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
+        }
+        return true;
+    }
+
+    @Override
+    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
+                               WebSocketHandler wsHandler, Exception exception) {
+        // do nothing
+    }
+
+}

+ 24 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.framework.websocket.core.security;
+
+import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
+import cn.iocoder.yudao.framework.websocket.config.WebSocketProperties;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
+
+/**
+ * WebSocket 的权限自定义
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer {
+
+    private final WebSocketProperties webSocketProperties;
+
+    @Override
+    public void customize(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) {
+        registry.antMatchers(webSocketProperties.getPath()).permitAll();
+    }
+
+}

+ 104 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/AbstractWebSocketMessageSender.java

@@ -0,0 +1,104 @@
+package cn.iocoder.yudao.framework.websocket.core.sender;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * WebSocketMessageSender 实现类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+@RequiredArgsConstructor
+public abstract class AbstractWebSocketMessageSender implements WebSocketMessageSender {
+
+    private final WebSocketSessionManager sessionManager;
+
+    @Override
+    public void send(Integer userType, Long userId, String messageType, String messageContent) {
+        send(null, userType, userId, messageType, messageContent);
+    }
+
+    @Override
+    public void send(Integer userType, String messageType, String messageContent) {
+        send(null, userType, null, messageType, messageContent);
+    }
+
+    @Override
+    public void send(String sessionId, String messageType, String messageContent) {
+        send(sessionId, null, null, messageType, messageContent);
+    }
+
+    /**
+     * 发送消息
+     *
+     * @param sessionId Session 编号
+     * @param userType 用户类型
+     * @param userId 用户编号
+     * @param messageType 消息类型
+     * @param messageContent 消息内容
+     */
+    public void send(String sessionId, Integer userType, Long userId, String messageType, String messageContent) {
+        // 1. 获得 Session 列表
+        List<WebSocketSession> sessions = Collections.emptyList();
+        if (StrUtil.isNotEmpty(sessionId)) {
+            WebSocketSession session = sessionManager.getSession(sessionId);
+            if (session != null) {
+                sessions = Collections.singletonList(session);
+            }
+        } else if (userType != null && userId != null) {
+            sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType, userId);
+        } else if (userType != null) {
+            sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType);
+        }
+        if (CollUtil.isEmpty(sessions)) {
+            log.info("[send][sessionId({}) userType({}) userId({}) messageType({}) messageContent({}) 未匹配到会话]",
+                    sessionId, userType, userId, messageType, messageContent);
+        }
+        // 2. 执行发送
+        doSend(sessions, messageType, messageContent);
+    }
+
+    /**
+     * 发送消息的具体实现
+     *
+     * @param sessions Session 列表
+     * @param messageType 消息类型
+     * @param messageContent 消息内容
+     */
+    public void doSend(Collection<WebSocketSession> sessions, String messageType, String messageContent) {
+        JsonWebSocketMessage message = new JsonWebSocketMessage().setType(messageType).setContent(messageContent);
+        String payload = JsonUtils.toJsonString(message); // 关键,使用 JSON 序列化
+        sessions.forEach(session -> {
+            // 1. 各种校验,保证 Session 可以被发送
+            if (session == null) {
+                log.error("[doSend][session 为空, message({})]", message);
+                return;
+            }
+            if (!session.isOpen()) {
+                log.error("[doSend][session({}) 已关闭, message({})]", session.getId(), message);
+                return;
+            }
+            // 2. 执行发送
+            try {
+                session.sendMessage(new TextMessage(payload));
+                log.info("[doSend][session({}) 发送消息成功,message({})]", session.getId(), message);
+            } catch (IOException ex) {
+                log.error("[doSend][session({}) 发送消息失败,message({})]", session.getId(), message, ex);
+            }
+        });
+    }
+
+}

+ 52 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/WebSocketMessageSender.java

@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.framework.websocket.core.sender;
+
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+
+/**
+ * WebSocket 消息的发送器接口
+ *
+ * @author 芋道源码
+ */
+public interface WebSocketMessageSender {
+
+    /**
+     * 发送消息给指定用户
+     *
+     * @param userType 用户类型
+     * @param userId 用户编号
+     * @param messageType 消息类型
+     * @param messageContent 消息内容,JSON 格式
+     */
+    void send(Integer userType, Long userId, String messageType, String messageContent);
+
+    /**
+     * 发送消息给指定用户类型
+     *
+     * @param userType 用户类型
+     * @param messageType 消息类型
+     * @param messageContent 消息内容,JSON 格式
+     */
+    void send(Integer userType, String messageType, String messageContent);
+
+    /**
+     * 发送消息给指定 Session
+     *
+     * @param sessionId Session 编号
+     * @param messageType 消息类型
+     * @param messageContent 消息内容,JSON 格式
+     */
+    void send(String sessionId, String messageType, String messageContent);
+
+    default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
+        send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
+    }
+
+    default void sendObject(Integer userType, String messageType, Object messageContent) {
+        send(userType, messageType, JsonUtils.toJsonString(messageContent));
+    }
+
+    default void sendObject(String sessionId, String messageType, Object messageContent) {
+        send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
+    }
+
+}

+ 35 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.kafka;
+
+import lombok.Data;
+
+/**
+ * Kafka 广播 WebSocket 的消息
+ *
+ * @author 芋道源码
+ */
+@Data
+public class KafkaWebSocketMessage {
+
+    /**
+     * Session 编号
+     */
+    private String sessionId;
+    /**
+     * 用户类型
+     */
+    private Integer userType;
+    /**
+     * 用户编号
+     */
+    private Long userId;
+
+    /**
+     * 消息类型
+     */
+    private String messageType;
+    /**
+     * 消息内容
+     */
+    private String messageContent;
+
+}

+ 28 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.kafka;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.amqp.rabbit.annotation.RabbitHandler;
+import org.springframework.kafka.annotation.KafkaListener;
+
+/**
+ * {@link KafkaWebSocketMessage} 广播消息的消费者,真正把消息发送出去
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+public class KafkaWebSocketMessageConsumer {
+
+    private final KafkaWebSocketMessageSender rabbitMQWebSocketMessageSender;
+
+    @RabbitHandler
+    @KafkaListener(
+            topics = "${yudao.websocket.sender-kafka.topic}",
+            // 在 Group 上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Group 不同,以达到广播消费的目的
+            groupId = "${yudao.websocket.sender-kafka.consumer-group}" + "-" + "#{T(java.util.UUID).randomUUID()}")
+    public void onMessage(KafkaWebSocketMessage message) {
+        rabbitMQWebSocketMessageSender.send(message.getSessionId(),
+                message.getUserType(), message.getUserId(),
+                message.getMessageType(), message.getMessageContent());
+    }
+
+}

+ 67 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.kafka;
+
+import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.core.KafkaTemplate;
+
+import java.util.concurrent.ExecutionException;
+
+/**
+ * 基于 Kafka 的 {@link WebSocketMessageSender} 实现类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class KafkaWebSocketMessageSender extends AbstractWebSocketMessageSender {
+
+    private final KafkaTemplate<Object, Object> kafkaTemplate;
+
+    private final String topic;
+
+    public KafkaWebSocketMessageSender(WebSocketSessionManager sessionManager,
+                                       KafkaTemplate<Object, Object> kafkaTemplate,
+                                       String topic) {
+        super(sessionManager);
+        this.kafkaTemplate = kafkaTemplate;
+        this.topic = topic;
+    }
+
+    @Override
+    public void send(Integer userType, Long userId, String messageType, String messageContent) {
+        sendKafkaMessage(null, userId, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(Integer userType, String messageType, String messageContent) {
+        sendKafkaMessage(null, null, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(String sessionId, String messageType, String messageContent) {
+        sendKafkaMessage(sessionId, null, null, messageType, messageContent);
+    }
+
+    /**
+     * 通过 Kafka 广播消息
+     *
+     * @param sessionId Session 编号
+     * @param userId 用户编号
+     * @param userType 用户类型
+     * @param messageType 消息类型
+     * @param messageContent 消息内容
+     */
+    private void sendKafkaMessage(String sessionId, Long userId, Integer userType,
+                                  String messageType, String messageContent) {
+        KafkaWebSocketMessage mqMessage = new KafkaWebSocketMessage()
+                .setSessionId(sessionId).setUserId(userId).setUserType(userType)
+                .setMessageType(messageType).setMessageContent(messageContent);
+        try {
+            kafkaTemplate.send(topic, mqMessage).get();
+        } catch (InterruptedException | ExecutionException e) {
+            log.error("[sendKafkaMessage][发送消息({}) 到 Kafka 失败]", mqMessage, e);
+        }
+    }
+
+}

+ 20 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.local;
+
+import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
+
+/**
+ * 本地的 {@link WebSocketMessageSender} 实现类
+ *
+ * 注意:仅仅适合单机场景!!!
+ *
+ * @author 芋道源码
+ */
+public class LocalWebSocketMessageSender extends AbstractWebSocketMessageSender {
+
+    public LocalWebSocketMessageSender(WebSocketSessionManager sessionManager) {
+        super(sessionManager);
+    }
+
+}

+ 37 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * RabbitMQ 广播 WebSocket 的消息
+ *
+ * @author 芋道源码
+ */
+@Data
+public class RabbitMQWebSocketMessage implements Serializable {
+
+    /**
+     * Session 编号
+     */
+    private String sessionId;
+    /**
+     * 用户类型
+     */
+    private Integer userType;
+    /**
+     * 用户编号
+     */
+    private Long userId;
+
+    /**
+     * 消息类型
+     */
+    private String messageType;
+    /**
+     * 消息内容
+     */
+    private String messageContent;
+
+}

+ 39 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java

@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.amqp.core.ExchangeTypes;
+import org.springframework.amqp.rabbit.annotation.*;
+
+/**
+ * {@link RabbitMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去
+ *
+ * @author 芋道源码
+ */
+@RabbitListener(
+        bindings = @QueueBinding(
+                value = @Queue(
+                        // 在 Queue 的名字上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Queue 不同,以达到广播消费的目的
+                        name = "${yudao.websocket.sender-rabbitmq.queue}" + "-" + "#{T(java.util.UUID).randomUUID()}",
+                        // Consumer 关闭时,该队列就可以被自动删除了
+                        autoDelete = "true"
+                ),
+                exchange = @Exchange(
+                        name = "${yudao.websocket.sender-rabbitmq.exchange}",
+                        type = ExchangeTypes.TOPIC,
+                        declare = "false"
+                )
+        )
+)
+@RequiredArgsConstructor
+public class RabbitMQWebSocketMessageConsumer {
+
+    private final RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender;
+
+    @RabbitHandler
+    public void onMessage(RabbitMQWebSocketMessage message) {
+        rabbitMQWebSocketMessageSender.send(message.getSessionId(),
+                message.getUserType(), message.getUserId(),
+                message.getMessageType(), message.getMessageContent());
+    }
+
+}

+ 62 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java

@@ -0,0 +1,62 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq;
+
+import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.amqp.core.TopicExchange;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+
+/**
+ * 基于 RabbitMQ 的 {@link WebSocketMessageSender} 实现类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class RabbitMQWebSocketMessageSender extends AbstractWebSocketMessageSender {
+
+    private final RabbitTemplate rabbitTemplate;
+
+    private final TopicExchange topicExchange;
+
+    public RabbitMQWebSocketMessageSender(WebSocketSessionManager sessionManager,
+                                          RabbitTemplate rabbitTemplate,
+                                          TopicExchange topicExchange) {
+        super(sessionManager);
+        this.rabbitTemplate = rabbitTemplate;
+        this.topicExchange = topicExchange;
+    }
+
+    @Override
+    public void send(Integer userType, Long userId, String messageType, String messageContent) {
+        sendRabbitMQMessage(null, userId, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(Integer userType, String messageType, String messageContent) {
+        sendRabbitMQMessage(null, null, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(String sessionId, String messageType, String messageContent) {
+        sendRabbitMQMessage(sessionId, null, null, messageType, messageContent);
+    }
+
+    /**
+     * 通过 RabbitMQ 广播消息
+     *
+     * @param sessionId Session 编号
+     * @param userId 用户编号
+     * @param userType 用户类型
+     * @param messageType 消息类型
+     * @param messageContent 消息内容
+     */
+    private void sendRabbitMQMessage(String sessionId, Long userId, Integer userType,
+                                     String messageType, String messageContent) {
+        RabbitMQWebSocketMessage mqMessage = new RabbitMQWebSocketMessage()
+                .setSessionId(sessionId).setUserId(userId).setUserType(userType)
+                .setMessageType(messageType).setMessageContent(messageContent);
+        rabbitTemplate.convertAndSend(topicExchange.getName(), null, mqMessage);
+    }
+
+}

+ 34 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessage.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.redis;
+
+import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage;
+import lombok.Data;
+
+/**
+ * Redis 广播 WebSocket 的消息
+ */
+@Data
+public class RedisWebSocketMessage extends AbstractRedisChannelMessage {
+
+    /**
+     * Session 编号
+     */
+    private String sessionId;
+    /**
+     * 用户类型
+     */
+    private Integer userType;
+    /**
+     * 用户编号
+     */
+    private Long userId;
+
+    /**
+     * 消息类型
+     */
+    private String messageType;
+    /**
+     * 消息内容
+     */
+    private String messageContent;
+
+}

+ 23 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.redis;
+
+import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * {@link RedisWebSocketMessage} 广播消息的消费者,真正把消息发送出去
+ *
+ * @author 芋道源码
+ */
+@RequiredArgsConstructor
+public class RedisWebSocketMessageConsumer extends AbstractRedisChannelMessageListener<RedisWebSocketMessage> {
+
+    private final RedisWebSocketMessageSender redisWebSocketMessageSender;
+
+    @Override
+    public void onMessage(RedisWebSocketMessage message) {
+        redisWebSocketMessageSender.send(message.getSessionId(),
+                message.getUserType(), message.getUserId(),
+                message.getMessageType(), message.getMessageContent());
+    }
+
+}

+ 57 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java

@@ -0,0 +1,57 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.redis;
+
+import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
+import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 基于 Redis 的 {@link WebSocketMessageSender} 实现类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender {
+
+    private final RedisMQTemplate redisMQTemplate;
+
+    public RedisWebSocketMessageSender(WebSocketSessionManager sessionManager,
+                                       RedisMQTemplate redisMQTemplate) {
+        super(sessionManager);
+        this.redisMQTemplate = redisMQTemplate;
+    }
+
+    @Override
+    public void send(Integer userType, Long userId, String messageType, String messageContent) {
+        sendRedisMessage(null, userId, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(Integer userType, String messageType, String messageContent) {
+        sendRedisMessage(null, null, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(String sessionId, String messageType, String messageContent) {
+        sendRedisMessage(sessionId, null, null, messageType, messageContent);
+    }
+
+    /**
+     * 通过 Redis 广播消息
+     *
+     * @param sessionId Session 编号
+     * @param userId 用户编号
+     * @param userType 用户类型
+     * @param messageType 消息类型
+     * @param messageContent 消息内容
+     */
+    private void sendRedisMessage(String sessionId, Long userId, Integer userType,
+                                  String messageType, String messageContent) {
+        RedisWebSocketMessage mqMessage = new RedisWebSocketMessage()
+                .setSessionId(sessionId).setUserId(userId).setUserType(userType)
+                .setMessageType(messageType).setMessageContent(messageContent);
+        redisMQTemplate.send(mqMessage);
+    }
+
+}

+ 35 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq;
+
+import lombok.Data;
+
+/**
+ * RocketMQ 广播 WebSocket 的消息
+ *
+ * @author 芋道源码
+ */
+@Data
+public class RocketMQWebSocketMessage {
+
+    /**
+     * Session 编号
+     */
+    private String sessionId;
+    /**
+     * 用户类型
+     */
+    private Integer userType;
+    /**
+     * 用户编号
+     */
+    private Long userId;
+
+    /**
+     * 消息类型
+     */
+    private String messageType;
+    /**
+     * 消息内容
+     */
+    private String messageContent;
+
+}

+ 30 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq;
+
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.spring.annotation.MessageModel;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+
+/**
+ * {@link RocketMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去
+ *
+ * @author 芋道源码
+ */
+@RocketMQMessageListener( // 重点:添加 @RocketMQMessageListener 注解,声明消费的 topic
+        topic = "${yudao.websocket.sender-rocketmq.topic}",
+        consumerGroup = "${yudao.websocket.sender-rocketmq.consumer-group}",
+        messageModel = MessageModel.BROADCASTING // 设置为广播模式,保证每个实例都能收到消息
+)
+@RequiredArgsConstructor
+public class RocketMQWebSocketMessageConsumer implements RocketMQListener<RocketMQWebSocketMessage> {
+
+    private final RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender;
+
+    @Override
+    public void onMessage(RocketMQWebSocketMessage message) {
+        rocketMQWebSocketMessageSender.send(message.getSessionId(),
+                message.getUserType(), message.getUserId(),
+                message.getMessageType(), message.getMessageContent());
+    }
+
+}

+ 61 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java

@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq;
+
+import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
+import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+
+/**
+ * 基于 RocketMQ 的 {@link WebSocketMessageSender} 实现类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class RocketMQWebSocketMessageSender extends AbstractWebSocketMessageSender {
+
+    private final RocketMQTemplate rocketMQTemplate;
+
+    private final String topic;
+
+    public RocketMQWebSocketMessageSender(WebSocketSessionManager sessionManager,
+                                          RocketMQTemplate rocketMQTemplate,
+                                          String topic) {
+        super(sessionManager);
+        this.rocketMQTemplate = rocketMQTemplate;
+        this.topic = topic;
+    }
+
+    @Override
+    public void send(Integer userType, Long userId, String messageType, String messageContent) {
+        sendRocketMQMessage(null, userId, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(Integer userType, String messageType, String messageContent) {
+        sendRocketMQMessage(null, null, userType, messageType, messageContent);
+    }
+
+    @Override
+    public void send(String sessionId, String messageType, String messageContent) {
+        sendRocketMQMessage(sessionId, null, null, messageType, messageContent);
+    }
+
+    /**
+     * 通过 RocketMQ 广播消息
+     *
+     * @param sessionId Session 编号
+     * @param userId 用户编号
+     * @param userType 用户类型
+     * @param messageType 消息类型
+     * @param messageContent 消息内容
+     */
+    private void sendRocketMQMessage(String sessionId, Long userId, Integer userType,
+                                     String messageType, String messageContent) {
+        RocketMQWebSocketMessage mqMessage = new RocketMQWebSocketMessage()
+                .setSessionId(sessionId).setUserId(userId).setUserType(userType)
+                .setMessageType(messageType).setMessageContent(messageContent);
+        rocketMQTemplate.syncSend(topic, mqMessage);
+    }
+
+}

+ 49 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.framework.websocket.core.session;
+
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
+import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
+
+/**
+ * {@link WebSocketHandler} 的装饰类,实现了以下功能:
+ *
+ * 1. {@link WebSocketSession} 连接或关闭时,使用 {@link #sessionManager} 进行管理
+ * 2. 封装 {@link WebSocketSession} 支持并发操作
+ *
+ * @author 芋道源码
+ */
+public class WebSocketSessionHandlerDecorator extends WebSocketHandlerDecorator {
+
+    /**
+     * 发送时间的限制,单位:毫秒
+     */
+    private static final Integer SEND_TIME_LIMIT = 1000 * 5;
+    /**
+     * 发送消息缓冲上线,单位:bytes
+     */
+    private static final Integer BUFFER_SIZE_LIMIT = 1024 * 100;
+
+    private final WebSocketSessionManager sessionManager;
+
+    public WebSocketSessionHandlerDecorator(WebSocketHandler delegate,
+                                            WebSocketSessionManager sessionManager) {
+        super(delegate);
+        this.sessionManager = sessionManager;
+    }
+
+    @Override
+    public void afterConnectionEstablished(WebSocketSession session) {
+        // 实现 session 支持并发,可参考 https://blog.csdn.net/abu935009066/article/details/131218149
+        session = new ConcurrentWebSocketSessionDecorator(session, SEND_TIME_LIMIT, BUFFER_SIZE_LIMIT);
+        // 添加到 WebSocketSessionManager 中
+        sessionManager.addSession(session);
+    }
+
+    @Override
+    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
+        sessionManager.removeSession(session);
+    }
+
+}

+ 53 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManager.java

@@ -0,0 +1,53 @@
+package cn.iocoder.yudao.framework.websocket.core.session;
+
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.Collection;
+
+/**
+ * {@link WebSocketSession} 管理器的接口
+ *
+ * @author 芋道源码
+ */
+public interface WebSocketSessionManager {
+
+    /**
+     * 添加 Session
+     *
+     * @param session Session
+     */
+    void addSession(WebSocketSession session);
+
+    /**
+     * 移除 Session
+     *
+     * @param session Session
+     */
+    void removeSession(WebSocketSession session);
+
+    /**
+     * 获得指定编号的 Session
+     *
+     * @param id Session 编号
+     * @return Session
+     */
+    WebSocketSession getSession(String id);
+
+    /**
+     * 获得指定用户类型的 Session 列表
+     *
+     * @param userType 用户类型
+     * @return Session 列表
+     */
+    Collection<WebSocketSession> getSessionList(Integer userType);
+
+    /**
+     * 获得指定用户编号的 Session 列表
+     *
+     * @param userType 用户类型
+     * @param userId 用户编号
+     * @return Session 列表
+     */
+    Collection<WebSocketSession> getSessionList(Integer userType, Long userId);
+
+}

+ 125 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManagerImpl.java

@@ -0,0 +1,125 @@
+package cn.iocoder.yudao.framework.websocket.core.session;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * 默认的 {@link WebSocketSessionManager} 实现类
+ *
+ * @author 芋道源码
+ */
+public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
+
+    /**
+     * id 与 WebSocketSession 映射
+     *
+     * key:Session 编号
+     */
+    private final ConcurrentMap<String, WebSocketSession> idSessions = new ConcurrentHashMap<>();
+
+    /**
+     * user 与 WebSocketSession 映射
+     *
+     * key1:用户类型
+     * key2:用户编号
+     */
+    private final ConcurrentMap<Integer, ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>>> userSessions
+            = new ConcurrentHashMap<>();
+
+    @Override
+    public void addSession(WebSocketSession session) {
+        // 添加到 idSessions 中
+        idSessions.put(session.getId(), session);
+        // 添加到 userSessions 中
+        LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
+        if (user == null) {
+            return;
+        }
+        ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());
+        if (userSessionsMap == null) {
+            userSessionsMap = new ConcurrentHashMap<>();
+            if (userSessions.putIfAbsent(user.getUserType(), userSessionsMap) != null) {
+                userSessionsMap = userSessions.get(user.getUserType());
+            }
+        }
+        CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());
+        if (sessions == null) {
+            sessions = new CopyOnWriteArrayList<>();
+            if (userSessionsMap.putIfAbsent(user.getId(), sessions) != null) {
+                sessions = userSessionsMap.get(user.getId());
+            }
+        }
+        sessions.add(session);
+    }
+
+    @Override
+    public void removeSession(WebSocketSession session) {
+        // 移除从 idSessions 中
+        idSessions.remove(session.getId(), session);
+        // 移除从 idSessions 中
+        LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
+        if (user == null) {
+            return;
+        }
+        ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());
+        if (userSessionsMap == null) {
+            return;
+        }
+        CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());
+        sessions.removeIf(session0 -> session0.getId().equals(session.getId()));
+        if (CollUtil.isEmpty(sessions)) {
+            userSessionsMap.remove(user.getId(), sessions);
+        }
+    }
+
+    @Override
+    public WebSocketSession getSession(String id) {
+        return idSessions.get(id);
+    }
+
+    @Override
+    public Collection<WebSocketSession> getSessionList(Integer userType) {
+        ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);
+        if (CollUtil.isEmpty(userSessionsMap)) {
+            return new ArrayList<>();
+        }
+        LinkedList<WebSocketSession> result = new LinkedList<>(); // 避免扩容
+        Long contextTenantId = TenantContextHolder.getTenantId();
+        for (List<WebSocketSession> sessions : userSessionsMap.values()) {
+            if (CollUtil.isEmpty(sessions)) {
+                continue;
+            }
+            // 特殊:如果租户不匹配,则直接排除
+            if (contextTenantId != null) {
+                Long userTenantId = WebSocketFrameworkUtils.getTenantId(sessions.get(0));
+                if (!contextTenantId.equals(userTenantId)) {
+                    continue;
+                }
+            }
+            result.addAll(sessions);
+        }
+        return result;
+    }
+
+    @Override
+    public Collection<WebSocketSession> getSessionList(Integer userType, Long userId) {
+        ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);
+        if (CollUtil.isEmpty(userSessionsMap)) {
+            return new ArrayList<>();
+        }
+        CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(userId);
+        return CollUtil.isNotEmpty(sessions) ? new ArrayList<>(sessions) : new ArrayList<>();
+    }
+
+}

+ 67 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/util/WebSocketFrameworkUtils.java

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.framework.websocket.core.util;
+
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.Map;
+
+/**
+ * 专属于 web 包的工具类
+ *
+ * @author 芋道源码
+ */
+public class WebSocketFrameworkUtils {
+
+    public static final String ATTRIBUTE_LOGIN_USER = "LOGIN_USER";
+
+    /**
+     * 设置当前用户
+     *
+     * @param loginUser 登录用户
+     * @param attributes Session
+     */
+    public static void setLoginUser(LoginUser loginUser, Map<String, Object> attributes) {
+        attributes.put(ATTRIBUTE_LOGIN_USER, loginUser);
+    }
+
+    /**
+     * 获取当前用户
+     *
+     * @return 当前用户
+     */
+    public static LoginUser getLoginUser(WebSocketSession session) {
+        return (LoginUser) session.getAttributes().get(ATTRIBUTE_LOGIN_USER);
+    }
+
+    /**
+     * 获得当前用户的编号
+     *
+     * @return 用户编号
+     */
+    public static Long getLoginUserId(WebSocketSession session) {
+        LoginUser loginUser = getLoginUser(session);
+        return loginUser != null ? loginUser.getId() : null;
+    }
+
+    /**
+     * 获得当前用户的类型
+     *
+     * @return 用户编号
+     */
+    public static Integer getLoginUserType(WebSocketSession session) {
+        LoginUser loginUser = getLoginUser(session);
+        return loginUser != null ? loginUser.getUserType() : null;
+    }
+
+    /**
+     * 获得当前用户的租户编号
+     *
+     * @param session Session
+     * @return 租户编号
+     */
+    public static Long getTenantId(WebSocketSession session) {
+        LoginUser loginUser = getLoginUser(session);
+        return loginUser != null ? loginUser.getTenantId() : null;
+    }
+
+}

+ 3 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java

@@ -1 +1,4 @@
+/**
+ * WebSocket 框架,支持多节点的广播
+ */
 package cn.iocoder.yudao.framework.websocket;

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-websocket/《芋道 Spring Boot WebSocket 入门》.md

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

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