diff --git a/README.md b/README.md index 80b60d900..3841fda95 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,4 @@ -**严肃声明:现在、未来都不会有商业版本,所有功能全部开源!** - -**拒绝虚假开源,售卖商业版,程序员不骗程序员!!** +**严肃声明:现在、未来都不会有商业版本,所有代码全部开源!** **「我喜欢写代码,乐此不疲」** **「我喜欢做开源,以此为乐」** @@ -27,7 +25,7 @@ * 管理后台的 Vue3 版本采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) ,Vue2 版本采用 [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) * 管理后台的移动端采用 [uni-app](https://github.com/dcloudio/uni-app) 方案,一份代码多终端适配,同时支持 APP、小程序、H5! -* 后端采用 Spring Boot、MySQL + MyBatis Plus、Redis + Redisson +* 后端采用 Spring Boot 多模块架构、MySQL + MyBatis Plus、Redis + Redisson * 数据库可使用 MySQL、Oracle、PostgreSQL、SQL Server、MariaDB、国产达梦 DM、TiDB 等 * 权限认证使用 Spring Security & Token & Redis,支持多终端、多种用户的认证系统,支持 SSO 单点登录 * 支持加载动态权限菜单,按钮级别权限控制,本地缓存提升性能 @@ -38,12 +36,12 @@ * 集成阿里云、腾讯云等短信渠道,集成 MinIO、阿里云、腾讯云、七牛云等云存储服务 * 集成报表设计器,支持数据报表、图形报表、打印设计等 -| 项目名 | 说明 | 传送门 | -|--------------------|------------------------|-----------------------------------------------------------------------------------------------------------------------------------| -| `ruoyi-vue-pro` | Spring Boot 多模块 | **[Gitee](https://gitee.com/zhijiantianya/ruoyi-vue-pro)**     [Github](https://github.com/YunaiV/ruoyi-vue-pro) | -| `yudao-cloud` | Spring Cloud 微服务 | **[Gitee](https://gitee.com/zhijiantianya/yudao-cloud)**     [Github](https://github.com/YunaiV/yudao-cloud) | -| `Spring-Boot-Labs` | Spring Boot & Cloud 入门 | **[Gitee](https://gitee.com/zhijiantianya/SpringBoot-Labs)**     [Github](https://github.com/YunaiV/SpringBoot-Labs) | - | `ruoyi-vue-pro-mini` | 精简版 移除工作流 支付等模块| **[Gitee](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/mini)** | +| 项目名 | 说明 | 传送门 | +|----------------------|------------------------|-------------------------------------------------------------------------------------------------------------------------------------| +| `ruoyi-vue-pro` | Spring Boot 多模块 | **[Gitee](https://gitee.com/zhijiantianya/ruoyi-vue-pro)**     [Github](https://github.com/YunaiV/ruoyi-vue-pro) | +| `yudao-cloud` | Spring Cloud 微服务 | **[Gitee](https://gitee.com/zhijiantianya/yudao-cloud)**     [Github](https://github.com/YunaiV/yudao-cloud) | +| `Spring-Boot-Labs` | Spring Boot & Cloud 入门 | **[Gitee](https://gitee.com/zhijiantianya/SpringBoot-Labs)**     [Github](https://github.com/YunaiV/SpringBoot-Labs) | + | `ruoyi-vue-pro-mini` | 精简版:移除工作流、支付等模块 | **[Gitee](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/mini)** | ## 😎 开源协议 @@ -96,8 +94,9 @@ | ⭐️ | 登录日志 | 系统登录日志记录查询,包含登录异常 | | 🚀 | 错误码管理 | 系统所有错误码的管理,可在线修改错误提示,无需重启服务 | | | 通知公告 | 系统通知公告信息发布维护 | -| 🚀 | 敏感词 | 配置系统敏感词,支持标签分组 | +| 🚀 | 敏感词 | 配置系统敏感词,支持标签分组 | | 🚀 | 应用管理 | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 | +| 🚀 | 地区管理 | 展示省份、城市、区镇等城市信息,支持 IP 对应城市 | ### 工作流程 @@ -132,7 +131,7 @@ ps:核心功能已经实现,正在对接微信小程序中... | | 表单构建 | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件 | | 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 | | ⭐️ | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 | -| 🚀 | 文件服务 | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等 | +| 🚀 | 文件服务 | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等 | | 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 | | | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈 | | | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理 | @@ -169,47 +168,47 @@ ps:核心功能已经实现,正在对接微信小程序中... ## 🐨 技术栈 -| 项目 | 说明 | -|-------------------------|-----------------------| -| `yudao-dependencies` | Maven 依赖版本管理 | -| `yudao-framework` | Java 框架拓展 | -| `yudao-server` | 管理后台 + 用户 APP 的服务端 | -| `yudao-ui-admin` | 管理后台的 Vue2 前端项目 | -| `yudao-ui-admin-vue3` | 管理后台的 Vue3 前端项目 | -| `yudao-ui-admin-uniapp` | 管理后台的 uni-app 多端项目 | -| `yudao-ui-app` | 用户 APP 的 UI 界面 | -| `yudao-module-system` | 系统功能的 Module 模块 | -| `yudao-module-member` | 会员中心的 Module 模块 | -| `yudao-module-infra` | 基础设施的 Module 模块 | -| `yudao-module-tool` | 研发工具的 Module 模块 | -| `yudao-module-bpm` | 工作流程的 Module 模块 | -| `yudao-module-pay` | 支付系统的 Module 模块 | +| 项目 | 说明 | +|------------------------------|--------------------| +| `yudao-dependencies` | Maven 依赖版本管理 | +| `yudao-framework` | Java 框架拓展 | +| `yudao-server` | 管理后台 + 用户 APP 的服务端 | +| `yudao-ui-admin` | 管理后台的 Vue2 前端项目 | +| `yudao-ui-admin-vue3` | 管理后台的 Vue3 前端项目 | +| `yudao-ui-admin-uniapp` | 管理后台的 uni-app 多端项目 | +| `yudao-ui-app` | 用户 APP 的 UI 界面 | +| `yudao-module-system` | 系统功能的 Module 模块 | +| `yudao-module-member` | 会员中心的 Module 模块 | +| `yudao-module-infra` | 基础设施的 Module 模块 | +| `yudao-module-bpm` | 工作流程的 Module 模块 | +| `yudao-module-pay` | 支付系统的 Module 模块 | +| `yudao-module-visualization` | 大屏报表 Module 模块 | ### 后端 -| 框架 | 说明 | 版本 | 学习指南 | -|---------------------------------------------------------------------------------------------|-----------------------|-------------|----------------------------------------------------------------| -| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.7.6 | [文档](https://github.com/YunaiV/SpringBoot-Labs) | -| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | | -| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.15 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | -| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.2 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) | -| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.6.0 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | -| [Redis](https://redis.io/) | key-value 数据库 | 5.0 / 6.0 | | -| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.18.0 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) | -| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.3.24 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) | -| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.7.5 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) | -| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.5 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) | -| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 6.7.2 | [文档](https://doc.iocoder.cn/bpm/) | -| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) | -| [Knife4j](https://gitee.com/xiaoym/knife4j) | Swagger 增强 UI 实现 | 3.0.3 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) | -| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 1.7.1 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) | +| 框架 | 说明 | 版本 | 学习指南 | +|---------------------------------------------------------------------------------------------|------------------|-------------|----------------------------------------------------------------| +| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.7.7 | [文档](https://github.com/YunaiV/SpringBoot-Labs) | +| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | | +| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.15 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | +| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.3 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) | +| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.6.1 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | +| [Redis](https://redis.io/) | key-value 数据库 | 5.0 / 6.0 | | +| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.18.0 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) | +| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.3.24 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) | +| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.7.5 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) | +| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.5 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) | +| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 6.8.0 | [文档](https://doc.iocoder.cn/bpm/) | +| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) | +| [Knife4j](https://gitee.com/xiaoym/knife4j) | Swagger 增强 UI 实现 | 3.0.3 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) | +| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 1.7.1 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) | | [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 8.12.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) | -| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 2.7.9 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) | -| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.13.3 | | -| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.5.3.Final | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) | -| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.18.24 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) | -| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.8.2 | - | -| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 4.8.0 | - | +| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 2.7.9 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) | +| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.13.3 | | +| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.5.3.Final | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) | +| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.18.24 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) | +| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.8.2 | - | +| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 4.8.0 | - | ### [管理后台 Vue2 前端](./yudao-ui-admin) @@ -220,22 +219,22 @@ ps:核心功能已经实现,正在对接微信小程序中... ### [管理后台 Vue3 前端](./yudao-ui-admin-vue3) -| 框架 | 说明 | 版本 | +| 框架 | 说明 | 版本 | |----------------------------------------------------------------------|:------------:|:------:| -| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.2.45 | -| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.0.3 | -| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.27 | +| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.2.45 | +| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.0.4 | +| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.28 | | [TypeScript](https://www.typescriptlang.org/docs/) | TypeScript | 4.9.4 | | [pinia](https://pinia.vuejs.org/) | vuex5 | 2.0.28 | -| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 | -| [vxe-table](https://vxetable.cn/) | vue最强表单 | 4.3.7 | +| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 | +| [vxe-table](https://vxetable.cn/) | vue最强表单 | 4.3.7 | ### [管理后台 uni-app 跨端](./yudao-ui-admin-uniapp) -| 框架 | 说明 | 版本 | -|----------------------------------------------------------------------|------------------|--------| -| [uni-app](hhttps://github.com/dcloudio/uni-app) | 跨平台框架 | 2.0.0 | -| [uni-ui](https://github.com/dcloudio/uni-ui) | 基于 uni-app 的 UI 框架 | 1.4.20 | +| 框架 | 说明 | 版本 | +|-------------------------------------------------|--------------------|--------| +| [uni-app](hhttps://github.com/dcloudio/uni-app) | 跨平台框架 | 2.0.0 | +| [uni-ui](https://github.com/dcloudio/uni-ui) | 基于 uni-app 的 UI 框架 | 1.4.20 | ## 🐷 演示图 diff --git a/pom.xml b/pom.xml index 8f8f62990..bfe536ed2 100644 --- a/pom.xml +++ b/pom.xml @@ -14,12 +14,12 @@ yudao-server yudao-module-member - yudao-module-bpm yudao-module-system yudao-module-infra yudao-module-pay - + yudao-module-visualization + yudao-example @@ -29,7 +29,7 @@ https://github.com/YunaiV/ruoyi-vue-pro - 1.6.5-snapshot + 1.6.6-snapshot 1.8 ${java.version} diff --git a/sql/mysql/optional/vue3-menu.sql b/sql/mysql/optional/vue3-menu.sql index 3ba7242ff..dfe3a6123 100644 --- a/sql/mysql/optional/vue3-menu.sql +++ b/sql/mysql/optional/vue3-menu.sql @@ -262,5 +262,6 @@ INSERT INTO `system_menu` VALUES (1266, '客户端更新', 'system:oauth2-client INSERT INTO `system_menu` VALUES (1267, '客户端删除', 'system:oauth2-client:delete', 3, 4, 1263, '', '', '', 0, b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:33', b'0'); INSERT INTO `system_menu` VALUES (1281, '可视化报表', '', 1, 12, 0, '/visualization', 'ep:histogram', NULL, 0, b'1', b'1', '1', '2022-07-10 20:22:15', '1', '2022-07-10 20:33:30', b'0'); INSERT INTO `system_menu` VALUES (1282, '积木报表', '', 2, 1, 1281, 'jimu-report', 'ep:histogram', 'visualization/jmreport/index', 0, b'1', b'1', '1', '2022-07-10 20:26:36', '1', '2022-07-28 21:17:34', b'0'); +INSERT INTO `system_menu` VALUES (1283, 'webSocket连接', '', 2, 14, 2, 'webSocket', 'ep:turn-off', 'infra/webSocket/index', 0, b'1', b'1', '1', '2023-01-01 11:43:04', '1', '2023-01-01 11:43:04', b'0'); SET FOREIGN_KEY_CHECKS = 1; diff --git a/sql/mysql/ruoyi-vue-pro.sql b/sql/mysql/ruoyi-vue-pro.sql index 39cb52afe..7b08e86fe 100644 --- a/sql/mysql/ruoyi-vue-pro.sql +++ b/sql/mysql/ruoyi-vue-pro.sql @@ -1710,6 +1710,8 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1267, '客户端删除', 'system:oauth2-client:delete', 3, 4, 1263, '', '', '', 0, b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:33', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1281, '可视化报表', '', 1, 12, 0, '/visualization', 'chart', NULL, 0, b'1', b'1', '1', '2022-07-10 20:22:15', '1', '2022-07-10 20:33:30', b'0'); INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1282, '积木报表', '', 2, 1, 1281, 'jimu-report', 'example', 'visualization/jmreport/index', 0, b'1', b'1', '1', '2022-07-10 20:26:36', '1', '2022-07-28 21:17:34', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1283, 'webSocket连接', '', 2, 14, 2, 'webSocket', 'message', 'infra/webSocket/index', 0, b'1', b'1', '1', '2023-01-01 11:43:04', '1', '2023-01-01 11:43:04', b'0'); + COMMIT; -- ---------------------------- diff --git a/sql/optional/visualization/jimureport.mysql5.7.create.sql b/sql/optional/visualization/jimureport.mysql5.7.create.sql index e32134005..c90ea27d8 100644 --- a/sql/optional/visualization/jimureport.mysql5.7.create.sql +++ b/sql/optional/visualization/jimureport.mysql5.7.create.sql @@ -1344,6 +1344,7 @@ CREATE TABLE `jimu_report_share` ( `last_update_time` datetime NULL DEFAULT NULL COMMENT '最后更新时间', `term_of_validity` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '有效期(0:永久有效,1:1天,2:7天)', `status` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '是否过期(0未过期,1已过期)', + `preview_lock_status` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码锁状态', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '积木报表预览权限表' ROW_FORMAT = Dynamic; diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index 78d3bcf34..7b056c46b 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -14,18 +14,18 @@ https://github.com/YunaiV/ruoyi-vue-pro - 1.6.5-snapshot + 1.6.6-snapshot - 2.7.6 + 2.7.7 3.0.3 1.6.8 2.5 1.2.15 - 3.5.2 - 3.5.2 - 3.6.0 + 3.5.3.1 + 3.5.3.1 + 3.6.1 3.18.0 2.2.3 @@ -37,14 +37,14 @@ 7.2.11.RELEASE 1.0.5 - 4.8.0 + 4.11.0 - 6.7.2 + 6.8.0 1.18.24 1.5.3.Final - 5.8.10 - 3.1.3 + 5.8.11 + 3.1.4 2.3 1.0.5 1.2.83 @@ -55,14 +55,15 @@ 0.1.55 2.6.0 1.3.0 - 4.1.85.Final + 4.1.86.Final + 2.6.6 3.0.0 4.10.0 8.4.6 4.6.3 2.2.1 - 3.1.637 + 3.1.660 1.4.0 1.5.6 2.12.2 @@ -515,6 +516,12 @@ ${netty-all.version} + + org.lionsoul + ip2region + ${ip2region.version} + + com.squareup.okio @@ -588,6 +595,12 @@ xercesImpl ${xercesImpl.version} + + + org.springframework.boot + spring-boot-starter-websocket + ${spring.boot.version} + diff --git a/yudao-example/yudao-sso-demo-by-code/pom.xml b/yudao-example/yudao-sso-demo-by-code/pom.xml index f6a168f76..ef83df4ac 100644 --- a/yudao-example/yudao-sso-demo-by-code/pom.xml +++ b/yudao-example/yudao-sso-demo-by-code/pom.xml @@ -21,7 +21,7 @@ 8 UTF-8 - 2.7.6 + 2.7.7 @@ -52,7 +52,7 @@ cn.hutool hutool-all - 5.8.10 + 5.8.11 diff --git a/yudao-example/yudao-sso-demo-by-password/pom.xml b/yudao-example/yudao-sso-demo-by-password/pom.xml index c00032169..391358ee2 100644 --- a/yudao-example/yudao-sso-demo-by-password/pom.xml +++ b/yudao-example/yudao-sso-demo-by-password/pom.xml @@ -21,7 +21,7 @@ 8 UTF-8 - 2.7.6 + 2.7.7 @@ -52,7 +52,7 @@ cn.hutool hutool-all - 5.8.10 + 5.8.11 diff --git a/yudao-framework/pom.xml b/yudao-framework/pom.xml index 1928aa88c..3bfc020ba 100644 --- a/yudao-framework/pom.xml +++ b/yudao-framework/pom.xml @@ -40,6 +40,7 @@ yudao-spring-boot-starter-flowable yudao-spring-boot-starter-captcha + yudao-spring-boot-starter-websocket yudao-framework diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java index a50e2382f..2481b2acb 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/test/java/cn/iocoder/yudao/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java @@ -227,7 +227,7 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { // 调用 Expression expression = rule.getExpression(tableName, tableAlias); // 断言 - assertEquals("u.dept_id IN (10, 20) OR u.id = 1", expression.toString()); + assertEquals("(u.dept_id IN (10, 20) OR u.id = 1)", expression.toString()); assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); } } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-ip/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-ip/pom.xml index f30ce1255..4ee1dc5b8 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-ip/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-biz-ip/pom.xml @@ -34,7 +34,6 @@ org.lionsoul ip2region - ${ip2region.version} diff --git a/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java b/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java index c77f8be30..d3fe59a6c 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-ip/src/main/java/cn/iocoder/yudao/framework/ip/core/utils/AreaUtils.java @@ -28,7 +28,9 @@ public class AreaUtils { @SuppressWarnings("InstantiationOfUtilityClass") private final static AreaUtils INSTANCE = new AreaUtils(); - + /** + * Area 内存缓存,提升访问速度 + */ private static Map areas; private AreaUtils() { diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml b/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml index 03f0013aa..06b127a0a 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/pom.xml @@ -52,7 +52,7 @@ com.alipay.sdk alipay-sdk-java - 4.35.0.ALL + 4.35.9.ALL org.bouncycastle diff --git a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java index 88e7ac75f..a04114a85 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-pay/src/main/java/cn/iocoder/yudao/framework/pay/core/client/impl/wx/WXPubPayClient.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.framework.pay.core.client.impl.wx; import cn.hutool.core.bean.BeanUtil; -import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.date.TemporalAccessorUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; @@ -26,8 +26,8 @@ import com.github.binarywang.wxpay.service.WxPayService; import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; import lombok.extern.slf4j.Slf4j; +import java.time.LocalDateTime; import java.time.ZoneId; -import java.util.Date; import java.util.Objects; import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString; @@ -100,8 +100,8 @@ public class WXPubPayClient extends AbstractPayClient { WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder() .outTradeNo(reqDTO.getMerchantOrderId()) .body(reqDTO.getBody()) - .totalFee(reqDTO.getAmount().intValue()) // 单位分 - .timeExpire(DateUtil.format(Date.from(reqDTO.getExpireTime().atZone(ZoneId.systemDefault()).toInstant()), "yyyy-MM-dd'T'HH:mm:ssXXX")) + .totalFee(reqDTO.getAmount()) // 单位分 + .timeExpire(formatDate(reqDTO.getExpireTime())) .spbillCreateIp(reqDTO.getUserIp()) .openid(getOpenid(reqDTO)) .notifyUrl(reqDTO.getNotifyUrl()) @@ -115,8 +115,8 @@ public class WXPubPayClient extends AbstractPayClient { WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); request.setOutTradeNo(reqDTO.getMerchantOrderId()); request.setDescription(reqDTO.getBody()); - request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount().intValue())); // 单位分 - request.setTimeExpire(DateUtil.format(Date.from(reqDTO.getExpireTime().atZone(ZoneId.systemDefault()).toInstant()), "yyyy-MM-dd'T'HH:mm:ssXXX")); + request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getAmount())); // 单位分 + request.setTimeExpire(formatDate(reqDTO.getExpireTime())); request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO))); request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp())); request.setNotifyUrl(reqDTO.getNotifyUrl()); @@ -196,4 +196,8 @@ public class WXPubPayClient extends AbstractPayClient { throw new UnsupportedOperationException(); } + private static String formatDate(LocalDateTime time) { + return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), "yyyy-MM-dd'T'HH:mm:ssXXX"); + } + } diff --git a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java index 549bf8b0b..fd188d9b2 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-sms/src/main/java/cn/iocoder/yudao/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java @@ -35,8 +35,8 @@ public class AliyunSmsCodeMapping implements SmsCodeMapping { 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; } - return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; } } diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java index a2b11b1dc..f554e0b52 100644 --- a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClient.java @@ -9,10 +9,11 @@ import io.minio.*; import java.io.ByteArrayInputStream; import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_ALIYUN; +import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_TENCENT; /** * 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务 - * + *

* S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库 * * @author 芋道源码 @@ -78,6 +79,11 @@ public class S3FileClient extends AbstractFileClient { .replaceAll("-internal", "")// 去除内网 Endpoint 的后缀 .replaceAll("https://", ""); } + // 腾讯云必须有 region,否则会报错 + if (config.getEndpoint().contains(ENDPOINT_TENCENT)) { + return StrUtil.subAfter(config.getEndpoint(), ".cos.", false) + .replaceAll("." + ENDPOINT_TENCENT, ""); // 去除 Endpoint + } return null; } diff --git a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientConfig.java b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientConfig.java index 151159f5e..0c46e8aa6 100644 --- a/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientConfig.java +++ b/yudao-framework/yudao-spring-boot-starter-file/src/main/java/cn/iocoder/yudao/framework/file/core/client/s3/S3FileClientConfig.java @@ -19,6 +19,7 @@ public class S3FileClientConfig implements FileClientConfig { public static final String ENDPOINT_QINIU = "qiniucs.com"; public static final String ENDPOINT_ALIYUN = "aliyuncs.com"; + public static final String ENDPOINT_TENCENT = "myqcloud.com"; /** * 节点地址 diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/config/YudaoMQAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/config/YudaoMQAutoConfiguration.java index c369d49d6..042a4f736 100644 --- a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/config/YudaoMQAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/config/YudaoMQAutoConfiguration.java @@ -8,8 +8,11 @@ import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor; import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener; import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener; +import cn.iocoder.yudao.framework.mq.job.RedisPendingMessageResendJob; import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.data.redis.connection.RedisServerCommands; @@ -24,7 +27,7 @@ import org.springframework.data.redis.listener.ChannelTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.stream.DefaultStreamMessageListenerContainerX; import org.springframework.data.redis.stream.StreamMessageListenerContainer; -import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableScheduling; import java.util.List; import java.util.Properties; @@ -35,6 +38,7 @@ import java.util.Properties; * @author 芋道源码 */ @Slf4j +@EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息 @AutoConfiguration(after = YudaoRedisAutoConfiguration.class) public class YudaoMQAutoConfiguration { @@ -69,9 +73,20 @@ public class YudaoMQAutoConfiguration { return container; } + /** + * 创建 Redis Stream 重新消费的任务 + */ + @Bean + public RedisPendingMessageResendJob redisPendingMessageResendJob(List> listeners, + RedisMQTemplate redisTemplate, + @Value("${spring.application.name}") String groupName, + RedissonClient redissonClient) { + return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient); + } + /** * 创建 Redis Stream 集群消费的容器 - * + *

* Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html */ @Bean(initMethod = "start", destroyMethod = "stop") @@ -99,7 +114,8 @@ public class YudaoMQAutoConfiguration { // 创建 listener 对应的消费者分组 try { redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup()); - } catch (Exception ignore) {} + } catch (Exception ignore) { + } // 设置 listener 对应的 redisTemplate listener.setRedisMQTemplate(redisMQTemplate); // 创建 Consumer 对象 diff --git a/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/job/RedisPendingMessageResendJob.java b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/job/RedisPendingMessageResendJob.java new file mode 100644 index 000000000..f4ba050c0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/job/RedisPendingMessageResendJob.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.framework.mq.job; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate; +import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.data.redis.connection.stream.Consumer; +import org.springframework.data.redis.connection.stream.MapRecord; +import org.springframework.data.redis.connection.stream.PendingMessagesSummary; +import org.springframework.data.redis.connection.stream.ReadOffset; +import org.springframework.data.redis.connection.stream.StreamOffset; +import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.core.StreamOperations; +import org.springframework.scheduling.annotation.Scheduled; + +import java.util.List; +import java.util.Map; + +/** + * 这个任务用于处理,crash 之后的消费者未消费完的消息 + */ +@Slf4j +@AllArgsConstructor +public class RedisPendingMessageResendJob { + + private static final String LOCK_KEY = "redis:pending:msg:lock"; + + private final List> listeners; + private final RedisMQTemplate redisTemplate; + private final String groupName; + private final RedissonClient redissonClient; + + /** + * 一分钟执行一次,这里选择每分钟的35秒执行,是为了避免整点任务过多的问题 + */ + @Scheduled(cron = "35 * * * * ?") + public void messageResend() { + RLock lock = redissonClient.getLock(LOCK_KEY); + // 尝试加锁 + if (lock.tryLock()) { + try { + execute(); + } catch (Exception ex) { + log.error("[messageResend][执行异常]", ex); + } finally { + lock.unlock(); + } + } + } + + private void execute() { + StreamOperations ops = redisTemplate.getRedisTemplate().opsForStream(); + listeners.forEach(listener -> { + PendingMessagesSummary pendingMessagesSummary = ops.pending(listener.getStreamKey(), groupName); + // 每个消费者的 pending 队列消息数量 + Map pendingMessagesPerConsumer = pendingMessagesSummary.getPendingMessagesPerConsumer(); + pendingMessagesPerConsumer.forEach((consumerName, pendingMessageCount) -> { + log.info("[processPendingMessage][消费者({}) 消息数量({})]", consumerName, pendingMessageCount); + + // 从消费者的 pending 队列中读取消息 + List> records = ops.read(Consumer.from(groupName, consumerName), StreamOffset.create(listener.getStreamKey(), ReadOffset.from("0"))); + if (CollUtil.isEmpty(records)) { + return; + } + for (MapRecord record : records) { + // 重新投递消息 + redisTemplate.getRedisTemplate().opsForStream().add(StreamRecords.newRecord() + .ofObject(record.getValue()) // 设置内容 + .withStreamKey(listener.getStreamKey())); + + // ack 消息消费完成 + redisTemplate.getRedisTemplate().opsForStream().acknowledge(groupName, record); + } + }); + }); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java index c3715c185..082d84756 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java @@ -129,6 +129,8 @@ 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))) diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/pom.xml b/yudao-framework/yudao-spring-boot-starter-websocket/pom.xml new file mode 100644 index 000000000..320e52c48 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-websocket/pom.xml @@ -0,0 +1,37 @@ + + + + cn.iocoder.boot + yudao-framework + ${revision} + + 4.0.0 + yudao-spring-boot-starter-websocket + jar + + ${project.artifactId} + WebSocket + https://github.com/YunaiV/ruoyi-vue-pro + + + + + + cn.iocoder.boot + yudao-common + + + + cn.iocoder.boot + yudao-spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-websocket + + + + \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java new file mode 100644 index 000000000..02c3415d5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java @@ -0,0 +1,14 @@ +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(); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java new file mode 100644 index 000000000..0ab1b498f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.framework.websocket.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * WebSocket 配置项 + * + * @author xingyu4j + */ +@ConfigurationProperties("yudao.websocket") +@Data +@Validated +public class WebSocketProperties { + + /** + * 路径 + */ + private String path = ""; + /** + * 默认最多允许同时在线用户数 + */ + private int maxOnlineCount = 0; + /** + * 是否保存session + */ + private boolean sessionMap = true; +} diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java new file mode 100644 index 000000000..f8c50ae6a --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java @@ -0,0 +1,34 @@ +package cn.iocoder.yudao.framework.websocket.config; + +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.web.socket.WebSocketHandler; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.List; + +/** + * WebSocket 自动配置 + * + * @author xingyu4j + */ +@AutoConfiguration +// 允许使用 yudao.websocket.enable=false 禁用websocket +@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true) +@EnableConfigurationProperties(WebSocketProperties.class) +public class YudaoWebSocketAutoConfiguration { + @Bean + @ConditionalOnMissingBean + public WebSocketConfigurer webSocketConfigurer(List handshakeInterceptor, + WebSocketHandler webSocketHandler, + WebSocketProperties webSocketProperties) { + + return registry -> registry + .addHandler(webSocketHandler, webSocketProperties.getPath()) + .addInterceptors(handshakeInterceptor.toArray(new HandshakeInterceptor[0])); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java new file mode 100644 index 000000000..3f2fa4ec3 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java @@ -0,0 +1,24 @@ +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 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) { + + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java new file mode 100644 index 000000000..f75ebc41c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java @@ -0,0 +1,9 @@ +package cn.iocoder.yudao.framework.websocket.core; + + +import lombok.Data; + +@Data +public class WebSocketKeyDefine { + public static final String LOGIN_USER ="LOGIN_USER"; +} diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java new file mode 100644 index 000000000..7bb348e99 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java @@ -0,0 +1,24 @@ +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 seesionKeyList; + /** + * 发送消息 + */ + private String msgText; + + public static WebSocketMessageDO build(List seesionKeyList, String msgText) { + return new WebSocketMessageDO().setMsgText(msgText).setSeesionKeyList(seesionKeyList); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java new file mode 100644 index 000000000..2747f8192 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java @@ -0,0 +1,36 @@ +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 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 getSessions() { + return SESSION_MAP.values(); + } + + public static Set getSessionKeys() { + return SESSION_MAP.keySet(); + } + +} diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java new file mode 100644 index 000000000..816e664cc --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java @@ -0,0 +1,31 @@ +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); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java new file mode 100644 index 000000000..dd8dc602e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java @@ -0,0 +1,49 @@ +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; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java new file mode 100644 index 000000000..c771dfaac --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java @@ -0,0 +1 @@ +package cn.iocoder.yudao.framework.websocket; diff --git a/yudao-framework/yudao-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..6260e407e --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +cn.iocoder.yudao.framework.websocket.config.YudaoWebSocketAutoConfiguration \ No newline at end of file diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/application-unit-test.yaml b/yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/application-unit-test.yaml index 25bfe0a62..1bbe0f530 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/application-unit-test.yaml +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/test/resources/application-unit-test.yaml @@ -9,7 +9,7 @@ spring: # 数据源配置项 datasource: name: ruoyi-vue-pro - url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 driver-class-name: org.h2.Driver username: sa password: diff --git a/yudao-module-infra/yudao-module-infra-biz/pom.xml b/yudao-module-infra/yudao-module-infra-biz/pom.xml index db23e697d..6d7ac0cff 100644 --- a/yudao-module-infra/yudao-module-infra-biz/pom.xml +++ b/yudao-module-infra/yudao-module-infra-biz/pom.xml @@ -111,6 +111,12 @@ cn.iocoder.boot yudao-spring-boot-starter-file + + + + org.springframework.boot + spring-boot-starter-websocket + diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/ConfigController.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/ConfigController.java index 726a10498..493b5faa6 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/ConfigController.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/ConfigController.java @@ -75,7 +75,7 @@ public class ConfigController { if (config == null) { return null; } - if (config.getVisible()) { + if (!config.getVisible()) { throw ServiceExceptionUtil.exception(ErrorCodeConstants.CONFIG_GET_VALUE_ERROR_IF_VISIBLE); } return success(config.getValue()); diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentDAOImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentDAOImpl.java index c4dcfe8a0..2492c803d 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentDAOImpl.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileContentDAOImpl.java @@ -1,11 +1,14 @@ package cn.iocoder.yudao.module.infra.dal.mysql.file; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.file.core.client.db.DBFileContentFrameworkDAO; import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileContentDO; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.stereotype.Repository; import javax.annotation.Resource; +import java.util.List; +import java.util.Optional; @Repository public class FileContentDAOImpl implements DBFileContentFrameworkDAO { @@ -27,9 +30,11 @@ public class FileContentDAOImpl implements DBFileContentFrameworkDAO { @Override public byte[] selectContent(Long configId, String path) { - FileContentDO fileContentDO = fileContentMapper.selectOne( - buildQuery(configId, path).select(FileContentDO::getContent)); - return fileContentDO != null ? fileContentDO.getContent() : null; + List list = fileContentMapper.selectList( + buildQuery(configId, path).select(FileContentDO::getContent).orderByDesc(FileContentDO::getId)); + return Optional.ofNullable(CollUtil.getFirst(list)) + .map(FileContentDO::getContent) + .orElse(null); } private LambdaQueryWrapper buildQuery(Long configId, String path) { diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/SemaphoreUtils.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/SemaphoreUtils.java new file mode 100644 index 000000000..67a87f169 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/SemaphoreUtils.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.module.infra.websocket; + +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.Semaphore; + +/** + * 信号量相关处理 + * + */ +@Slf4j +public class SemaphoreUtils { + + /** + * 获取信号量 + * + * @param semaphore + * @return + */ + public static boolean tryAcquire(Semaphore semaphore) { + boolean flag = false; + + try { + flag = semaphore.tryAcquire(); + } catch (Exception e) { + log.error("获取信号量异常", e); + } + + return flag; + } + + /** + * 释放信号量 + * + * @param semaphore + */ + public static void release(Semaphore semaphore) { + + try { + semaphore.release(); + } catch (Exception e) { + log.error("释放信号量异常", e); + } + } +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketConfig.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketConfig.java new file mode 100644 index 000000000..380bc9317 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketConfig.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.infra.websocket; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +/** + * websocket 配置 + */ +@Configuration +public class WebSocketConfig { + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketServer.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketServer.java new file mode 100644 index 000000000..f0cfdd9dc --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketServer.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.module.infra.websocket; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.websocket.*; +import javax.websocket.server.ServerEndpoint; +import java.util.concurrent.Semaphore; + +/** + * websocket 消息处理 + */ +@Component +@ServerEndpoint("/websocket/message") +@Slf4j +public class WebSocketServer { + + /** + * 默认最多允许同时在线用户数100 + */ + public static int socketMaxOnlineCount = 100; + + private static final Semaphore SOCKET_SEMAPHORE = new Semaphore(socketMaxOnlineCount); + + /** + * 连接建立成功调用的方法 + */ + @OnOpen + public void onOpen(Session session) throws Exception { + // 尝试获取信号量 + boolean semaphoreFlag = SemaphoreUtils.tryAcquire(SOCKET_SEMAPHORE); + if (!semaphoreFlag) { + // 未获取到信号量 + log.error("当前在线人数超过限制数:{}", socketMaxOnlineCount); + WebSocketUsers.sendMessage(session, "当前在线人数超过限制数:" + socketMaxOnlineCount); + session.close(); + } else { + String userId = WebSocketUsers.getParam("userId", session); + if (userId != null) { + // 添加用户 + WebSocketUsers.addSession(userId, session); + log.info("用户【userId={}】建立连接,当前连接用户总数:{}", userId, WebSocketUsers.getUsers().size()); + WebSocketUsers.sendMessage(session, "接收内容:连接成功"); + } else { + WebSocketUsers.sendMessage(session, "接收内容:连接失败"); + } + } + } + + /** + * 连接关闭时处理 + */ + @OnClose + public void onClose(Session session) { + log.info("用户【sessionId={}】关闭连接!", session.getId()); + // 移除用户 + WebSocketUsers.removeSession(session); + // 获取到信号量则需释放 + SemaphoreUtils.release(SOCKET_SEMAPHORE); + } + + /** + * 抛出异常时处理 + */ + @OnError + public void onError(Session session, Throwable exception) throws Exception { + if (session.isOpen()) { + // 关闭连接 + session.close(); + } + String sessionId = session.getId(); + log.info("用户【sessionId={}】连接异常!异常信息:{}", sessionId, exception); + // 移出用户 + WebSocketUsers.removeSession(session); + // 获取到信号量则需释放 + SemaphoreUtils.release(SOCKET_SEMAPHORE); + } + + /** + * 收到客户端消息时调用的方法 + */ + @OnMessage + public void onMessage(Session session, String message) { + WebSocketUsers.sendMessage(session, "接收内容:" + message); + } +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketUsers.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketUsers.java new file mode 100644 index 000000000..281a97c7d --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketUsers.java @@ -0,0 +1,178 @@ +package cn.iocoder.yudao.module.infra.websocket; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.util.Strings; + +import javax.validation.constraints.NotNull; +import javax.websocket.Session; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * websocket 客户端用户 + */ +@Slf4j +public class WebSocketUsers { + + /** + * 用户集 + * TODO 需要登录用户的session? + */ + private static final Map SESSION_MAP = new ConcurrentHashMap<>(); + + /** + * 存储用户 + * + * @param userId 唯一键 + * @param session 用户信息 + */ + public static void addSession(String userId, Session session) { + SESSION_MAP.put(userId, session); + } + + /** + * 移除用户 + * + * @param session 用户信息 + * @return 移除结果 + */ + public static boolean removeSession(Session session) { + String key = null; + boolean flag = SESSION_MAP.containsValue(session); + if (flag) { + Set> entries = SESSION_MAP.entrySet(); + for (Map.Entry entry : entries) { + Session value = entry.getValue(); + if (value.equals(session)) { + key = entry.getKey(); + break; + } + } + } else { + return true; + } + return removeSession(key); + } + + /** + * 移出用户 + * + * @param userId 用户id + */ + public static boolean removeSession(String userId) { + log.info("用户【userId={}】退出", userId); + Session remove = SESSION_MAP.remove(userId); + if (remove != null) { + boolean containsValue = SESSION_MAP.containsValue(remove); + log.info("用户【userId={}】退出{},当前连接用户总数:{}", userId, containsValue ? "失败" : "成功", SESSION_MAP.size()); + return containsValue; + } else { + return true; + } + } + + /** + * 获取在线用户列表 + * + * @return 返回用户集合 + */ + public static Map getUsers() { + return SESSION_MAP; + } + + /** + * 向所有在线人发送消息 + * + * @param message 消息内容 + */ + public static void sendMessageToAll(String message) { + SESSION_MAP.forEach((userId, session) -> { + if (session.isOpen()) { + sendMessage(session, message); + } + }); + } + + /** + * 异步发送文本消息 + * + * @param session 用户session + * @param message 消息内容 + */ + public static void sendMessageAsync(Session session, String message) { + if (session.isOpen()) { + // TODO 需要加synchronized锁(synchronized(session))?单个session创建线程? + session.getAsyncRemote().sendText(message); + } else { + log.warn("用户【session={}】不在线", session.getId()); + } + } + + /** + * 同步发送文本消息 + * + * @param session 用户session + * @param message 消息内容 + */ + public static void sendMessage(Session session, String message) { + try { + if (session.isOpen()) { + // TODO 需要加synchronized锁(synchronized(session))?单个session创建线程? + session.getBasicRemote().sendText(message); + } else { + log.warn("用户【session={}】不在线", session.getId()); + } + } catch (IOException e) { + log.error("发送消息异常", e); + } + + } + + /** + * 根据用户id发送消息 + * + * @param userId 用户id + * @param message 消息内容 + */ + public static void sendMessage(String userId, String message) { + Session session = SESSION_MAP.get(userId); + //判断是否存在该用户的session,并且是否在线 + if (session == null || !session.isOpen()) { + return; + } + sendMessage(session, message); + } + + + /** + * 获取session中的指定参数值 + * + * @param key 参数key + * @param session 用户session + */ + public static String getParam(@NotNull String key, Session session) { + //TODO 目前只针对获取一个key的值,后期根据情况拓展多个 或者直接在onClose onOpen上获取参数? + String value = null; + Map> parameters = session.getRequestParameterMap(); + if (MapUtil.isNotEmpty(parameters)) { + value = parameters.get(key).get(0); + } else { + String queryString = session.getQueryString(); + if (!StrUtil.isEmpty(queryString)) { + String[] params = Strings.split(queryString, '&'); + for (String paramPair : params) { + String[] nameValues = Strings.split(paramPair, '='); + if (key.equals(nameValues[0])) { + value = nameValues[1]; + } + } + } + } + return value; + } +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm index 06fe3002a..6b520c2c3 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm @@ -1,7 +1,7 @@