Kaynağa Gözat

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 yıl önce
ebeveyn
işleme
73e76ab453
100 değiştirilmiş dosya ile 4928 ekleme ve 1858 silme
  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/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/YudaoRedisMQAutoConfiguration.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. 0 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;
+    }
+
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 2068 - 0
sql/dm/flowable-patch/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java


+ 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

sql/mysql/crm.sql → sql/mysql/optinal/crm.sql


sql/mysql/crm_data.sql → sql/mysql/optinal/crm_data.sql


sql/mysql/crm_menu.sql → sql/mysql/optinal/crm_menu.sql


sql/mysql/mall.sql → sql/mysql/optinal/mall.sql


+ 8 - 0
sql/mysql/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`;

Dosya farkı çok büyük olduğundan ihmal edildi
+ 63 - 44
sql/mysql/ruoyi-vue-pro.sql


+ 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>
@@ -115,11 +115,6 @@
             </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>
                 <version>${revision}</version>
             </dependency>
@@ -176,6 +171,12 @@
             </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>
                 <version>${knife4j.version}</version>
@@ -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>
@@ -614,11 +621,6 @@
             </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>
                 <version>${weixin-java.version}</version>
             </dependency>

+ 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

@@ -11,8 +11,33 @@ import lombok.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;
 
@@ -47,6 +42,11 @@ import static cn.iocoder.yudao.framework.common.util.date.DateUtils.TIME_ZONE_DE
 public class AliyunSmsClient extends AbstractSmsClient {
 
     /**
+     * 调用成功 code
+     */
+    public static final String API_CODE_SUCCESS = "OK";
+
+    /**
      * REGION, 使用杭州
      */
     private static final String ENDPOINT = "cn-hangzhou";
@@ -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

@@ -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;

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


Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor