diff --git a/.gitignore b/.gitignore index 125a2c39..ac7c8134 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,5 @@ logs docker/*/data/ docker/minio/config -docker/xxljob/logs \ No newline at end of file +docker/xxljob/logs +application-youlai.yml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index cf008264..842060dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,23 @@ # 基础镜像 -FROM openjdk:17-jdk-alpine +FROM openjdk:17 # 维护者信息 MAINTAINER youlai -# 设置国内镜像源(中国科技大学镜像源),修改容器时区(alpine镜像需安装tzdata来设置时区),安装字体库(验证码) -RUN echo -e https://mirrors.ustc.edu.cn/alpine/v3.7/main/ > /etc/apk/repositories \ - && apk --no-cache add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo "Asia/Shanghai" > /etc/timezone \ - && apk --no-cache add ttf-dejavu fontconfig +# 设置时区(Debian直接使用环境变量) +ENV TZ=Asia/Shanghai -# 在运行时自动挂载 /tmp 目录为匿名卷,提高可移植性。如果 /tmp 目录没有挂载为卷,这些文件会写入容器的可写层,可能导致容器镜像膨胀。 +# 在运行时自动挂载 /tmp 目录为匿名卷 VOLUME /tmp -# 将构建的 Spring Boot 可执行 JAR 复制到容器中,重命名为 app.jar +# 添加应用 ADD target/youlai-boot.jar app.jar -# 指定容器启动时执行的命令 +# 启动命令 CMD java \ -Xms512m -Xmx512m \ -Djava.security.egd=file:/dev/./urandom \ -jar /app.jar -# 暴露容器的端口 +# 暴露端口 EXPOSE 8989 diff --git a/README.md b/README.md index c649f243..bdbef79a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,34 @@ +
+ +## 🎉 正在参加 Gitee 2025 最受欢迎开源软件评选 + + + 投票 + + +

+ 一票就够,不用每天投 🙏 您的支持是我们持续更新的最大动力! +

+ +

+ + 👉 点击徽章或这里投票 👈 + +

+ +
+ +![](https://foruda.gitee.com/images/1708618984641188532/a7cca095_716974.png "rainbow.png") +
logo

youlai-boot

有来技术 - 有来技术 + 有来技术 + + 有来技术 + 有来技术 @@ -37,8 +62,8 @@ ## 🌈 项目源码 -| 项目类型 | Gitee | Github | GitCode | -| --------------| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | +| 项目类型 | Gitee | Github | GitCode | +| --------------| ------------------------- | ------------------------------- | ------------------------------------- | | ✅ Java 后端 | [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) | [youlai-boot](https://github.com/haoxianrui/youlai-boot) | [youlai-boot](https://gitcode.com/youlai/youlai-boot) | | vue3 前端 | [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) | [vue3-element-admin](https://github.com/youlaitech/vue3-element-admin) | [vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) | | uni-app 移动端 | [vue-uniapp-template](https://gitee.com/youlaiorg/vue-uniapp-template) | [vue-uniapp-template](https://github.com/youlaitech/vue-uniapp-template) | [vue-uniapp-template](https://gitcode.com/youlai/vue-uniapp-template) | @@ -70,47 +95,55 @@ youlai-boot ├── sql # SQL脚本 │ ├── mysql # MySQL 脚本 ├── src # 源码目录 +│ ├── auth # 认证模块(登录入口) │ ├── common # 公共模块 │ │ ├── annotation # 注解定义 │ │ ├── base # 基础类 │ │ ├── constant # 常量 │ │ ├── enums # 枚举类型 -│ │ ├── exception # 异常处理 │ │ ├── model # 数据模型 -│ │ ├── result # 结果封装 │ │ └── util # 工具类 │ ├── config # 自动装配配置 │ │ └── property # 配置属性目录 -│ ├── core # 核心功能 +│ ├── core # 核心框架 │ │ ├── aspect # 切面(日志、防重提交) +│ │ ├── exception # 异常处理 │ │ ├── filter # 过滤器(请求日志、限流) -│ │ ├── handler # 处理器(数据权限、数据填充) -│ │ └── security # Spring Security 安全模块 -│ ├── modules # 业务模块 -│ │ ├── member # 会员模块【业务模块演示】 -│ │ ├── order # 订单模块【业务模块演示】 -│ │ ├── product # 商品模块【业务模块演示】 -│ ├── shared # 共享模块 -│ │ ├── auth # 认证模块 -│ │ ├── file # 文件模块 +│ │ ├── validator # 验证器 +│ │ └── web # Web响应封装(Result、PageResult等) +│ ├── platform # 平台服务(通用服务) │ │ ├── codegen # 代码生成模块 -│ │ ├── mail # 邮件模块 -│ │ ├── sms # 短信模块 -│ │ └── websocket # WebSocket 模块 +│ │ ├── file # 文件服务 +│ │ ├── mail # 邮件服务 +│ │ ├── sms # 短信服务 +│ │ └── websocket # WebSocket服务 +│ ├── plugin # 插件扩展 +│ │ ├── knife4j # Knife4j 扩展 +│ │ └── mybatis # Mybatis 扩展 +│ ├── security # 安全框架(Spring Security) +│ │ ├── exception # 安全异常 +│ │ ├── filter # 安全过滤器 +│ │ ├── handler # 安全处理器 +│ │ ├── model # 安全模型 +│ │ ├── provider # 认证提供者 +│ │ ├── service # 安全服务 +│ │ ├── token # Token管理 +│ │ └── util # 安全工具类 │ ├── system # 系统模块 │ │ ├── controller # 控制层 │ │ ├── converter # MapStruct 转换器 -│ │ ├── event # 事件处理 +│ │ ├── enums # 枚举 │ │ ├── handler # 处理器 │ │ ├── listener # 监听器 +│ │ ├── mapper # 数据库访问层 │ │ ├── model # 模型层 │ │ │ ├── bo # 业务对象 │ │ │ ├── dto # 数据传输对象 │ │ │ ├── entity # 实体对象 +│ │ │ ├── event # 事件对象 │ │ │ ├── form # 表单对象 │ │ │ ├── query # 查询参数对象 │ │ │ └── vo # 视图对象 -│ │ ├── mapper # 数据库访问层 │ │ └── service # 业务逻辑层 │ └── YouLaiBootApplication # 启动类 └── end diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index f27b011e..2144edd7 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -37,7 +37,7 @@ services: - youlai-boot minio: - image: minio/minio:latest + image: minio/minio:RELEASE.2024-07-16T23-46-41Z container_name: minio restart: unless-stopped command: server /data --console-address ":9001" @@ -66,4 +66,4 @@ services: ports: - 8080:8080 networks: - - youlai-boot \ No newline at end of file + - youlai-boot diff --git a/docker/redis/config/redis.conf b/docker/redis/config/redis.conf index 36aaf08e..272c8486 100644 --- a/docker/redis/config/redis.conf +++ b/docker/redis/config/redis.conf @@ -133,7 +133,7 @@ protected-mode no # # enable-protected-configs no # enable-debug-command no -# enable-module-command no +# enable-business-command no # Accept connections on the specified port, default is 6379 (IANA #815344). # If port 0 is specified Redis will not listen on a TCP socket. @@ -867,7 +867,7 @@ replica-priority 100 # Warning: since Redis is pretty fast, an outside user can try up to # 1 million passwords per second against a modern box. This means that you # should use very strong passwords, otherwise they will be very easy to break. -# Note that because the password is really a shared secret between the client +# Note that because the password is really a platform secret between the client # and the server, and should not be memorized by any human, the password # can be easily a long string from /dev/urandom or whatever, so by using a # long and unguessable password no brute force attack will be possible. @@ -964,7 +964,7 @@ replica-priority 100 # # user alice on +@all -DEBUG ~* >somepassword # -# This will allow "alice" to use all the commands with the exception of the +# This will allow "alice" to use all the commands with the handler of the # DEBUG command, since +@all added all the commands to the set of the commands # alice can use, and later DEBUG was removed. However if we invert the order # of two ACL rules the result will be different: @@ -1066,7 +1066,7 @@ acllog-max-len 128 # create for administrative purposes. # ------------------------------------------------------------------------ # -# It is possible to change the name of dangerous commands in a shared +# It is possible to change the name of dangerous commands in a platform # environment. For instance the CONFIG command may be renamed into something # hard to guess so that it will still be available for internal-use tools # but not available for general clients. @@ -1095,7 +1095,7 @@ acllog-max-len 128 # an error 'max number of clients reached'. # # IMPORTANT: When Redis Cluster is used, the max number of connections is also -# shared with the cluster bus: every node in the cluster will use two +# platform with the cluster bus: every node in the cluster will use two # connections, one incoming and another outgoing. It is important to size the # limit accordingly in case of very large clusters. # @@ -1563,7 +1563,7 @@ aof-timestamp-enabled no # # In this state Redis will only allow a handful of commands to be executed. # For instance, SCRIPT KILL, FUNCTION KILL, SHUTDOWN NOSAVE and possibly some -# module specific 'allow-busy' commands. +# business specific 'allow-busy' commands. # # SCRIPT KILL and FUNCTION KILL will only be able to stop a script that did not # yet call any write commands, so SHUTDOWN NOSAVE may be the only way to stop diff --git a/pom.xml b/pom.xml index cdae3095..4d1eb2f6 100644 --- a/pom.xml +++ b/pom.xml @@ -6,13 +6,13 @@ com.youlai youlai-boot - 2.22.0 + 3.3.0 基于 Java 17 + SpringBoot 3 + Spring Security 构建的权限管理系统。 org.springframework.boot spring-boot-starter-parent - 3.3.6 + 3.5.6 @@ -25,15 +25,16 @@ 9.1.0 1.2.24 3.5.5 + 4.3.1 4.5.0 1.6.3 0.2.0 - 2.4.2 + 3.2.0 - 1.1.0 + 1.3.0 8.5.10 @@ -42,7 +43,7 @@ 3.16.3 - 3.40.2 + 3.51.0 3.5.6 @@ -52,11 +53,11 @@ 2.7.0 - 4.6.4 + 4.7.6 2.2.1 - 4.5.5.B + 4.7.7.B 2.9.3 @@ -152,6 +153,17 @@ com.github.xiaoymin knife4j-openapi3-jakarta-spring-boot-starter ${knife4j.version} + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.8.9 @@ -248,6 +260,13 @@ ${caffeine.version} + + + @@ -260,4 +279,4 @@ - \ No newline at end of file + diff --git a/sql/mysql/youlai_boot.sql b/sql/mysql/youlai_boot.sql index a6402555..aedf2e54 100644 --- a/sql/mysql/youlai_boot.sql +++ b/sql/mysql/youlai_boot.sql @@ -143,8 +143,8 @@ INSERT INTO `sys_menu` VALUES (4, 1, '0,1', '菜单管理', 1, 'SysMenu', 'menu' INSERT INTO `sys_menu` VALUES (5, 1, '0,1', '部门管理', 1, 'Dept', 'dept', 'system/dept/index', NULL, NULL, 1, 1, 4, 'tree', NULL, now(), now(), NULL); INSERT INTO `sys_menu` VALUES (6, 1, '0,1', '字典管理', 1, 'Dict', 'dict', 'system/dict/index', NULL, NULL, 1, 1, 5, 'dict', NULL, now(), now(), NULL); INSERT INTO `sys_menu` VALUES (20, 0, '0', '多级菜单', 2, NULL, '/multi-level', 'Layout', NULL, 1, NULL, 1, 9, 'cascader', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (21, 20, '0,20', '菜单一级', 1, NULL, 'multi-level1', 'demo/multi-level/level1', NULL, 1, NULL, 1, 1, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (22, 21, '0,20,21', '菜单二级', 1, NULL, 'multi-level2', 'demo/multi-level/children/level2', NULL, 0, NULL, 1, 1, '', NULL, now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (21, 20, '0,20', '菜单一级', 2, NULL, 'multi-level1', 'Layout', NULL, 1, NULL, 1, 1, '', '', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (22, 21, '0,20,21', '菜单二级', 2, NULL, 'multi-level2', 'Layout', NULL, 0, NULL, 1, 1, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` VALUES (23, 22, '0,20,21,22', '菜单三级-1', 1, NULL, 'multi-level3-1', 'demo/multi-level/children/children/level3-1', NULL, 0, 1, 1, 1, '', '', now(), now(), NULL); INSERT INTO `sys_menu` VALUES (24, 22, '0,20,21,22', '菜单三级-2', 1, NULL, 'multi-level3-2', 'demo/multi-level/children/children/level3-2', NULL, 0, 1, 1, 2, '', '', now(), now(), NULL); INSERT INTO `sys_menu` VALUES (26, 0, '0', '平台文档', 2, '', '/doc', 'Layout', NULL, NULL, NULL, 1, 8, 'document', 'https://juejin.cn/post/7228990409909108793', now(), now(), NULL); @@ -153,11 +153,11 @@ INSERT INTO `sys_menu` VALUES (31, 2, '0,1,2', '用户新增', 4, NULL, '', NULL INSERT INTO `sys_menu` VALUES (32, 2, '0,1,2', '用户编辑', 4, NULL, '', NULL, 'sys:user:edit', NULL, NULL, 1, 2, '', '', now(), now(), NULL); INSERT INTO `sys_menu` VALUES (33, 2, '0,1,2', '用户删除', 4, NULL, '', NULL, 'sys:user:delete', NULL, NULL, 1, 3, '', '', now(), now(), NULL); INSERT INTO `sys_menu` VALUES (36, 0, '0', '组件封装', 2, NULL, '/component', 'Layout', NULL, NULL, NULL, 1, 10, 'menu', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (37, 36, '0,36', '富文本编辑器', 1, NULL, 'wang-editor', 'demo/wang-editor', NULL, NULL, 1, 1, 2, '', '', NULL, NULL, NULL); -INSERT INTO `sys_menu` VALUES (38, 36, '0,36', '图片上传', 1, NULL, 'upload', 'demo/upload', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (39, 36, '0,36', '图标选择器', 1, NULL, 'icon-selector', 'demo/icon-selector', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (37, 36, '0,36', '富文本编辑器', 1, 'WangEditor', 'wang-editor', 'demo/wang-editor', NULL, NULL, 1, 1, 2, '', '', NULL, NULL, NULL); +INSERT INTO `sys_menu` VALUES (38, 36, '0,36', '图片上传', 1, 'Upload', 'upload', 'demo/upload', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (39, 36, '0,36', '图标选择器', 1, 'IconSelect', 'icon-select', 'demo/icon-select', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL); INSERT INTO `sys_menu` VALUES (40, 0, '0', '接口文档', 2, NULL, '/api', 'Layout', NULL, 1, NULL, 1, 7, 'api', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (41, 40, '0,40', 'Apifox', 1, NULL, 'apifox', 'demo/api/apifox', NULL, NULL, 1, 1, 1, 'api', '', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (41, 40, '0,40', 'Apifox', 1, 'Apifox', 'apifox', 'demo/api/apifox', NULL, NULL, 1, 1, 1, 'api', '', now(), now(), NULL); INSERT INTO `sys_menu` VALUES (70, 3, '0,1,3', '角色新增', 4, NULL, '', NULL, 'sys:role:add', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` VALUES (71, 3, '0,1,3', '角色编辑', 4, NULL, '', NULL, 'sys:role:edit', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` VALUES (72, 3, '0,1,3', '角色删除', 4, NULL, '', NULL, 'sys:role:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); @@ -172,19 +172,18 @@ INSERT INTO `sys_menu` VALUES (81, 6, '0,1,6', '字典编辑', 4, NULL, '', NULL INSERT INTO `sys_menu` VALUES (84, 6, '0,1,6', '字典删除', 4, NULL, '', NULL, 'sys:dict:delete', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` VALUES (88, 2, '0,1,2', '重置密码', 4, NULL, '', NULL, 'sys:user:reset-password', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` VALUES (89, 0, '0', '功能演示', 2, NULL, '/function', 'Layout', NULL, NULL, NULL, 1, 12, 'menu', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (90, 89, '0,89', 'Websocket', 1, NULL, '/function/websocket', 'demo/websocket', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (91, 89, '0,89', '敬请期待...', 2, NULL, 'other/:id', 'demo/other', NULL, NULL, NULL, 1, 4, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (95, 36, '0,36', '字典组件', 1, NULL, 'dict-demo', 'demo/dictionary', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (97, 89, '0,89', 'Icons', 1, NULL, 'icon-demo', 'demo/icons', NULL, NULL, 1, 1, 2, 'el-icon-Notification', '', now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (102, 26, '0,26', 'document', 3, '', 'internal-doc', 'demo/internal-doc', NULL, NULL, NULL, 1, 1, 'document', '', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (90, 89, '0,89', 'Websocket', 1, 'WebSocket', '/function/websocket', 'demo/websocket', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (95, 36, '0,36', '字典组件', 1, 'DictDemo', 'dict-demo', 'demo/dictionary', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (97, 89, '0,89', 'Icons', 1, 'IconDemo', 'icon-demo', 'demo/icons', NULL, NULL, 1, 1, 2, 'el-icon-Notification', '', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (102, 26, '0,26', 'document', 3, NULL, 'internal-doc', 'demo/internal-doc', NULL, NULL, NULL, 1, 1, 'document', '', now(), now(), NULL); INSERT INTO `sys_menu` VALUES (105, 2, '0,1,2', '用户查询', 4, NULL, '', NULL, 'sys:user:query', 0, 0, 1, 0, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` VALUES (106, 2, '0,1,2', '用户导入', 4, NULL, '', NULL, 'sys:user:import', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` VALUES (107, 2, '0,1,2', '用户导出', 4, NULL, '', NULL, 'sys:user:export', NULL, NULL, 1, 6, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (108, 36, '0,36', '增删改查', 1, NULL, 'curd', 'demo/curd/index', NULL, NULL, 1, 1, 0, '', '', NULL, NULL, NULL); -INSERT INTO `sys_menu` VALUES (109, 36, '0,36', '列表选择器', 1, NULL, 'table-select', 'demo/table-select/index', NULL, NULL, 1, 1, 1, '', '', NULL, NULL, NULL); +INSERT INTO `sys_menu` VALUES (108, 36, '0,36', '增删改查', 1, 'Curd', 'curd', 'demo/curd/index', NULL, NULL, 1, 1, 0, '', '', NULL, NULL, NULL); +INSERT INTO `sys_menu` VALUES (109, 36, '0,36', '列表选择器', 1, 'TableSelect', 'table-select', 'demo/table-select/index', NULL, NULL, 1, 1, 1, '', '', NULL, NULL, NULL); INSERT INTO `sys_menu` VALUES (110, 0, '0', '路由参数', 2, NULL, '/route-param', 'Layout', NULL, 1, 1, 1, 11, 'el-icon-ElementPlus', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (111, 110, '0,110', '参数(type=1)', 1, NULL, 'route-param-type1', 'demo/route-param', NULL, 0, 1, 1, 1, 'el-icon-Star', NULL, now(), now(), '{\"type\": \"1\"}'); -INSERT INTO `sys_menu` VALUES (112, 110, '0,110', '参数(type=2)', 1, NULL, 'route-param-type2', 'demo/route-param', NULL, 0, 1, 1, 2, 'el-icon-StarFilled', NULL, now(), now(), '{\"type\": \"2\"}'); +INSERT INTO `sys_menu` VALUES (111, 110, '0,110', '参数(type=1)', 1, 'RouteParamType1', 'route-param-type1', 'demo/route-param', NULL, 0, 1, 1, 1, 'el-icon-Star', NULL, now(), now(), '{\"type\": \"1\"}'); +INSERT INTO `sys_menu` VALUES (112, 110, '0,110', '参数(type=2)', 1, 'RouteParamType2', 'route-param-type2', 'demo/route-param', NULL, 0, 1, 1, 2, 'el-icon-StarFilled', NULL, now(), now(), '{\"type\": \"2\"}'); INSERT INTO `sys_menu` VALUES (117, 1, '0,1', '系统日志', 1, 'Log', 'log', 'system/log/index', NULL, 0, 1, 1, 6, 'document', NULL, now(), now(), NULL); INSERT INTO `sys_menu` VALUES (118, 0, '0', '系统工具', 2, NULL, '/codegen', 'Layout', NULL, 0, 1, 1, 2, 'menu', NULL, now(), now(), NULL); INSERT INTO `sys_menu` VALUES (119, 118, '0,118', '代码生成', 1, 'Codegen', 'codegen', 'codegen/index', NULL, 0, 1, 1, 1, 'code', NULL, now(), now(), NULL); @@ -210,10 +209,15 @@ INSERT INTO `sys_menu` VALUES (140, 4, '0,1,4', '菜单查询', 4, NULL, '', NUL INSERT INTO `sys_menu` VALUES (141, 5, '0,1,5', '部门查询', 4, NULL, '', NULL, 'sys:dept:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` VALUES (142, 6, '0,1,6', '字典查询', 4, NULL, '', NULL, 'sys:dict:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); INSERT INTO `sys_menu` VALUES (143, 135, '0,1,135', '字典项查询', 4, NULL, '', NULL, 'sys:dict-item:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); -INSERT INTO `sys_menu` VALUES (144, 26, '0,26', '后端文档', 3, NULL, 'https://youlai.blog.csdn.net/article/details/145178880', '', NULL, NULL, NULL, 1, 3, 'document', '', '2024-10-05 23:36:03', '2024-10-05 23:36:03', NULL); -INSERT INTO `sys_menu` VALUES (145, 26, '0,26', '移动端文档', 3, NULL, 'https://youlai.blog.csdn.net/article/details/143222890', '', NULL, NULL, NULL, 1, 4, 'document', '', '2024-10-05 23:36:03', '2024-10-05 23:36:03', NULL); -INSERT INTO `sys_menu` VALUES (146, 36, '0,36', '拖拽组件', 1, NULL, 'drag', 'demo/drag', NULL, NULL, NULL, 1, 5, '', '', '2025-03-31 14:14:45', '2025-03-31 14:14:52', NULL); -INSERT INTO `sys_menu` VALUES (147, 36, '0,36', '滚动文本', 1, NULL, 'text-scroll', 'demo/text-scroll', NULL, NULL, NULL, 1, 6, '', '', '2025-03-31 14:14:49', '2025-03-31 14:14:56', NULL); +INSERT INTO `sys_menu` VALUES (144, 26, '0,26', '后端文档', 3, NULL, 'https://youlai.blog.csdn.net/article/details/145178880', '', NULL, NULL, NULL, 1, 3, 'document', '', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (145, 26, '0,26', '移动端文档', 3, NULL, 'https://youlai.blog.csdn.net/article/details/143222890', '', NULL, NULL, NULL, 1, 4, 'document', '', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (146, 36, '0,36', '拖拽组件', 1, 'Drag', 'drag', 'demo/drag', NULL, NULL, NULL, 1, 5, '', '', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (147, 36, '0,36', '滚动文本', 1, 'TextScroll', 'text-scroll', 'demo/text-scroll', NULL, NULL, NULL, 1, 6, '', '', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (148, 89, '0,89', '字典实时同步', 1, 'DictSync', 'dict-sync', 'demo/dict-sync', NULL, NULL, NULL, 1, 3, '', '', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (149, 89, '0,89', 'VxeTable', 1, 'VxeTable', 'vxe-table', 'demo/vxe-table/index', NULL, NULL, 1, 1, 0, 'el-icon-MagicStick', '', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (150, 36, '0,36', '自适应表格操作列', 1, 'AutoOperationColumn', 'operation-column', 'demo/auto-operation-column', NULL, NULL, 1, 1, 1, '', '', now(), now(), NULL); +INSERT INTO `sys_menu` VALUES (151, 89, '0,89', 'CURD单文件', 1, 'CurdSingle', 'curd-single', 'demo/curd-single', NULL, NULL, 1, 1, 7, 'el-icon-Reading', '', now(),now(), NULL); + -- ---------------------------- -- Table structure for sys_role @@ -351,6 +355,10 @@ INSERT INTO `sys_role_menu` VALUES (2, 144); INSERT INTO `sys_role_menu` VALUES (2, 145); INSERT INTO `sys_role_menu` VALUES (2, 146); INSERT INTO `sys_role_menu` VALUES (2, 147); +INSERT INTO `sys_role_menu` VALUES (2, 148); +INSERT INTO `sys_role_menu` VALUES (2, 149); +INSERT INTO `sys_role_menu` VALUES (2, 150); +INSERT INTO `sys_role_menu` VALUES (2, 151); -- ---------------------------- -- Table structure for sys_user @@ -374,7 +382,7 @@ CREATE TABLE `sys_user` ( `is_deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)', `openid` char(28) COMMENT '微信 openid', PRIMARY KEY (`id`) USING BTREE, - UNIQUE INDEX `login_name`(`username` ASC) USING BTREE + KEY `login_name` (`username`) ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '用户信息表'; -- ---------------------------- @@ -442,9 +450,11 @@ CREATE TABLE `gen_config` ( `entity_name` varchar(100) NOT NULL COMMENT '实体类名', `author` varchar(50) NOT NULL COMMENT '作者', `parent_menu_id` bigint COMMENT '上级菜单ID,对应sys_menu的id ', + `remove_table_prefix` varchar(20) COMMENT '要移除的表前缀,如: sys_', + `page_type` varchar(20) COMMENT '页面类型(classic|curd)', `create_time` datetime COMMENT '创建时间', `update_time` datetime COMMENT '更新时间', - `is_deleted` bit(1) DEFAULT b'0' COMMENT '是否删除', + `is_deleted` tinyint(4) DEFAULT 0 COMMENT '是否删除', PRIMARY KEY (`id`), UNIQUE KEY `uk_tablename` (`table_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成基础配置表'; @@ -559,4 +569,4 @@ INSERT INTO `sys_user_notice` VALUES (8, 8, 2, 1, NULL, now(), now(), 0); INSERT INTO `sys_user_notice` VALUES (9, 9, 2, 1, NULL, now(), now(), 0); INSERT INTO `sys_user_notice` VALUES (10, 10, 2, 1, NULL, now(), now(), 0); -SET FOREIGN_KEY_CHECKS = 1; \ No newline at end of file +SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/main/java/com/youlai/boot/shared/auth/controller/AuthController.java b/src/main/java/com/youlai/boot/auth/controller/AuthController.java similarity index 66% rename from src/main/java/com/youlai/boot/shared/auth/controller/AuthController.java rename to src/main/java/com/youlai/boot/auth/controller/AuthController.java index c306bdee..e4d169fc 100644 --- a/src/main/java/com/youlai/boot/shared/auth/controller/AuthController.java +++ b/src/main/java/com/youlai/boot/auth/controller/AuthController.java @@ -1,18 +1,22 @@ -package com.youlai.boot.shared.auth.controller; +package com.youlai.boot.auth.controller; +import com.youlai.boot.auth.model.vo.CaptchaVO; +import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO; import com.youlai.boot.common.enums.LogModuleEnum; -import com.youlai.boot.common.result.Result; -import com.youlai.boot.shared.auth.service.AuthService; -import com.youlai.boot.shared.auth.model.CaptchaInfo; -import com.youlai.boot.core.security.model.AuthenticationToken; +import com.youlai.boot.core.web.Result; +import com.youlai.boot.auth.service.AuthService; +import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO; import com.youlai.boot.common.annotation.Log; +import com.youlai.boot.security.model.AuthenticationToken; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; + /** * 认证控制层 * @@ -28,10 +32,10 @@ public class AuthController { private final AuthService authService; - @Operation(summary = "获取登录验证码") + @Operation(summary = "获取验证码") @GetMapping("/captcha") - public Result getCaptcha() { - CaptchaInfo captcha = authService.getCaptcha(); + public Result getCaptcha() { + CaptchaVO captcha = authService.getCaptcha(); return Result.success(captcha); } @@ -46,42 +50,6 @@ public class AuthController { return Result.success(authenticationToken); } - @Operation(summary = "注销登录") - @DeleteMapping("/logout") - @Log(value = "注销", module = LogModuleEnum.LOGIN) - public Result logout() { - authService.logout(); - return Result.success(); - } - - @Operation(summary = "刷新访问令牌") - @PostMapping("/refresh-token") - public Result refreshToken( - @Parameter(description = "刷新令牌", example = "xxx.xxx.xxx") @RequestParam String refreshToken - ) { - AuthenticationToken authenticationToken = authService.refreshToken(refreshToken); - return Result.success(authenticationToken); - } - - @Operation(summary = "微信授权登录") - @PostMapping("/login/wechat") - @Log(value = "微信登录", module = LogModuleEnum.LOGIN) - public Result loginByWechat( - @Parameter(description = "微信授权码", example = "code") @RequestParam String code - ) { - AuthenticationToken loginResult = authService.loginByWechat(code); - return Result.success(loginResult); - } - - @Operation(summary = "发送登录短信验证码") - @PostMapping("/login/sms/code") - public Result sendLoginVerifyCode( - @Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile - ) { - authService.sendSmsLoginCode(mobile); - return Result.success(); - } - @Operation(summary = "短信验证码登录") @PostMapping("/login/sms") @Log(value = "短信验证码登录", module = LogModuleEnum.LOGIN) @@ -92,4 +60,56 @@ public class AuthController { AuthenticationToken loginResult = authService.loginBySms(mobile, code); return Result.success(loginResult); } + + @Operation(summary = "发送登录短信验证码") + @PostMapping("/sms/code") + public Result sendLoginVerifyCode( + @Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile + ) { + authService.sendSmsLoginCode(mobile); + return Result.success(); + } + + @Operation(summary = "微信授权登录(Web)") + @PostMapping("/login/wechat") + @Log(value = "微信登录", module = LogModuleEnum.LOGIN) + public Result loginByWechat( + @Parameter(description = "微信授权码", example = "code") @RequestParam String code + ) { + AuthenticationToken loginResult = authService.loginByWechat(code); + return Result.success(loginResult); + } + + @Operation(summary = "微信小程序登录(Code)") + @PostMapping("/wx/miniapp/code-login") + public Result loginByWxMiniAppCode(@RequestBody @Valid WxMiniAppCodeLoginDTO loginDTO) { + AuthenticationToken token = authService.loginByWxMiniAppCode(loginDTO); + return Result.success(token); + } + + @Operation(summary = "微信小程序登录(手机号)") + @PostMapping("/wx/miniapp/phone-login") + public Result loginByWxMiniAppPhone(@RequestBody @Valid WxMiniAppPhoneLoginDTO loginDTO) { + AuthenticationToken token = authService.loginByWxMiniAppPhone(loginDTO); + return Result.success(token); + } + + + @Operation(summary = "退出登录") + @DeleteMapping("/logout") + @Log(value = "退出登录", module = LogModuleEnum.LOGIN) + public Result logout() { + authService.logout(); + return Result.success(); + } + + @Operation(summary = "刷新令牌") + @PostMapping("/refresh-token") + public Result refreshToken( + @Parameter(description = "刷新令牌", example = "xxx.xxx.xxx") @RequestParam String refreshToken + ) { + AuthenticationToken authenticationToken = authService.refreshToken(refreshToken); + return Result.success(authenticationToken); + } + } diff --git a/src/main/java/com/youlai/boot/auth/model/dto/WxMiniAppCodeLoginDTO.java b/src/main/java/com/youlai/boot/auth/model/dto/WxMiniAppCodeLoginDTO.java new file mode 100644 index 00000000..69c653fb --- /dev/null +++ b/src/main/java/com/youlai/boot/auth/model/dto/WxMiniAppCodeLoginDTO.java @@ -0,0 +1,22 @@ +package com.youlai.boot.auth.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; + +/** + * 微信小程序Code登录请求参数 + * + * @author 有来技术团队 + * @since 2.0.0 + */ +@Schema(description = "微信小程序Code登录请求参数") +@Data +public class WxMiniAppCodeLoginDTO { + + @Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "code不能为空") + private String code; + +} \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/auth/model/dto/WxMiniAppPhoneLoginDTO.java b/src/main/java/com/youlai/boot/auth/model/dto/WxMiniAppPhoneLoginDTO.java new file mode 100644 index 00000000..a881f271 --- /dev/null +++ b/src/main/java/com/youlai/boot/auth/model/dto/WxMiniAppPhoneLoginDTO.java @@ -0,0 +1,28 @@ +package com.youlai.boot.auth.model.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import jakarta.validation.constraints.NotBlank; + +/** + * 微信小程序手机号登录请求参数 + * + * @author Ray.Hao + * @since 2.0.0 + */ +@Schema(description = "微信小程序手机号登录请求参数") +@Data +public class WxMiniAppPhoneLoginDTO { + + @Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "code不能为空") + private String code; + + @Schema(description = "包括敏感数据在内的完整用户信息的加密数据") + private String encryptedData; + + @Schema(description = "加密算法的初始向量") + private String iv; + +} \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/shared/auth/model/CaptchaInfo.java b/src/main/java/com/youlai/boot/auth/model/vo/CaptchaVO.java similarity index 85% rename from src/main/java/com/youlai/boot/shared/auth/model/CaptchaInfo.java rename to src/main/java/com/youlai/boot/auth/model/vo/CaptchaVO.java index b290cc50..3f42c300 100644 --- a/src/main/java/com/youlai/boot/shared/auth/model/CaptchaInfo.java +++ b/src/main/java/com/youlai/boot/auth/model/vo/CaptchaVO.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.auth.model; +package com.youlai.boot.auth.model.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; @@ -13,7 +13,7 @@ import lombok.Data; @Schema(description = "验证码信息") @Data @Builder -public class CaptchaInfo { +public class CaptchaVO { @Schema(description = "验证码缓存 Key") private String captchaKey; diff --git a/src/main/java/com/youlai/boot/shared/auth/service/AuthService.java b/src/main/java/com/youlai/boot/auth/service/AuthService.java similarity index 60% rename from src/main/java/com/youlai/boot/shared/auth/service/AuthService.java rename to src/main/java/com/youlai/boot/auth/service/AuthService.java index 1243403f..5fe2eadf 100644 --- a/src/main/java/com/youlai/boot/shared/auth/service/AuthService.java +++ b/src/main/java/com/youlai/boot/auth/service/AuthService.java @@ -1,7 +1,9 @@ -package com.youlai.boot.shared.auth.service; +package com.youlai.boot.auth.service; -import com.youlai.boot.shared.auth.model.CaptchaInfo; -import com.youlai.boot.core.security.model.AuthenticationToken; +import com.youlai.boot.auth.model.vo.CaptchaVO; +import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO; +import com.youlai.boot.security.model.AuthenticationToken; +import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO; /** * 认证服务接口 @@ -30,7 +32,7 @@ public interface AuthService { * * @return 验证码 */ - CaptchaInfo getCaptcha(); + CaptchaVO getCaptcha(); /** * 刷新令牌 @@ -48,6 +50,22 @@ public interface AuthService { */ AuthenticationToken loginByWechat(String code); + /** + * 微信小程序Code登录 + * + * @param loginDTO 登录参数 + * @return 访问令牌 + */ + AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDTO loginDTO); + + /** + * 微信小程序手机号登录 + * + * @param loginDTO 登录参数 + * @return 访问令牌 + */ + AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDTO); + /** * 发送短信验证码 * diff --git a/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java b/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java similarity index 72% rename from src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java rename to src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java index d44b0c18..b19f68a1 100644 --- a/src/main/java/com/youlai/boot/shared/auth/service/impl/AuthServiceImpl.java +++ b/src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java @@ -1,25 +1,26 @@ -package com.youlai.boot.shared.auth.service.impl; +package com.youlai.boot.auth.service.impl; import cn.hutool.captcha.AbstractCaptcha; import cn.hutool.captcha.CaptchaUtil; import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; +import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO; +import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO; +import com.youlai.boot.auth.model.vo.CaptchaVO; +import com.youlai.boot.auth.service.AuthService; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.SecurityConstants; -import com.youlai.boot.common.exception.BusinessException; -import com.youlai.boot.common.result.ResultCode; +import com.youlai.boot.common.enums.CaptchaTypeEnum; import com.youlai.boot.config.property.CaptchaProperties; -import com.youlai.boot.core.security.extension.sms.SmsAuthenticationToken; -import com.youlai.boot.core.security.extension.wechat.WechatAuthenticationToken; -import com.youlai.boot.core.security.util.SecurityUtils; -import com.youlai.boot.shared.auth.enums.CaptchaTypeEnum; -import com.youlai.boot.core.security.model.AuthenticationToken; -import com.youlai.boot.shared.auth.model.CaptchaInfo; -import com.youlai.boot.shared.auth.service.AuthService; -import com.youlai.boot.core.security.token.TokenManager; -import com.youlai.boot.shared.sms.enums.SmsTypeEnum; -import com.youlai.boot.shared.sms.service.SmsService; +import com.youlai.boot.platform.sms.enums.SmsTypeEnum; +import com.youlai.boot.platform.sms.service.SmsService; +import com.youlai.boot.security.model.AuthenticationToken; +import com.youlai.boot.security.model.SmsAuthenticationToken; +import com.youlai.boot.security.model.WxMiniAppCodeAuthenticationToken; +import com.youlai.boot.security.model.WxMiniAppPhoneAuthenticationToken; +import com.youlai.boot.security.token.TokenManager; +import com.youlai.boot.security.util.SecurityUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; @@ -87,16 +88,16 @@ public class AuthServiceImpl implements AuthService { @Override public AuthenticationToken loginByWechat(String code) { // 1. 创建用户微信认证的令牌(未认证) - WechatAuthenticationToken wechatAuthenticationToken = new WechatAuthenticationToken(code); + WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(code); // 2. 执行认证(认证中) - Authentication authentication = authenticationManager.authenticate(wechatAuthenticationToken); + Authentication authentication = authenticationManager.authenticate(authenticationToken); // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证) - AuthenticationToken authenticationToken = tokenManager.generateToken(authentication); + AuthenticationToken token = tokenManager.generateToken(authentication); SecurityContextHolder.getContext().setAuthentication(authentication); - return authenticationToken; + return token; } /** @@ -167,7 +168,7 @@ public class AuthServiceImpl implements AuthService { * @return 验证码 */ @Override - public CaptchaInfo getCaptcha() { + public CaptchaVO getCaptcha() { String captchaType = captchaProperties.getType(); int width = captchaProperties.getWidth(); @@ -203,7 +204,7 @@ public class AuthServiceImpl implements AuthService { TimeUnit.SECONDS ); - return CaptchaInfo.builder() + return CaptchaVO.builder() .captchaKey(captchaKey) .captchaBase64(imageBase64Data) .build(); @@ -217,15 +218,53 @@ public class AuthServiceImpl implements AuthService { */ @Override public AuthenticationToken refreshToken(String refreshToken) { - // 验证刷新令牌 - boolean isValidate = tokenManager.validateRefreshToken(refreshToken); - - if (!isValidate) { - throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); - } - // 刷新令牌有效,生成新的访问令牌 return tokenManager.refreshToken(refreshToken); } + /** + * 微信小程序Code登录 + * + * @param loginDTO 登录参数 + * @return 访问令牌 + */ + @Override + public AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDTO loginDTO) { + // 1. 创建微信小程序认证令牌(未认证) + WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(loginDTO.getCode()); + + // 2. 执行认证(认证中) + Authentication authentication = authenticationManager.authenticate(authenticationToken); + + // 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证) + AuthenticationToken token = tokenManager.generateToken(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + + return token; + } + + /** + * 微信小程序手机号登录 + * + * @param loginDTO 登录参数 + * @return 访问令牌 + */ + @Override + public AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDTO) { + // 创建微信小程序手机号认证Token + WxMiniAppPhoneAuthenticationToken authenticationToken = new WxMiniAppPhoneAuthenticationToken( + loginDTO.getCode(), + loginDTO.getEncryptedData(), + loginDTO.getIv() + ); + + // 执行认证 + Authentication authentication = authenticationManager.authenticate(authenticationToken); + + // 认证成功后生成JWT令牌,并存入Security上下文 + AuthenticationToken token = tokenManager.generateToken(authentication); + SecurityContextHolder.getContext().setAuthentication(authentication); + + return token; + } } diff --git a/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java b/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java index a74ac0a3..8f6e2d4f 100644 --- a/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java +++ b/src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java @@ -10,6 +10,11 @@ package com.youlai.boot.common.constant; */ public interface JwtClaimConstants { + /** + * 令牌类型 + */ + String TOKEN_TYPE = "tokenType"; + /** * 用户ID */ diff --git a/src/main/java/com/youlai/boot/shared/auth/enums/CaptchaTypeEnum.java b/src/main/java/com/youlai/boot/common/enums/CaptchaTypeEnum.java similarity index 88% rename from src/main/java/com/youlai/boot/shared/auth/enums/CaptchaTypeEnum.java rename to src/main/java/com/youlai/boot/common/enums/CaptchaTypeEnum.java index b2f1dd4e..8d3fe10e 100644 --- a/src/main/java/com/youlai/boot/shared/auth/enums/CaptchaTypeEnum.java +++ b/src/main/java/com/youlai/boot/common/enums/CaptchaTypeEnum.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.auth.enums; +package com.youlai.boot.common.enums; /** * EasyCaptcha 验证码类型枚举 diff --git a/src/main/java/com/youlai/boot/common/util/ResponseUtils.java b/src/main/java/com/youlai/boot/common/util/ResponseUtils.java deleted file mode 100644 index d90804dc..00000000 --- a/src/main/java/com/youlai/boot/common/util/ResponseUtils.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.youlai.boot.common.util; - -import cn.hutool.json.JSONUtil; -import com.youlai.boot.common.result.Result; -import com.youlai.boot.common.result.ResultCode; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; - -import java.io.IOException; -import java.io.PrintWriter; -import java.nio.charset.StandardCharsets; - -/** - * 响应工具类 - * - * @author Ray.Hao - * @since 2.0.0 - */ -@Slf4j -public class ResponseUtils { - - - /** - * 异常消息返回(适用过滤器中处理异常响应) - * - * @param response HttpServletResponse - * @param resultCode 响应结果码 - */ - public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) { - int status = getHttpStatus(resultCode); - - response.setStatus(status); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setCharacterEncoding(StandardCharsets.UTF_8.name()); - - try (PrintWriter writer = response.getWriter()) { - String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode)); - writer.print(jsonResponse); - writer.flush(); // 确保将响应内容写入到输出流 - } catch (IOException e) { - log.error("响应异常处理失败", e); - } - } - - /** - * 异常消息返回(适用过滤器中处理异常响应) - * - * @param response HttpServletResponse - * @param resultCode 响应结果码 - */ - public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode, String message) { - int status = getHttpStatus(resultCode); - - response.setStatus(status); - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.setCharacterEncoding(StandardCharsets.UTF_8.name()); - - try (PrintWriter writer = response.getWriter()) { - String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode, message)); - writer.print(jsonResponse); - writer.flush(); // 确保将响应内容写入到输出流 - } catch (IOException e) { - log.error("响应异常处理失败", e); - } - } - - - /** - * 根据结果码获取HTTP状态码 - * - * @param resultCode 结果码 - * @return HTTP状态码 - */ - private static int getHttpStatus(ResultCode resultCode) { - return switch (resultCode) { - case ACCESS_UNAUTHORIZED, ACCESS_TOKEN_INVALID, REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); - default -> HttpStatus.BAD_REQUEST.value(); - }; - } - -} diff --git a/src/main/java/com/youlai/boot/config/MybatisConfig.java b/src/main/java/com/youlai/boot/config/MybatisConfig.java index 233792be..a3711fbf 100644 --- a/src/main/java/com/youlai/boot/config/MybatisConfig.java +++ b/src/main/java/com/youlai/boot/config/MybatisConfig.java @@ -5,8 +5,8 @@ import com.baomidou.mybatisplus.core.config.GlobalConfig; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; -import com.youlai.boot.core.handler.MyDataPermissionHandler; -import com.youlai.boot.core.handler.MyMetaObjectHandler; +import com.youlai.boot.plugin.mybatis.MyDataPermissionHandler; +import com.youlai.boot.plugin.mybatis.MyMetaObjectHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.transaction.annotation.EnableTransactionManagement; diff --git a/src/main/java/com/youlai/boot/config/SecurityConfig.java b/src/main/java/com/youlai/boot/config/SecurityConfig.java index fef74b6f..bab3ccaa 100644 --- a/src/main/java/com/youlai/boot/config/SecurityConfig.java +++ b/src/main/java/com/youlai/boot/config/SecurityConfig.java @@ -5,14 +5,15 @@ import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.core.util.ArrayUtil; import com.youlai.boot.config.property.SecurityProperties; import com.youlai.boot.core.filter.RateLimiterFilter; -import com.youlai.boot.core.security.exception.MyAccessDeniedHandler; -import com.youlai.boot.core.security.exception.MyAuthenticationEntryPoint; -import com.youlai.boot.core.security.extension.sms.SmsAuthenticationProvider; -import com.youlai.boot.core.security.extension.wechat.WechatAuthenticationProvider; -import com.youlai.boot.core.security.filter.CaptchaValidationFilter; -import com.youlai.boot.core.security.filter.TokenAuthenticationFilter; -import com.youlai.boot.core.security.token.TokenManager; -import com.youlai.boot.core.security.service.SysUserDetailsService; +import com.youlai.boot.security.filter.CaptchaValidationFilter; +import com.youlai.boot.security.filter.TokenAuthenticationFilter; +import com.youlai.boot.security.handler.MyAccessDeniedHandler; +import com.youlai.boot.security.handler.MyAuthenticationEntryPoint; +import com.youlai.boot.security.provider.SmsAuthenticationProvider; +import com.youlai.boot.security.provider.WxMiniAppCodeAuthenticationProvider; +import com.youlai.boot.security.provider.WxMiniAppPhoneAuthenticationProvider; +import com.youlai.boot.security.token.TokenManager; +import com.youlai.boot.security.service.SysUserDetailsService; import com.youlai.boot.system.service.ConfigService; import com.youlai.boot.system.service.UserService; import lombok.RequiredArgsConstructor; @@ -118,20 +119,26 @@ public class SecurityConfig { */ @Bean public DaoAuthenticationProvider daoAuthenticationProvider() { - DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); + DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(userDetailsService); daoAuthenticationProvider.setPasswordEncoder(passwordEncoder); - daoAuthenticationProvider.setUserDetailsService(userDetailsService); return daoAuthenticationProvider; } /** - * 微信认证 Provider + * 微信小程序Code认证Provider */ @Bean - public WechatAuthenticationProvider weChatAuthenticationProvider() { - return new WechatAuthenticationProvider(userService, wxMaService); + public WxMiniAppCodeAuthenticationProvider wxMiniAppCodeAuthenticationProvider() { + return new WxMiniAppCodeAuthenticationProvider(userService, wxMaService); } + /** + * 微信小程序手机号认证Provider + */ + @Bean + public WxMiniAppPhoneAuthenticationProvider wxMiniAppPhoneAuthenticationProvider() { + return new WxMiniAppPhoneAuthenticationProvider(userService, wxMaService); + } /** * 短信验证码认证 Provider @@ -147,12 +154,14 @@ public class SecurityConfig { @Bean public AuthenticationManager authenticationManager( DaoAuthenticationProvider daoAuthenticationProvider, - WechatAuthenticationProvider weChatAuthenticationProvider, + WxMiniAppCodeAuthenticationProvider wxMiniAppCodeAuthenticationProvider, + WxMiniAppPhoneAuthenticationProvider wxMiniAppPhoneAuthenticationProvider, SmsAuthenticationProvider smsAuthenticationProvider ) { return new ProviderManager( daoAuthenticationProvider, - weChatAuthenticationProvider, + wxMiniAppCodeAuthenticationProvider, + wxMiniAppPhoneAuthenticationProvider, smsAuthenticationProvider ); } diff --git a/src/main/java/com/youlai/boot/config/WebSocketConfig.java b/src/main/java/com/youlai/boot/config/WebSocketConfig.java index aee90d81..c9e28765 100644 --- a/src/main/java/com/youlai/boot/config/WebSocketConfig.java +++ b/src/main/java/com/youlai/boot/config/WebSocketConfig.java @@ -1,13 +1,20 @@ package com.youlai.boot.config; import cn.hutool.core.util.StrUtil; +<<<<<<< HEAD import com.youlai.boot.core.security.model.SysUserDetails; import com.youlai.boot.core.security.token.TokenManager; import com.youlai.boot.system.service.UserOnlineService; import lombok.RequiredArgsConstructor; +======= +import com.youlai.boot.security.model.SysUserDetails; +import com.youlai.boot.security.token.TokenManager; +import com.youlai.boot.system.service.WebSocketService; +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpHeaders; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -27,9 +34,21 @@ import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; /** +<<<<<<< HEAD * WebSocket配置 * * @author You Lai +======= + * WebSocket 配置类 + * + * 核心功能: + * - 配置 WebSocket 端点 + * - 配置消息代理 + * - 实现连接认证与授权 + * - 管理用户会话生命周期 + * + * @author Ray.Hao +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d * @since 3.0.0 */ @EnableWebSocketMessageBroker @@ -38,15 +57,34 @@ import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerCo @RequiredArgsConstructor public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { +<<<<<<< HEAD private final TokenManager tokenManager; private final UserOnlineService userOnlineService; +======= + private static final String WS_ENDPOINT = "/ws"; + private static final String APP_DESTINATION_PREFIX = "/app"; + private static final String USER_DESTINATION_PREFIX = "/user"; + private static final String[] BROKER_DESTINATIONS = {"/topic", "/queue"}; + + private final TokenManager tokenManager; + private final WebSocketService webSocketService; + + public WebSocketConfig(TokenManager tokenManager, @Lazy WebSocketService webSocketService) { + this.tokenManager = tokenManager; + this.webSocketService = webSocketService; + log.info("✓ WebSocket 配置已加载"); + } +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d /** - * 注册一个端点,客户端通过这个端点进行连接 + * 注册 STOMP 端点 + * + * 客户端通过该端点建立 WebSocket 连接 */ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry +<<<<<<< HEAD // 注册 /ws 的端点 .addEndpoint("/ws") // 允许跨域 @@ -54,31 +92,53 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { // 开启SockJS支持,用于不支持WebSocket的浏览器 .withSockJS(); } +======= + .addEndpoint(WS_ENDPOINT) + .setAllowedOriginPatterns("*"); // 允许跨域(生产环境建议配置具体域名) +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d + log.info("✓ STOMP 端点已注册: {}", WS_ENDPOINT); + } /** * 配置消息代理 + * + * - /app 前缀:客户端发送消息到服务端的前缀 + * - /topic 前缀:用于广播消息 + * - /queue 前缀:用于点对点消息 + * - /user 前缀:服务端发送给特定用户的消息前缀 */ @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 客户端发送消息的请求前缀 - registry.setApplicationDestinationPrefixes("/app"); + registry.setApplicationDestinationPrefixes(APP_DESTINATION_PREFIX); +<<<<<<< HEAD // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送 registry.enableSimpleBroker("/topic", "/queue"); // 服务端通知客户端的前缀,可以不设置,默认为user registry.setUserDestinationPrefix("/user"); } +======= + // 启用简单消息代理,处理 /topic 和 /queue 前缀的消息 + registry.enableSimpleBroker(BROKER_DESTINATIONS); + // 服务端通知客户端的前缀 + registry.setUserDestinationPrefix(USER_DESTINATION_PREFIX); +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d + + log.info("✓ 消息代理已配置: app={}, broker={}, user={}", + APP_DESTINATION_PREFIX, BROKER_DESTINATIONS, USER_DESTINATION_PREFIX); + } /** * 配置客户端入站通道拦截器 - *

+ * * 核心功能: - * 1. 连接建立时解析令牌并绑定用户身份 - * 2. 连接关闭时触发下线通知 - * 3. 异常Token的防御性处理 + * 1. 连接建立时:解析 JWT Token 并绑定用户身份 + * 2. 连接关闭时:触发用户下线通知 + * 3. 安全防护:拦截无效连接请求 */ @Override public void configureClientInboundChannel(ChannelRegistration registration) { @@ -86,11 +146,20 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public Message preSend(@NotNull Message message, @NotNull MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + + // 防御性检查:确保 accessor 不为空 if (accessor == null) { + log.warn("⚠ 收到异常消息:无法获取 StompHeaderAccessor"); + return ChannelInterceptor.super.preSend(message, channel); + } + + StompCommand command = accessor.getCommand(); + if (command == null) { return ChannelInterceptor.super.preSend(message, channel); } try { +<<<<<<< HEAD // 处理客户端连接请求 if (StompCommand.CONNECT.equals(accessor.getCommand())) { /* @@ -149,19 +218,178 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { // 记录用户下线状态 userOnlineService.userDisconnected(username); } +======= + switch (command) { + case CONNECT: + handleConnect(accessor); + break; + + case DISCONNECT: + handleDisconnect(accessor); + break; + + case SUBSCRIBE: + handleSubscribe(accessor); + break; + + default: + // 其他命令不需要特殊处理 + break; +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d } } catch (AuthenticationException ex) { // 认证失败时强制关闭连接 - log.error("连接认证失败:{}", ex.getMessage()); + log.error("❌ 连接认证失败: {}", ex.getMessage()); throw ex; } catch (Exception ex) { // 捕获其他未知异常 - log.error("WebSocket连接处理异常:", ex); - throw new MessagingException("Connection processing failed"); + log.error("❌ WebSocket 消息处理异常", ex); + throw new MessagingException("消息处理失败: " + ex.getMessage()); } return ChannelInterceptor.super.preSend(message, channel); } }); + + log.info("✓ 客户端入站通道拦截器已配置"); } +<<<<<<< HEAD +======= + + /** + * 处理客户端连接请求 + * + * 安全校验流程: + * 1. 提取 Authorization 头 + * 2. 验证 Bearer Token 格式 + * 3. 解析并验证 JWT 有效性 + * 4. 绑定用户身份到当前会话 + * 5. 记录用户上线状态 + */ + private void handleConnect(StompHeaderAccessor accessor) { + String authorization = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + + // 安全检查:确保 Authorization 头存在且格式正确 + if (StrUtil.isBlank(authorization)) { + log.warn("⚠ 非法连接请求:缺少 Authorization 头"); + throw new AuthenticationCredentialsNotFoundException("缺少 Authorization 头"); + } + + if (!authorization.startsWith("Bearer ")) { + log.warn("⚠ 非法连接请求:Authorization 头格式错误"); + throw new BadCredentialsException("Authorization 头格式错误"); + } + + // 提取 JWT Token(移除 "Bearer " 前缀) + String token = authorization.substring(7); + + if (StrUtil.isBlank(token)) { + log.warn("⚠ 非法连接请求:Token 为空"); + throw new BadCredentialsException("Token 为空"); + } + + // 解析并验证 Token + Authentication authentication; + try { + authentication = tokenManager.parseToken(token); + } catch (Exception ex) { + log.error("❌ Token 解析失败", ex); + throw new BadCredentialsException("Token 无效: " + ex.getMessage()); + } + + // 验证解析结果 + if (authentication == null || !authentication.isAuthenticated()) { + log.warn("⚠ Token 解析失败:认证对象无效"); + throw new BadCredentialsException("Token 解析失败"); + } + + // 获取用户详细信息 + Object principal = authentication.getPrincipal(); + if (!(principal instanceof SysUserDetails)) { + log.error("❌ 无效的用户凭证类型: {}", principal.getClass().getName()); + throw new BadCredentialsException("用户凭证类型错误"); + } + + SysUserDetails userDetails = (SysUserDetails) principal; + String username = userDetails.getUsername(); + + if (StrUtil.isBlank(username)) { + log.warn("⚠ 用户名为空"); + throw new BadCredentialsException("用户名为空"); + } + + // 绑定用户身份到当前会话(重要:用于 @SendToUser 等注解) + accessor.setUser(authentication); + + // 获取会话 ID + String sessionId = accessor.getSessionId(); + if (sessionId == null) { + log.warn("⚠ 会话 ID 为空,使用临时 ID"); + sessionId = "temp-" + System.nanoTime(); + } + + // 记录用户上线状态 + try { + webSocketService.userConnected(username, sessionId); + log.info("✓ WebSocket 连接建立成功: 用户[{}], 会话[{}]", username, sessionId); + } catch (Exception ex) { + log.error("❌ 记录用户上线状态失败: 用户[{}], 会话[{}]", username, sessionId, ex); + // 不抛出异常,允许连接继续 + } + } + + /** + * 处理客户端断开连接事件 + * + * 注意: + * - 只有成功建立过认证的连接才会触发下线事件 + * - 防止未认证成功的连接产生脏数据 + */ + private void handleDisconnect(StompHeaderAccessor accessor) { + Authentication authentication = (Authentication) accessor.getUser(); + + // 防御性检查:只处理已认证的连接 + if (authentication == null || !authentication.isAuthenticated()) { + log.debug("未认证的连接断开,跳过处理"); + return; + } + + Object principal = authentication.getPrincipal(); + if (!(principal instanceof SysUserDetails)) { + log.warn("⚠ 断开连接时用户凭证类型异常"); + return; + } + + SysUserDetails userDetails = (SysUserDetails) principal; + String username = userDetails.getUsername(); + + if (StrUtil.isNotBlank(username)) { + try { + webSocketService.userDisconnected(username); + log.info("✓ WebSocket 连接断开: 用户[{}]", username); + } catch (Exception ex) { + log.error("❌ 记录用户下线状态失败: 用户[{}]", username, ex); + } + } + } + + /** + * 处理客户端订阅事件(可选) + * + * 用于记录订阅信息或实施订阅级别的权限控制 + */ + private void handleSubscribe(StompHeaderAccessor accessor) { + Authentication authentication = (Authentication) accessor.getUser(); + + if (authentication != null && authentication.isAuthenticated()) { + String destination = accessor.getDestination(); + String username = authentication.getName(); + + log.debug("用户[{}]订阅主题: {}", username, destination); + + // TODO: 这里可以实现订阅级别的权限控制 + // 例如:检查用户是否有权限订阅某个主题 + } + } +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d } diff --git a/src/main/java/com/youlai/boot/core/aspect/LogAspect.java b/src/main/java/com/youlai/boot/core/aspect/LogAspect.java index f39a7562..4b7ed91b 100644 --- a/src/main/java/com/youlai/boot/core/aspect/LogAspect.java +++ b/src/main/java/com/youlai/boot/core/aspect/LogAspect.java @@ -10,7 +10,7 @@ import cn.hutool.json.JSONUtil; import com.aliyun.oss.HttpMethod; import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.util.IPUtils; -import com.youlai.boot.core.security.util.SecurityUtils; +import com.youlai.boot.security.util.SecurityUtils; import com.youlai.boot.system.model.entity.Log; import com.youlai.boot.system.service.LogService; import jakarta.servlet.http.HttpServletRequest; @@ -60,6 +60,9 @@ public class LogAspect { */ @Around("logPointcut() && @annotation(logAnnotation)") public Object doAround(ProceedingJoinPoint joinPoint, com.youlai.boot.common.annotation.Log logAnnotation) throws Throwable { + // 在方法执行前获取用户ID,避免在方法执行过程中清除上下文导致获取不到用户ID + Long userId = SecurityUtils.getUserId(); + TimeInterval timer = DateUtil.timer(); Object result = null; Exception exception = null; @@ -71,7 +74,7 @@ public class LogAspect { throw e; } finally { long executionTime = timer.interval(); // 执行时长 - this.saveLog(joinPoint, exception, result, logAnnotation, executionTime); + this.saveLog(joinPoint, exception, result, logAnnotation, executionTime, userId); } return result; } @@ -84,8 +87,9 @@ public class LogAspect { * @param e 异常 * @param jsonResult 响应结果 * @param logAnnotation 日志注解 + * @param userId 用户ID */ - private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, com.youlai.boot.common.annotation.Log logAnnotation, long executionTime) { + private void saveLog(final JoinPoint joinPoint, final Exception e, Object jsonResult, com.youlai.boot.common.annotation.Log logAnnotation, long executionTime, Long userId) { String requestURI = request.getRequestURI(); // 创建日志记录 Log log = new Log(); @@ -108,7 +112,6 @@ public class LogAspect { } } log.setRequestUri(requestURI); - Long userId = SecurityUtils.getUserId(); log.setCreateBy(userId); String ipAddr = IPUtils.getIpAddr(request); if (StrUtil.isNotBlank(ipAddr)) { diff --git a/src/main/java/com/youlai/boot/core/aspect/RepeatSubmitAspect.java b/src/main/java/com/youlai/boot/core/aspect/RepeatSubmitAspect.java index 7ac704a9..6c0a0f4e 100644 --- a/src/main/java/com/youlai/boot/core/aspect/RepeatSubmitAspect.java +++ b/src/main/java/com/youlai/boot/core/aspect/RepeatSubmitAspect.java @@ -4,8 +4,8 @@ import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.SecurityConstants; -import com.youlai.boot.common.result.ResultCode; -import com.youlai.boot.common.exception.BusinessException; +import com.youlai.boot.core.web.ResultCode; +import com.youlai.boot.core.exception.BusinessException; import com.youlai.boot.common.annotation.RepeatSubmit; import com.youlai.boot.common.util.IPUtils; import jakarta.servlet.http.HttpServletRequest; diff --git a/src/main/java/com/youlai/boot/common/exception/BusinessException.java b/src/main/java/com/youlai/boot/core/exception/BusinessException.java similarity index 91% rename from src/main/java/com/youlai/boot/common/exception/BusinessException.java rename to src/main/java/com/youlai/boot/core/exception/BusinessException.java index 06467091..033981c5 100644 --- a/src/main/java/com/youlai/boot/common/exception/BusinessException.java +++ b/src/main/java/com/youlai/boot/core/exception/BusinessException.java @@ -1,6 +1,6 @@ -package com.youlai.boot.common.exception; +package com.youlai.boot.core.exception; -import com.youlai.boot.common.result.IResultCode; +import com.youlai.boot.core.web.IResultCode; import lombok.Getter; import org.slf4j.helpers.MessageFormatter; diff --git a/src/main/java/com/youlai/boot/common/exception/GlobalExceptionHandler.java b/src/main/java/com/youlai/boot/core/exception/GlobalExceptionHandler.java similarity index 92% rename from src/main/java/com/youlai/boot/common/exception/GlobalExceptionHandler.java rename to src/main/java/com/youlai/boot/core/exception/GlobalExceptionHandler.java index cf7722a8..9b20ddb9 100644 --- a/src/main/java/com/youlai/boot/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/youlai/boot/core/exception/GlobalExceptionHandler.java @@ -1,9 +1,12 @@ -package com.youlai.boot.common.exception; +package com.youlai.boot.core.exception; import cn.hutool.core.util.StrUtil; import com.fasterxml.jackson.core.JsonProcessingException; -import com.youlai.boot.common.result.Result; -import com.youlai.boot.common.result.ResultCode; +import com.youlai.boot.core.web.Result; +import com.youlai.boot.core.web.ResultCode; +import jakarta.servlet.ServletException; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.TypeMismatchException; import org.springframework.context.support.DefaultMessageSourceResolvable; @@ -21,10 +24,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.NoHandlerFoundException; -import jakarta.servlet.ServletException; -import jakarta.validation.ConstraintViolation; -import jakarta.validation.ConstraintViolationException; - +import java.sql.SQLIntegrityConstraintViolationException; import java.sql.SQLSyntaxErrorException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -192,7 +192,7 @@ public class GlobalExceptionHandler { log.error(e.getMessage(), e); String errorMsg = e.getMessage(); if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) { - return Result.failed(ResultCode.ACCESS_UNAUTHORIZED); + return Result.failed(ResultCode.DATABASE_ACCESS_DENIED); } else { return Result.failed(e.getMessage()); } @@ -207,7 +207,20 @@ public class GlobalExceptionHandler { @ResponseStatus(HttpStatus.FORBIDDEN) public Result processSQLSyntaxErrorException(SQLSyntaxErrorException e) { log.error(e.getMessage(), e); - return Result.failed(e.getMessage()); + return Result.failed(ResultCode.DATABASE_EXECUTION_SYNTAX_ERROR); + } + + + /** + * 处理 SQL 违反了完整性约束 + *

+ * 当 SQL 违反了完整性约束时,会抛出 SQLIntegrityConstraintViolationException 异常。 + */ + @ExceptionHandler(SQLIntegrityConstraintViolationException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleSQLIntegrityConstraintViolationException(SQLIntegrityConstraintViolationException e) { + log.error(e.getMessage(), e); + return Result.failed(ResultCode.INTEGRITY_CONSTRAINT_VIOLATION); } /** diff --git a/src/main/java/com/youlai/boot/core/filter/RateLimiterFilter.java b/src/main/java/com/youlai/boot/core/filter/RateLimiterFilter.java index 166455b1..186175e1 100644 --- a/src/main/java/com/youlai/boot/core/filter/RateLimiterFilter.java +++ b/src/main/java/com/youlai/boot/core/filter/RateLimiterFilter.java @@ -4,9 +4,9 @@ import cn.hutool.core.convert.Convert; import cn.hutool.core.util.StrUtil; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.SystemConstants; -import com.youlai.boot.common.result.ResultCode; +import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.common.util.IPUtils; -import com.youlai.boot.common.util.ResponseUtils; +import com.youlai.boot.core.web.WebResponseHelper; import com.youlai.boot.system.service.ConfigService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -88,7 +88,7 @@ public class RateLimiterFilter extends OncePerRequestFilter { // 判断是否限流 if (rateLimit(ip)) { // 返回限流错误信息 - ResponseUtils.writeErrMsg(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED); + WebResponseHelper.writeError(response, ResultCode.REQUEST_CONCURRENCY_LIMIT_EXCEEDED); return; } diff --git a/src/main/java/com/youlai/boot/common/result/ExcelResult.java b/src/main/java/com/youlai/boot/core/web/ExcelResult.java similarity index 94% rename from src/main/java/com/youlai/boot/common/result/ExcelResult.java rename to src/main/java/com/youlai/boot/core/web/ExcelResult.java index d9d6590a..c73ef6e5 100644 --- a/src/main/java/com/youlai/boot/common/result/ExcelResult.java +++ b/src/main/java/com/youlai/boot/core/web/ExcelResult.java @@ -1,4 +1,4 @@ -package com.youlai.boot.common.result; +package com.youlai.boot.core.web; import lombok.Data; diff --git a/src/main/java/com/youlai/boot/common/result/IResultCode.java b/src/main/java/com/youlai/boot/core/web/IResultCode.java similarity index 78% rename from src/main/java/com/youlai/boot/common/result/IResultCode.java rename to src/main/java/com/youlai/boot/core/web/IResultCode.java index 741d604a..0f5930ae 100644 --- a/src/main/java/com/youlai/boot/common/result/IResultCode.java +++ b/src/main/java/com/youlai/boot/core/web/IResultCode.java @@ -1,4 +1,4 @@ -package com.youlai.boot.common.result; +package com.youlai.boot.core.web; /** * 响应码接口 diff --git a/src/main/java/com/youlai/boot/common/result/PageResult.java b/src/main/java/com/youlai/boot/core/web/PageResult.java similarity index 95% rename from src/main/java/com/youlai/boot/common/result/PageResult.java rename to src/main/java/com/youlai/boot/core/web/PageResult.java index 693fcb64..16efcaba 100644 --- a/src/main/java/com/youlai/boot/common/result/PageResult.java +++ b/src/main/java/com/youlai/boot/core/web/PageResult.java @@ -1,4 +1,4 @@ -package com.youlai.boot.common.result; +package com.youlai.boot.core.web; import com.baomidou.mybatisplus.core.metadata.IPage; import lombok.Data; diff --git a/src/main/java/com/youlai/boot/common/result/Result.java b/src/main/java/com/youlai/boot/core/web/Result.java similarity index 98% rename from src/main/java/com/youlai/boot/common/result/Result.java rename to src/main/java/com/youlai/boot/core/web/Result.java index 55fee091..783be757 100644 --- a/src/main/java/com/youlai/boot/common/result/Result.java +++ b/src/main/java/com/youlai/boot/core/web/Result.java @@ -1,4 +1,4 @@ -package com.youlai.boot.common.result; +package com.youlai.boot.core.web; import cn.hutool.core.util.StrUtil; import lombok.Data; diff --git a/src/main/java/com/youlai/boot/common/result/ResultCode.java b/src/main/java/com/youlai/boot/core/web/ResultCode.java similarity index 97% rename from src/main/java/com/youlai/boot/common/result/ResultCode.java rename to src/main/java/com/youlai/boot/core/web/ResultCode.java index 7491e913..d7ea897b 100644 --- a/src/main/java/com/youlai/boot/common/result/ResultCode.java +++ b/src/main/java/com/youlai/boot/core/web/ResultCode.java @@ -1,4 +1,4 @@ -package com.youlai.boot.common.result; +package com.youlai.boot.core.web; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; @@ -243,12 +243,16 @@ public enum ResultCode implements IResultCode, Serializable { TABLE_NOT_EXIST("C0311", "表不存在"), COLUMN_NOT_EXIST("C0312", "列不存在"), + DATABASE_EXECUTION_SYNTAX_ERROR("C0313", "数据库执行语法错误"), MULTIPLE_SAME_NAME_COLUMNS_IN_MULTI_TABLE_ASSOCIATION("C0321", "多表关联中存在多个相同名称的列"), DATABASE_DEADLOCK("C0331", "数据库死锁"), PRIMARY_KEY_CONFLICT("C0341", "主键冲突"), + INTEGRITY_CONSTRAINT_VIOLATION("C0342", "违反了完整性约束"), + + DATABASE_ACCESS_DENIED("C0351", "演示环境已禁用数据库写入功能,请本地部署修改数据库链接或开启Mock模式进行体验"), /** 二级宏观错误码 */ THIRD_PARTY_DISASTER_RECOVERY_SYSTEM_TRIGGERED("C0400", "第三方容灾系统被触发"), @@ -293,4 +297,4 @@ public enum ResultCode implements IResultCode, Serializable { } return SYSTEM_ERROR; // 默认系统执行错误 } -} \ No newline at end of file +} diff --git a/src/main/java/com/youlai/boot/core/web/WebResponseHelper.java b/src/main/java/com/youlai/boot/core/web/WebResponseHelper.java new file mode 100644 index 00000000..192ec058 --- /dev/null +++ b/src/main/java/com/youlai/boot/core/web/WebResponseHelper.java @@ -0,0 +1,77 @@ +package com.youlai.boot.core.web; + +import cn.hutool.extra.servlet.JakartaServletUtil; +import cn.hutool.json.JSONUtil; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +import java.nio.charset.StandardCharsets; + +/** + * Web响应辅助类 + *

+ * 用于在过滤器、处理器等无法使用 @RestControllerAdvice 的场景中统一处理响应 + * + * @author Ray.Hao + * @since 2.0.0 + */ +@Slf4j +public class WebResponseHelper { + + /** + * 写入错误响应 + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + */ + public static void writeError(HttpServletResponse response, ResultCode resultCode) { + writeError(response, resultCode, null); + } + + /** + * 写入错误响应(带自定义消息) + * + * @param response HttpServletResponse + * @param resultCode 响应结果码 + * @param message 自定义消息 + */ + public static void writeError(HttpServletResponse response, ResultCode resultCode, String message) { + try { + // 设置HTTP状态码 + int httpStatus = mapHttpStatus(resultCode); + response.setStatus(httpStatus); + response.setCharacterEncoding(StandardCharsets.UTF_8.toString()); + // 构建响应对象 + Result result = message == null + ? Result.failed(resultCode) + : Result.failed(resultCode, message); + + // 写入响应 + JakartaServletUtil.write(response, + JSONUtil.toJsonStr(result), + MediaType.APPLICATION_JSON_VALUE + ); + + } catch (Exception e) { + log.error("写入错误响应失败: resultCode={}, message={}", resultCode, message, e); + } + } + + /** + * 根据业务结果码映射HTTP状态码 + * + * @param resultCode 业务结果码 + * @return HTTP状态码 + */ + private static int mapHttpStatus(ResultCode resultCode) { + return switch (resultCode) { + case ACCESS_UNAUTHORIZED, + ACCESS_TOKEN_INVALID, + REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); + default -> HttpStatus.BAD_REQUEST.value(); + }; + } +} + diff --git a/src/main/java/com/youlai/boot/modules/member/controller/MemberController.java b/src/main/java/com/youlai/boot/modules/member/controller/MemberController.java deleted file mode 100644 index 1512905e..00000000 --- a/src/main/java/com/youlai/boot/modules/member/controller/MemberController.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.youlai.boot.modules.member.controller; - -/** - * 会员控制层-业务模块演示 - * - * @author haoxr - * @since 2024/10/10 - */ -public class MemberController { -} diff --git a/src/main/java/com/youlai/boot/modules/member/mapper/MemberMapper.java b/src/main/java/com/youlai/boot/modules/member/mapper/MemberMapper.java deleted file mode 100644 index 59be608e..00000000 --- a/src/main/java/com/youlai/boot/modules/member/mapper/MemberMapper.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.youlai.boot.modules.member.mapper; - -/** - * 会员数据访问层-业务模块演示 - * - * @author haoxr - * @since 2024/10/10 - */ -public class MemberMapper { -} diff --git a/src/main/java/com/youlai/boot/modules/member/model/Member.java b/src/main/java/com/youlai/boot/modules/member/model/Member.java deleted file mode 100644 index c75fda6f..00000000 --- a/src/main/java/com/youlai/boot/modules/member/model/Member.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.youlai.boot.modules.member.model; - -/** - * 会员实体-业务模块演示 - * - * @author haoxr - * @since 2024/10/10 - */ -public class Member { -} diff --git a/src/main/java/com/youlai/boot/modules/member/service/MemberService.java b/src/main/java/com/youlai/boot/modules/member/service/MemberService.java deleted file mode 100644 index 49a8a733..00000000 --- a/src/main/java/com/youlai/boot/modules/member/service/MemberService.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.youlai.boot.modules.member.service; - -/** - * 会员管理服务类-业务模块演示 - * - * @author haoxr - * @since 2024/10/10 - */ -public class MemberService { -} diff --git a/src/main/java/com/youlai/boot/modules/order/controller/OrderController.java b/src/main/java/com/youlai/boot/modules/order/controller/OrderController.java deleted file mode 100644 index 4c009b67..00000000 --- a/src/main/java/com/youlai/boot/modules/order/controller/OrderController.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.youlai.boot.modules.order.controller; - -/** - * 订单控制层-业务模块演示 - * - * @author haoxr - * @since 2024/10/10 - */ -public class OrderController { -} diff --git a/src/main/java/com/youlai/boot/modules/order/mapper/OrderMapper.java b/src/main/java/com/youlai/boot/modules/order/mapper/OrderMapper.java deleted file mode 100644 index c6f0c108..00000000 --- a/src/main/java/com/youlai/boot/modules/order/mapper/OrderMapper.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.youlai.boot.modules.order.mapper; - -/** - * 订单数据访问层-业务模块演示 - * - * @author haoxr - * @since 2024/10/10 - */ -public class OrderMapper { -} diff --git a/src/main/java/com/youlai/boot/modules/order/model/Order.java b/src/main/java/com/youlai/boot/modules/order/model/Order.java deleted file mode 100644 index cd1b00ac..00000000 --- a/src/main/java/com/youlai/boot/modules/order/model/Order.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.youlai.boot.modules.order.model; - -/** - * 订单实体-业务模块演示 - * - * @author haoxr - * @since 2024/10/10 - */ -public class Order { -} diff --git a/src/main/java/com/youlai/boot/modules/order/service/OrderService.java b/src/main/java/com/youlai/boot/modules/order/service/OrderService.java deleted file mode 100644 index 0341bce7..00000000 --- a/src/main/java/com/youlai/boot/modules/order/service/OrderService.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.youlai.boot.modules.order.service; - -/** - * 订单管理服务类-业务模块演示 - * - * @author haoxr - * @since 2024/10/10 - */ -public class OrderService { -} diff --git a/src/main/java/com/youlai/boot/modules/product/controller/ProductController.java b/src/main/java/com/youlai/boot/modules/product/controller/ProductController.java deleted file mode 100644 index 480b993c..00000000 --- a/src/main/java/com/youlai/boot/modules/product/controller/ProductController.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.youlai.boot.modules.product.controller; - -/** - * 商品控制层-业务模块演示 - * - * @author haoxr - * @since 2024/10/10 - */ -public class ProductController { -} diff --git a/src/main/java/com/youlai/boot/modules/product/mapper/ProductMapper.java b/src/main/java/com/youlai/boot/modules/product/mapper/ProductMapper.java deleted file mode 100644 index 52068175..00000000 --- a/src/main/java/com/youlai/boot/modules/product/mapper/ProductMapper.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.youlai.boot.modules.product.mapper; - -/** - * 商品数据访问层-业务模块演示 - * - * @author haoxr - * @since 2024/10/10 - */ -public class ProductMapper { -} diff --git a/src/main/java/com/youlai/boot/modules/product/model/Product.java b/src/main/java/com/youlai/boot/modules/product/model/Product.java deleted file mode 100644 index 6b746889..00000000 --- a/src/main/java/com/youlai/boot/modules/product/model/Product.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.youlai.boot.modules.product.model; - -/** - * 商品实体-业务模块演示 - * - * @author haoxr - * @since 2024/10/10 - */ -public class Product { -} diff --git a/src/main/java/com/youlai/boot/modules/product/service/ProductService.java b/src/main/java/com/youlai/boot/modules/product/service/ProductService.java deleted file mode 100644 index 27929770..00000000 --- a/src/main/java/com/youlai/boot/modules/product/service/ProductService.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.youlai.boot.modules.product.service; - -/** - * 会员管理服务类-业务模块演示 - * - * @author haoxr - * @since 2024/10/10 - */ -public class ProductService { -} diff --git a/src/main/java/com/youlai/boot/platform/ai/config/AiProperties.java b/src/main/java/com/youlai/boot/platform/ai/config/AiProperties.java new file mode 100644 index 00000000..69d19a02 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/config/AiProperties.java @@ -0,0 +1,113 @@ +package com.youlai.boot.platform.ai.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * AI 配置属性 + * + * 优势: + * 1. 统一管理所有提供商配置 + * 2. 添加新提供商只需在 yml 中添加配置,无需修改代码 + * 3. 类型安全,支持 IDE 提示 + * + * @author Ray.Hao + */ +@Data +@Component +@ConfigurationProperties(prefix = "ai") +public class AiProperties { + + /** + * 是否启用 AI 功能 + */ + private Boolean enabled = false; + + /** + * 当前使用的提供商(qwen、deepseek、openai 等) + */ + private String provider = "qwen"; + + /** + * 所有提供商的配置 + * Key: 提供商名称(qwen、deepseek、openai) + * Value: 提供商配置 + */ + private Map providers; + + /** + * 安全配置 + */ + private SecurityConfig security = new SecurityConfig(); + + /** + * 限流配置 + */ + private RateLimitConfig rateLimit = new RateLimitConfig(); + + /** + * 提供商配置 + */ + @Data + public static class ProviderConfig { + /** + * API Key + */ + private String apiKey; + + /** + * Base URL(统一命名,符合行业惯例) + */ + private String baseUrl; + + /** + * 模型名称 + */ + private String model; + + /** + * 提供商显示名称(可选) + */ + private String displayName; + + /** + * 超时时间(秒) + */ + private Integer timeout = 30; + } + + /** + * 安全配置 + */ + @Data + public static class SecurityConfig { + private Boolean enableAudit = true; + private Boolean dangerousOperationsConfirm = true; + private java.util.List functionWhitelist; + private java.util.List sensitiveParams; + } + + /** + * 限流配置 + */ + @Data + public static class RateLimitConfig { + private Integer maxExecutionsPerMinute = 10; + private Integer maxExecutionsPerDay = 100; + } + + /** + * 获取当前提供商配置 + */ + public ProviderConfig getCurrentProviderConfig() { + if (providers == null || !providers.containsKey(provider)) { + throw new IllegalStateException("未找到提供商配置: " + provider); + } + return providers.get(provider); + } +} + + diff --git a/src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java b/src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java new file mode 100644 index 00000000..3597fdca --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/controller/AiCommandController.java @@ -0,0 +1,99 @@ +package com.youlai.boot.platform.ai.controller; + +import com.youlai.boot.core.web.Result; +import com.youlai.boot.platform.ai.model.dto.*; +import com.youlai.boot.platform.ai.service.AiCommandService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + + +/** + * AI 命令控制器 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Tag(name = "AI命令接口") +@RestController +@RequestMapping("/api/v1/ai/command") +@RequiredArgsConstructor +@Slf4j +public class AiCommandController { + + private final AiCommandService aiCommandService; + + @Operation(summary = "解析自然语言命令") + @PostMapping("/parse") + public Result parseCommand( + @RequestBody AiCommandRequestDTO request, + HttpServletRequest httpRequest + ) { + log.info("收到AI命令解析请求: {}", request.getCommand()); + + try { + AiCommandResponseDTO response = aiCommandService.parseCommand(request, httpRequest); + return Result.success(response); + } catch (Exception e) { + log.error("命令解析失败", e); + return Result.success(AiCommandResponseDTO.builder() + .success(false) + .error(e.getMessage()) + .build()); + } + } + + @Operation(summary = "执行已解析的命令") + @PostMapping("/execute") + public Result executeCommand( + @RequestBody AiExecuteRequestDTO request, + HttpServletRequest httpRequest + ) { + log.info("收到AI命令执行请求: {}", request.getFunctionCall().getName()); + + try { + AiExecuteResponseDTO response = aiCommandService.executeCommand(request, httpRequest); + return Result.success(response); + } catch (Exception e) { + log.error("命令执行失败", e); + return Result.success(AiExecuteResponseDTO.builder() + .success(false) + .error(e.getMessage()) + .build()); + } + } + + @Operation(summary = "获取命令执行历史") + @GetMapping("/history") + public Result getCommandHistory( + @Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page, + @Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer size + ) { + return Result.success(aiCommandService.getCommandHistory(page, size)); + } + + @Operation(summary = "获取可用的函数列表") + @GetMapping("/functions") + public Result getAvailableFunctions() { + return Result.success(aiCommandService.getAvailableFunctions()); + } + + @Operation(summary = "撤销命令执行") + @PostMapping("/rollback/{auditId}") + public Result rollbackCommand( + @Parameter(description = "审计ID") @PathVariable String auditId + ) { + aiCommandService.rollbackCommand(auditId); + return Result.success("撤销成功"); + } +} + + + + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandAuditMapper.java b/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandAuditMapper.java new file mode 100644 index 00000000..c3fe86e5 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/mapper/AiCommandAuditMapper.java @@ -0,0 +1,20 @@ +package com.youlai.boot.platform.ai.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.youlai.boot.platform.ai.model.entity.AiCommandAudit; +import org.apache.ibatis.annotations.Mapper; + +/** + * AI 命令审计 Mapper + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Mapper +public interface AiCommandAuditMapper extends BaseMapper { +} + + + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiCommandRequestDTO.java b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiCommandRequestDTO.java new file mode 100644 index 00000000..33b8a811 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiCommandRequestDTO.java @@ -0,0 +1,37 @@ +package com.youlai.boot.platform.ai.model.dto; + +import lombok.Data; +import java.util.Map; + +/** + * AI 命令请求 DTO + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +public class AiCommandRequestDTO { + + /** + * 用户输入的自然语言命令 + */ + private String command; + + /** + * 当前页面路由(用于上下文) + */ + private String currentRoute; + + /** + * 当前激活的组件名称 + */ + private String currentComponent; + + /** + * 额外上下文信息 + */ + private Map context; +} + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiCommandResponseDTO.java b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiCommandResponseDTO.java new file mode 100644 index 00000000..4331b05b --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiCommandResponseDTO.java @@ -0,0 +1,53 @@ +package com.youlai.boot.platform.ai.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.List; + +/** + * AI 命令解析响应 DTO + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiCommandResponseDTO { + + /** + * 是否成功解析 + */ + private Boolean success; + + /** + * 解析后的函数调用列表 + */ + private List functionCalls; + + /** + * AI 的理解和说明 + */ + private String explanation; + + /** + * 置信度 (0-1) + */ + private Double confidence; + + /** + * 错误信息 + */ + private String error; + + /** + * 原始 LLM 响应(用于调试) + */ + private String rawResponse; +} + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteRequestDTO.java b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteRequestDTO.java new file mode 100644 index 00000000..f091d99b --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteRequestDTO.java @@ -0,0 +1,36 @@ +package com.youlai.boot.platform.ai.model.dto; + +import lombok.Data; + +/** + * AI 命令执行请求 DTO + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +public class AiExecuteRequestDTO { + + /** + * 要执行的函数调用 + */ + private FunctionCallDTO functionCall; + + /** + * 确认模式:auto=自动执行, manual=需要用户确认 + */ + private String confirmMode; + + /** + * 用户确认标志 + */ + private Boolean userConfirmed; + + /** + * 幂等性令牌(防止重复执行) + */ + private String idempotencyKey; +} + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteResponseDTO.java b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteResponseDTO.java new file mode 100644 index 00000000..279126d6 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/model/dto/AiExecuteResponseDTO.java @@ -0,0 +1,62 @@ +package com.youlai.boot.platform.ai.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * AI 命令执行响应 DTO + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AiExecuteResponseDTO { + + /** + * 是否执行成功 + */ + private Boolean success; + + /** + * 执行结果数据 + */ + private Object data; + + /** + * 执行结果说明 + */ + private String message; + + /** + * 影响的记录数 + */ + private Integer affectedRows; + + /** + * 错误信息 + */ + private String error; + + /** + * 审计ID(用于追踪) + */ + private String auditId; + + /** + * 需要用户确认 + */ + private Boolean requiresConfirmation; + + /** + * 确认提示信息 + */ + private String confirmationPrompt; +} + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/model/dto/FunctionCallDTO.java b/src/main/java/com/youlai/boot/platform/ai/model/dto/FunctionCallDTO.java new file mode 100644 index 00000000..8e08eb2b --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/model/dto/FunctionCallDTO.java @@ -0,0 +1,38 @@ +package com.youlai.boot.platform.ai.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.Map; + +/** + * 函数调用 DTO + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FunctionCallDTO { + + /** + * 函数名称 + */ + private String name; + + /** + * 函数描述 + */ + private String description; + + /** + * 参数对象 + */ + private Map arguments; +} + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandAudit.java b/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandAudit.java new file mode 100644 index 00000000..9746f349 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/model/entity/AiCommandAudit.java @@ -0,0 +1,121 @@ +package com.youlai.boot.platform.ai.model.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * AI 命令审计记录 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +@TableName("ai_command_audit") +public class AiCommandAudit { + + /** + * 主键ID + */ + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 用户名 + */ + private String username; + + /** + * 原始命令 + */ + private String originalCommand; + + /** + * 解析后的函数名称 + */ + private String functionName; + + /** + * 函数参数(JSON) + */ + private String functionArguments; + + /** + * 执行状态:pending, success, failed + */ + private String executeStatus; + + /** + * 执行结果(JSON) + */ + private String executeResult; + + /** + * 错误信息 + */ + private String errorMessage; + + /** + * 影响的记录数 + */ + private Integer affectedRows; + + /** + * 是否危险操作 + */ + private Boolean isDangerous; + + /** + * 是否需要确认 + */ + private Boolean requiresConfirmation; + + /** + * 用户是否确认 + */ + private Boolean userConfirmed; + + /** + * 幂等性令牌 + */ + private String idempotencyKey; + + /** + * IP 地址 + */ + private String ipAddress; + + /** + * 用户代理 + */ + private String userAgent; + + /** + * 当前路由 + */ + private String currentRoute; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + /** + * 执行时间(毫秒) + */ + private Long executionTime; + + /** + * 备注 + */ + private String remark; +} + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/provider/AbstractOpenAiCompatibleProvider.java b/src/main/java/com/youlai/boot/platform/ai/provider/AbstractOpenAiCompatibleProvider.java new file mode 100644 index 00000000..bf7ca322 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/provider/AbstractOpenAiCompatibleProvider.java @@ -0,0 +1,101 @@ +package com.youlai.boot.platform.ai.provider; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.youlai.boot.platform.ai.config.AiProperties; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.TimeUnit; + +/** + * OpenAI 兼容协议的抽象提供商 + * + * 适用于:通义千问、DeepSeek、OpenAI、ChatGLM 等兼容 OpenAI API 的模型 + * + * @author Ray.Hao + */ +@Slf4j +public abstract class AbstractOpenAiCompatibleProvider implements AiProvider { + + protected final AiProperties.ProviderConfig config; + + public AbstractOpenAiCompatibleProvider(AiProperties.ProviderConfig config) { + this.config = config; + } + + @Override + public String call(String systemPrompt, String userPrompt) { + if (!isConfigValid()) { + throw new IllegalStateException(getProviderName() + " 配置无效"); + } + + try { + // 构建请求体(OpenAI 标准格式) + JSONObject requestBody = JSONUtil.createObj() + .set("model", config.getModel()) + .set("messages", JSONUtil.createArray() + .put(JSONUtil.createObj() + .set("role", "system") + .set("content", systemPrompt)) + .put(JSONUtil.createObj() + .set("role", "user") + .set("content", userPrompt)) + ) + .set("temperature", 0.7); + + log.info("📤 调用 {} API: {}/chat/completions", getProviderName(), config.getBaseUrl()); + log.debug("请求参数: {}", requestBody); + + // 发送 HTTP 请求 + HttpResponse response = HttpRequest.post(config.getBaseUrl() + "/chat/completions") + .header("Authorization", "Bearer " + config.getApiKey()) + .header("Content-Type", "application/json") + .body(requestBody.toString()) + .timeout((int) TimeUnit.SECONDS.toMillis(config.getTimeout())) + .execute(); + + // 检查响应状态 + if (!response.isOk()) { + String errorMsg = String.format("%s API 调用失败: HTTP %d - %s", + getProviderName(), response.getStatus(), response.body()); + log.error(errorMsg); + throw new RuntimeException(errorMsg); + } + + // 解析响应 + JSONObject responseJson = JSONUtil.parseObj(response.body()); + String content = responseJson.getByPath("choices[0].message.content", String.class); + + // 记录 Token 使用情况 + JSONObject usage = responseJson.getJSONObject("usage"); + if (usage != null) { + Integer inputTokens = usage.getInt("prompt_tokens"); + Integer outputTokens = usage.getInt("completion_tokens"); + Integer totalTokens = usage.getInt("total_tokens"); + log.info("✅ {} 响应成功,tokens: 输入={}, 输出={}, 总计={}", + getProviderName(), inputTokens, outputTokens, totalTokens); + } + + log.debug("📥 {} 返回内容: {}", getProviderName(), content); + return content; + + } catch (Exception e) { + String errorMsg = String.format("%s API 调用失败: %s", getProviderName(), e.getMessage()); + log.error(errorMsg, e); + throw new RuntimeException(errorMsg, e); + } + } + + @Override + public boolean isConfigValid() { + return config != null + && StrUtil.isNotBlank(config.getApiKey()) + && StrUtil.isNotBlank(config.getBaseUrl()) + && StrUtil.isNotBlank(config.getModel()); + } +} + + diff --git a/src/main/java/com/youlai/boot/platform/ai/provider/AiProvider.java b/src/main/java/com/youlai/boot/platform/ai/provider/AiProvider.java new file mode 100644 index 00000000..d6c23301 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/provider/AiProvider.java @@ -0,0 +1,32 @@ +package com.youlai.boot.platform.ai.provider; + +/** + * AI 提供商接口 + * + * 策略模式:不同提供商实现各自的调用逻辑 + * + * @author Ray.Hao + */ +public interface AiProvider { + + /** + * 调用 AI API + * + * @param systemPrompt 系统提示词 + * @param userPrompt 用户提示词 + * @return AI 响应内容 + */ + String call(String systemPrompt, String userPrompt); + + /** + * 获取提供商名称 + */ + String getProviderName(); + + /** + * 检查配置是否有效 + */ + boolean isConfigValid(); +} + + diff --git a/src/main/java/com/youlai/boot/platform/ai/provider/AiProviderFactory.java b/src/main/java/com/youlai/boot/platform/ai/provider/AiProviderFactory.java new file mode 100644 index 00000000..16b32254 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/provider/AiProviderFactory.java @@ -0,0 +1,51 @@ +package com.youlai.boot.platform.ai.provider; + +import com.youlai.boot.platform.ai.config.AiProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * AI 提供商工厂 + * + * 职责:根据配置获取对应的提供商实例 + * + * @author Ray.Hao + */ +@Component +@RequiredArgsConstructor +public class AiProviderFactory { + + private final AiProperties aiProperties; + + /** + * Spring 自动注入所有 AiProvider 实现类 + * Key: Bean 名称(qwen、deepseek、openai) + * Value: 提供商实例 + */ + private final Map providers; + + /** + * 获取当前配置的提供商 + */ + public AiProvider getCurrentProvider() { + String providerName = aiProperties.getProvider(); + + if (!providers.containsKey(providerName)) { + throw new IllegalStateException("不支持的 AI 提供商: " + providerName + + ",可用提供商: " + providers.keySet()); + } + + AiProvider provider = providers.get(providerName); + + if (!provider.isConfigValid()) { + throw new IllegalStateException(provider.getProviderName() + + " 配置无效,请检查 API Key、Base URL 和 Model 是否配置"); + } + + return provider; + } +} + + diff --git a/src/main/java/com/youlai/boot/platform/ai/provider/impl/DeepSeekProvider.java b/src/main/java/com/youlai/boot/platform/ai/provider/impl/DeepSeekProvider.java new file mode 100644 index 00000000..12b80aaf --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/provider/impl/DeepSeekProvider.java @@ -0,0 +1,25 @@ +package com.youlai.boot.platform.ai.provider.impl; + +import com.youlai.boot.platform.ai.config.AiProperties; +import com.youlai.boot.platform.ai.provider.AbstractOpenAiCompatibleProvider; +import org.springframework.stereotype.Component; + +/** + * DeepSeek 提供商 + * + * @author Ray.Hao + */ +@Component("deepseek") +public class DeepSeekProvider extends AbstractOpenAiCompatibleProvider { + + public DeepSeekProvider(AiProperties aiProperties) { + super(aiProperties.getProviders().get("deepseek")); + } + + @Override + public String getProviderName() { + return config.getDisplayName() != null ? config.getDisplayName() : "DeepSeek"; + } +} + + diff --git a/src/main/java/com/youlai/boot/platform/ai/provider/impl/OpenAiProvider.java b/src/main/java/com/youlai/boot/platform/ai/provider/impl/OpenAiProvider.java new file mode 100644 index 00000000..09613e33 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/provider/impl/OpenAiProvider.java @@ -0,0 +1,30 @@ +package com.youlai.boot.platform.ai.provider.impl; + +import com.youlai.boot.platform.ai.config.AiProperties; +import com.youlai.boot.platform.ai.provider.AbstractOpenAiCompatibleProvider; +import org.springframework.stereotype.Component; + +/** + * OpenAI 提供商(GPT-4、GPT-3.5 等) + * + * 添加新提供商只需: + * 1. 继承 AbstractOpenAiCompatibleProvider + * 2. 实现 getProviderName() + * 3. 在配置文件中添加配置 + * + * @author Ray.Hao + */ +@Component("openai") +public class OpenAiProvider extends AbstractOpenAiCompatibleProvider { + + public OpenAiProvider(AiProperties aiProperties) { + super(aiProperties.getProviders().get("openai")); + } + + @Override + public String getProviderName() { + return config.getDisplayName() != null ? config.getDisplayName() : "OpenAI"; + } +} + + diff --git a/src/main/java/com/youlai/boot/platform/ai/provider/impl/QwenProvider.java b/src/main/java/com/youlai/boot/platform/ai/provider/impl/QwenProvider.java new file mode 100644 index 00000000..5a6b6dcd --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/provider/impl/QwenProvider.java @@ -0,0 +1,25 @@ +package com.youlai.boot.platform.ai.provider.impl; + +import com.youlai.boot.platform.ai.config.AiProperties; +import com.youlai.boot.platform.ai.provider.AbstractOpenAiCompatibleProvider; +import org.springframework.stereotype.Component; + +/** + * 阿里通义千问提供商 + * + * @author Ray.Hao + */ +@Component("qwen") +public class QwenProvider extends AbstractOpenAiCompatibleProvider { + + public QwenProvider(AiProperties aiProperties) { + super(aiProperties.getProviders().get("qwen")); + } + + @Override + public String getProviderName() { + return config.getDisplayName() != null ? config.getDisplayName() : "阿里通义千问"; + } +} + + diff --git a/src/main/java/com/youlai/boot/platform/ai/service/AiCommandService.java b/src/main/java/com/youlai/boot/platform/ai/service/AiCommandService.java new file mode 100644 index 00000000..f6c5b0a8 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/service/AiCommandService.java @@ -0,0 +1,63 @@ +package com.youlai.boot.platform.ai.service; + +import com.youlai.boot.platform.ai.model.dto.*; +import jakarta.servlet.http.HttpServletRequest; + +import java.util.List; +import java.util.Map; + +/** + * AI 命令服务接口 + * + * @author Ray.Hao + * @since 3.0.0 + */ +public interface AiCommandService { + + /** + * 解析自然语言命令 + * + * @param request 命令请求 + * @param httpRequest HTTP 请求 + * @return 解析结果 + */ + AiCommandResponseDTO parseCommand(AiCommandRequestDTO request, HttpServletRequest httpRequest); + + /** + * 执行已解析的命令 + * + * @param request 执行请求 + * @param httpRequest HTTP 请求 + * @return 执行结果 + */ + AiExecuteResponseDTO executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest); + + /** + * 获取命令执行历史 + * + * @param page 页码 + * @param size 每页数量 + * @return 历史记录 + */ + Map getCommandHistory(Integer page, Integer size); + + /** + * 获取可用的函数列表 + * + * @return 函数列表 + */ + List> getAvailableFunctions(); + + /** + * 撤销命令执行 + * + * @param auditId 审计ID + */ + void rollbackCommand(String auditId); +} + + + + + + diff --git a/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java new file mode 100644 index 00000000..346995b2 --- /dev/null +++ b/src/main/java/com/youlai/boot/platform/ai/service/impl/AiCommandServiceImpl.java @@ -0,0 +1,262 @@ +package com.youlai.boot.platform.ai.service.impl; + +import cn.hutool.json.JSONArray; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; +import com.youlai.boot.platform.ai.config.AiProperties; +import com.youlai.boot.platform.ai.model.dto.*; +import com.youlai.boot.platform.ai.model.entity.AiCommandAudit; +import com.youlai.boot.platform.ai.provider.AiProvider; +import com.youlai.boot.platform.ai.provider.AiProviderFactory; +import com.youlai.boot.platform.ai.service.AiCommandService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.*; + +/** + * AI 命令服务实现类(重构版) + * + * 重构改进: + * 1. ✅ 使用策略模式 + 工厂模式管理提供商,消除 switch-case + * 2. ✅ 配置映射化,添加新提供商只需配置,无需修改代码 + * 3. ✅ 统一命名为 base-url,符合行业惯例 + * 4. ✅ Service 层直接返回 DTO,不包装 Result(由 Controller 统一处理) + * 5. ✅ 职责清晰,扩展性强 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Service +@Slf4j +@RequiredArgsConstructor +public class AiCommandServiceImpl implements AiCommandService { + + private final AiProperties aiProperties; + private final AiProviderFactory providerFactory; + + // 审计日志存储(简化实现,实际应使用数据库) + private final Map auditStore = new HashMap<>(); + + /** + * 解析自然语言命令 + * + * 注意:直接返回 DTO,不包装 Result + * Controller 负责统一包装成 Result + */ + @Override + public AiCommandResponseDTO parseCommand(AiCommandRequestDTO request, HttpServletRequest httpRequest) { + // 检查 AI 功能是否启用 + if (!aiProperties.getEnabled()) { + throw new IllegalStateException("AI 功能未启用,请在配置文件中设置 ai.enabled=true"); + } + + try { + // 获取当前提供商(自动校验配置) + AiProvider provider = providerFactory.getCurrentProvider(); + + log.info("📤 使用 {} 解析命令: {}", provider.getProviderName(), request.getCommand()); + + // 构建提示词 + String systemPrompt = buildSystemPrompt(); + String userPrompt = buildUserPrompt(request); + + // 调用 AI API + String response = provider.call(systemPrompt, userPrompt); + + // 解析响应 + return parseAiResponse(response); + + } catch (IllegalStateException e) { + // 配置错误,抛出让 Controller 处理 + throw e; + } catch (Exception e) { + log.error("解析命令失败", e); + throw new RuntimeException("解析命令失败: " + e.getMessage(), e); + } + } + + /** + * 执行已解析的命令 + */ + @Override + public AiExecuteResponseDTO executeCommand(AiExecuteRequestDTO request, HttpServletRequest httpRequest) { + // TODO: 实现命令执行逻辑 + throw new UnsupportedOperationException("待实现"); + } + + /** + * 获取命令执行历史 + */ + @Override + public Map getCommandHistory(Integer page, Integer size) { + List allAudits = new ArrayList<>(auditStore.values()); + allAudits.sort(Comparator.comparing(AiCommandAudit::getCreateTime).reversed()); + + int total = allAudits.size(); + int start = (page - 1) * size; + int end = Math.min(start + size, total); + + List pageData = start < total ? allAudits.subList(start, end) : new ArrayList<>(); + + Map result = new HashMap<>(); + result.put("list", pageData); + result.put("total", total); + result.put("page", page); + result.put("size", size); + + return result; + } + + /** + * 获取可用的函数列表 + */ + @Override + public List> getAvailableFunctions() { + List> functions = new ArrayList<>(); + + // 用户管理函数 + functions.add(createFunctionDef( + "deleteUser", + "删除用户", + Map.of("name", "String - 用户姓名", "id", "Long - 用户ID(可选)") + )); + + functions.add(createFunctionDef( + "updateUser", + "更新用户信息", + Map.of("id", "Long - 用户ID", "nickname", "String - 昵称", "status", "Integer - 状态") + )); + + functions.add(createFunctionDef( + "queryUsers", + "查询用户列表", + Map.of("name", "String - 姓名(可选)", "status", "Integer - 状态(可选)") + )); + + // 角色管理函数 + functions.add(createFunctionDef( + "assignRole", + "分配角色给用户", + Map.of("userId", "Long - 用户ID", "roleIds", "List - 角色ID列表") + )); + + return functions; + } + + /** + * 撤销命令执行 + */ + @Override + public void rollbackCommand(String auditId) { + AiCommandAudit audit = auditStore.get(auditId); + if (audit == null) { + throw new RuntimeException("审计记录不存在"); + } + + if (!"success".equals(audit.getExecuteStatus())) { + throw new RuntimeException("只能撤销成功执行的命令"); + } + + // TODO: 实现具体的回滚逻辑 + log.info("撤销命令执行: auditId={}, function={}", auditId, audit.getFunctionName()); + throw new UnsupportedOperationException("回滚功能尚未实现"); + } + + // ==================== 私有方法 ==================== + + /** + * 构建系统提示词(包含可用函数定义) + */ + private String buildSystemPrompt() { + return """ + 你是一个专业的命令解析助手。你的任务是将用户的自然语言命令转换为结构化的函数调用。 + + 可用函数: + 1. queryUsers - 查询用户列表 + 参数:keywords(搜索关键字), status(状态), deptId(部门ID) + + 2. deleteUser - 删除用户 + 参数:userId(用户ID) + + 3. updateUser - 更新用户信息 + 参数:userId(用户ID), nickname(昵称), mobile(手机号) + + 请将命令解析为以下 JSON 格式: + { + "functionCalls": [ + { + "function": "函数名", + "parameters": { "参数名": "参数值" }, + "description": "操作说明" + } + ] + } + """; + } + + /** + * 构建用户提示词 + */ + private String buildUserPrompt(AiCommandRequestDTO request) { + return "请解析以下命令:" + request.getCommand(); + } + + /** + * 解析 AI 响应 + */ + private AiCommandResponseDTO parseAiResponse(String response) { + try { + // 提取 JSON + int jsonStart = response.indexOf("{"); + int jsonEnd = response.lastIndexOf("}") + 1; + + if (jsonStart == -1 || jsonEnd == 0) { + throw new IllegalArgumentException("AI 返回格式错误:未找到 JSON"); + } + + String jsonStr = response.substring(jsonStart, jsonEnd); + JSONObject json = JSONUtil.parseObj(jsonStr); + + // 解析函数调用列表 + List functionCalls = new ArrayList<>(); + JSONArray callsArray = json.getJSONArray("functionCalls"); + + if (callsArray != null) { + for (int i = 0; i < callsArray.size(); i++) { + JSONObject call = callsArray.getJSONObject(i); + functionCalls.add(FunctionCallDTO.builder() + .name(call.getStr("function")) + .arguments(call.getJSONObject("parameters") != null ? + call.getJSONObject("parameters").toBean(Map.class) : new HashMap<>()) + .description(call.getStr("description")) + .build()); + } + } + + return AiCommandResponseDTO.builder() + .success(true) + .functionCalls(functionCalls) + .rawResponse(response) + .build(); + + } catch (Exception e) { + log.error("解析 AI 响应失败", e); + throw new RuntimeException("解析响应失败: " + e.getMessage(), e); + } + } + + /** + * 创建函数定义 + */ + private Map createFunctionDef(String name, String description, Map parameters) { + Map func = new HashMap<>(); + func.put("name", name); + func.put("description", description); + func.put("parameters", parameters); + return func; + } +} + diff --git a/src/main/java/com/youlai/boot/shared/codegen/controller/CodegenController.java b/src/main/java/com/youlai/boot/platform/codegen/controller/CodegenController.java similarity index 79% rename from src/main/java/com/youlai/boot/shared/codegen/controller/CodegenController.java rename to src/main/java/com/youlai/boot/platform/codegen/controller/CodegenController.java index 50119908..497682f4 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/controller/CodegenController.java +++ b/src/main/java/com/youlai/boot/platform/codegen/controller/CodegenController.java @@ -1,17 +1,17 @@ -package com.youlai.boot.shared.codegen.controller; +package com.youlai.boot.platform.codegen.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.youlai.boot.common.result.PageResult; -import com.youlai.boot.common.result.Result; +import com.youlai.boot.core.web.PageResult; +import com.youlai.boot.core.web.Result; import com.youlai.boot.config.property.CodegenProperties; import com.youlai.boot.common.enums.LogModuleEnum; -import com.youlai.boot.shared.codegen.service.CodegenService; -import com.youlai.boot.shared.codegen.model.form.GenConfigForm; -import com.youlai.boot.shared.codegen.model.query.TablePageQuery; -import com.youlai.boot.shared.codegen.model.vo.CodegenPreviewVO; -import com.youlai.boot.shared.codegen.model.vo.TablePageVO; +import com.youlai.boot.platform.codegen.service.CodegenService; +import com.youlai.boot.platform.codegen.model.form.GenConfigForm; +import com.youlai.boot.platform.codegen.model.query.TablePageQuery; +import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO; +import com.youlai.boot.platform.codegen.model.vo.TablePageVO; import com.youlai.boot.common.annotation.Log; -import com.youlai.boot.shared.codegen.service.GenConfigService; +import com.youlai.boot.platform.codegen.service.GenConfigService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -82,17 +82,19 @@ public class CodegenController { @Operation(summary = "获取预览生成代码") @GetMapping("/{tableName}/preview") @Log(value = "预览生成代码", module = LogModuleEnum.OTHER) - public Result> getTablePreviewData(@PathVariable String tableName) { - List list = codegenService.getCodegenPreviewData(tableName); + public Result> getTablePreviewData(@PathVariable String tableName, + @RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType) { + List list = codegenService.getCodegenPreviewData(tableName, pageType); return Result.success(list); } @Operation(summary = "下载代码") @GetMapping("/{tableName}/download") @Log(value = "下载代码", module = LogModuleEnum.OTHER) - public void downloadZip(HttpServletResponse response, @PathVariable String tableName) { + public void downloadZip(HttpServletResponse response, @PathVariable String tableName, + @RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType) { String[] tableNames = tableName.split(","); - byte[] data = codegenService.downloadCode(tableNames); + byte[] data = codegenService.downloadCode(tableNames, pageType); response.reset(); response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); diff --git a/src/main/java/com/youlai/boot/shared/codegen/converter/CodegenConverter.java b/src/main/java/com/youlai/boot/platform/codegen/converter/CodegenConverter.java similarity index 74% rename from src/main/java/com/youlai/boot/shared/codegen/converter/CodegenConverter.java rename to src/main/java/com/youlai/boot/platform/codegen/converter/CodegenConverter.java index 0508ff99..999d5fd1 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/converter/CodegenConverter.java +++ b/src/main/java/com/youlai/boot/platform/codegen/converter/CodegenConverter.java @@ -1,8 +1,8 @@ -package com.youlai.boot.shared.codegen.converter; +package com.youlai.boot.platform.codegen.converter; -import com.youlai.boot.shared.codegen.model.entity.GenConfig; -import com.youlai.boot.shared.codegen.model.entity.GenFieldConfig; -import com.youlai.boot.shared.codegen.model.form.GenConfigForm; +import com.youlai.boot.platform.codegen.model.entity.GenConfig; +import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.model.form.GenConfigForm; import org.mapstruct.Mapper; import org.mapstruct.Mapping; @@ -23,6 +23,8 @@ public interface CodegenConverter { @Mapping(source = "genConfig.packageName", target = "packageName") @Mapping(source = "genConfig.entityName", target = "entityName") @Mapping(source = "genConfig.author", target = "author") + @Mapping(source = "genConfig.pageType", target = "pageType") + @Mapping(source = "genConfig.removeTablePrefix", target = "removeTablePrefix") @Mapping(source = "fieldConfigs", target = "fieldConfigs") GenConfigForm toGenConfigForm(GenConfig genConfig, List fieldConfigs); diff --git a/src/main/java/com/youlai/boot/shared/codegen/enums/FormTypeEnum.java b/src/main/java/com/youlai/boot/platform/codegen/enums/FormTypeEnum.java similarity index 97% rename from src/main/java/com/youlai/boot/shared/codegen/enums/FormTypeEnum.java rename to src/main/java/com/youlai/boot/platform/codegen/enums/FormTypeEnum.java index 255d83f5..34bcbd6e 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/enums/FormTypeEnum.java +++ b/src/main/java/com/youlai/boot/platform/codegen/enums/FormTypeEnum.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.codegen.enums; +package com.youlai.boot.platform.codegen.enums; import com.baomidou.mybatisplus.annotation.EnumValue; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/src/main/java/com/youlai/boot/shared/codegen/enums/JavaTypeEnum.java b/src/main/java/com/youlai/boot/platform/codegen/enums/JavaTypeEnum.java similarity index 93% rename from src/main/java/com/youlai/boot/shared/codegen/enums/JavaTypeEnum.java rename to src/main/java/com/youlai/boot/platform/codegen/enums/JavaTypeEnum.java index a5a4bf73..44ea85c6 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/enums/JavaTypeEnum.java +++ b/src/main/java/com/youlai/boot/platform/codegen/enums/JavaTypeEnum.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.codegen.enums; +package com.youlai.boot.platform.codegen.enums; import lombok.Getter; @@ -28,7 +28,8 @@ public enum JavaTypeEnum { DOUBLE("double", "Double", "number"), DECIMAL("decimal", "BigDecimal", "number"), DATE("date", "LocalDate", "Date"), - DATETIME("datetime", "LocalDateTime", "Date"); + DATETIME("datetime", "LocalDateTime", "Date"), + TIMESTAMP("timestamp", "LocalDateTime", "Date"); // 数据库类型 private final String dbType; diff --git a/src/main/java/com/youlai/boot/shared/codegen/enums/QueryTypeEnum.java b/src/main/java/com/youlai/boot/platform/codegen/enums/QueryTypeEnum.java similarity index 96% rename from src/main/java/com/youlai/boot/shared/codegen/enums/QueryTypeEnum.java rename to src/main/java/com/youlai/boot/platform/codegen/enums/QueryTypeEnum.java index cd2d52b4..7c0469d0 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/enums/QueryTypeEnum.java +++ b/src/main/java/com/youlai/boot/platform/codegen/enums/QueryTypeEnum.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.codegen.enums; +package com.youlai.boot.platform.codegen.enums; import com.baomidou.mybatisplus.annotation.EnumValue; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/src/main/java/com/youlai/boot/shared/codegen/mapper/DatabaseMapper.java b/src/main/java/com/youlai/boot/platform/codegen/mapper/DatabaseMapper.java similarity index 72% rename from src/main/java/com/youlai/boot/shared/codegen/mapper/DatabaseMapper.java rename to src/main/java/com/youlai/boot/platform/codegen/mapper/DatabaseMapper.java index 1a54cdb2..3d742c63 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/mapper/DatabaseMapper.java +++ b/src/main/java/com/youlai/boot/platform/codegen/mapper/DatabaseMapper.java @@ -1,11 +1,11 @@ -package com.youlai.boot.shared.codegen.mapper; +package com.youlai.boot.platform.codegen.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.youlai.boot.shared.codegen.model.bo.ColumnMetaData; -import com.youlai.boot.shared.codegen.model.bo.TableMetaData; -import com.youlai.boot.shared.codegen.model.query.TablePageQuery; -import com.youlai.boot.shared.codegen.model.vo.TablePageVO; +import com.youlai.boot.platform.codegen.model.bo.ColumnMetaData; +import com.youlai.boot.platform.codegen.model.bo.TableMetaData; +import com.youlai.boot.platform.codegen.model.query.TablePageQuery; +import com.youlai.boot.platform.codegen.model.vo.TablePageVO; import org.apache.ibatis.annotations.Mapper; import java.util.List; diff --git a/src/main/java/com/youlai/boot/shared/codegen/mapper/GenConfigMapper.java b/src/main/java/com/youlai/boot/platform/codegen/mapper/GenConfigMapper.java similarity index 69% rename from src/main/java/com/youlai/boot/shared/codegen/mapper/GenConfigMapper.java rename to src/main/java/com/youlai/boot/platform/codegen/mapper/GenConfigMapper.java index a33aa22a..ac3c79af 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/mapper/GenConfigMapper.java +++ b/src/main/java/com/youlai/boot/platform/codegen/mapper/GenConfigMapper.java @@ -1,7 +1,7 @@ -package com.youlai.boot.shared.codegen.mapper; +package com.youlai.boot.platform.codegen.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.youlai.boot.shared.codegen.model.entity.GenConfig; +import com.youlai.boot.platform.codegen.model.entity.GenConfig; import org.apache.ibatis.annotations.Mapper; /** diff --git a/src/main/java/com/youlai/boot/shared/codegen/mapper/GenFieldConfigMapper.java b/src/main/java/com/youlai/boot/platform/codegen/mapper/GenFieldConfigMapper.java similarity index 69% rename from src/main/java/com/youlai/boot/shared/codegen/mapper/GenFieldConfigMapper.java rename to src/main/java/com/youlai/boot/platform/codegen/mapper/GenFieldConfigMapper.java index de54ec3e..14aaf2b1 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/mapper/GenFieldConfigMapper.java +++ b/src/main/java/com/youlai/boot/platform/codegen/mapper/GenFieldConfigMapper.java @@ -1,7 +1,7 @@ -package com.youlai.boot.shared.codegen.mapper; +package com.youlai.boot.platform.codegen.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.youlai.boot.shared.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; import org.apache.ibatis.annotations.Mapper; /** diff --git a/src/main/java/com/youlai/boot/shared/codegen/model/bo/ColumnMetaData.java b/src/main/java/com/youlai/boot/platform/codegen/model/bo/ColumnMetaData.java similarity index 94% rename from src/main/java/com/youlai/boot/shared/codegen/model/bo/ColumnMetaData.java rename to src/main/java/com/youlai/boot/platform/codegen/model/bo/ColumnMetaData.java index 0bb0e6c0..154947bc 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/model/bo/ColumnMetaData.java +++ b/src/main/java/com/youlai/boot/platform/codegen/model/bo/ColumnMetaData.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.codegen.model.bo; +package com.youlai.boot.platform.codegen.model.bo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/src/main/java/com/youlai/boot/shared/codegen/model/bo/TableMetaData.java b/src/main/java/com/youlai/boot/platform/codegen/model/bo/TableMetaData.java similarity index 91% rename from src/main/java/com/youlai/boot/shared/codegen/model/bo/TableMetaData.java rename to src/main/java/com/youlai/boot/platform/codegen/model/bo/TableMetaData.java index 0f8a18bc..a741ba49 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/model/bo/TableMetaData.java +++ b/src/main/java/com/youlai/boot/platform/codegen/model/bo/TableMetaData.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.codegen.model.bo; +package com.youlai.boot.platform.codegen.model.bo; import lombok.Data; diff --git a/src/main/java/com/youlai/boot/shared/codegen/model/entity/GenConfig.java b/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenConfig.java similarity index 76% rename from src/main/java/com/youlai/boot/shared/codegen/model/entity/GenConfig.java rename to src/main/java/com/youlai/boot/platform/codegen/model/entity/GenConfig.java index 7fddaf12..13ad52f7 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/model/entity/GenConfig.java +++ b/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenConfig.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.codegen.model.entity; +package com.youlai.boot.platform.codegen.model.entity; import com.baomidou.mybatisplus.annotation.*; @@ -51,4 +51,14 @@ public class GenConfig extends BaseEntity { * 作者 */ private String author; + + /** + * 页面类型 classic|curd + */ + private String pageType; + + /** + * 要移除的表前缀,如: sys_ + */ + private String removeTablePrefix; } \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/shared/codegen/model/entity/GenFieldConfig.java b/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenFieldConfig.java similarity index 90% rename from src/main/java/com/youlai/boot/shared/codegen/model/entity/GenFieldConfig.java rename to src/main/java/com/youlai/boot/platform/codegen/model/entity/GenFieldConfig.java index 7b8e62e7..7c9bb91f 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/model/entity/GenFieldConfig.java +++ b/src/main/java/com/youlai/boot/platform/codegen/model/entity/GenFieldConfig.java @@ -1,12 +1,12 @@ -package com.youlai.boot.shared.codegen.model.entity; +package com.youlai.boot.platform.codegen.model.entity; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonIgnore; import com.youlai.boot.common.base.BaseEntity; -import com.youlai.boot.shared.codegen.enums.FormTypeEnum; -import com.youlai.boot.shared.codegen.enums.QueryTypeEnum; +import com.youlai.boot.platform.codegen.enums.FormTypeEnum; +import com.youlai.boot.platform.codegen.enums.QueryTypeEnum; import lombok.Getter; import lombok.Setter; diff --git a/src/main/java/com/youlai/boot/shared/codegen/model/form/GenConfigForm.java b/src/main/java/com/youlai/boot/platform/codegen/model/form/GenConfigForm.java similarity index 86% rename from src/main/java/com/youlai/boot/shared/codegen/model/form/GenConfigForm.java rename to src/main/java/com/youlai/boot/platform/codegen/model/form/GenConfigForm.java index 2893788d..cf5d22ec 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/model/form/GenConfigForm.java +++ b/src/main/java/com/youlai/boot/platform/codegen/model/form/GenConfigForm.java @@ -1,7 +1,7 @@ -package com.youlai.boot.shared.codegen.model.form; +package com.youlai.boot.platform.codegen.model.form; -import com.youlai.boot.shared.codegen.enums.FormTypeEnum; -import com.youlai.boot.shared.codegen.enums.QueryTypeEnum; +import com.youlai.boot.platform.codegen.enums.FormTypeEnum; +import com.youlai.boot.platform.codegen.enums.QueryTypeEnum; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -50,6 +50,12 @@ public class GenConfigForm { @Schema(description = "前端应用名") private String frontendAppName; + @Schema(description = "页面类型 classic|curd", example = "classic") + private String pageType; + + @Schema(description = "要移除的表前缀,如: sys_", example = "sys_") + private String removeTablePrefix; + @Schema(description = "字段配置") @Data public static class FieldConfig { diff --git a/src/main/java/com/youlai/boot/shared/codegen/model/query/TablePageQuery.java b/src/main/java/com/youlai/boot/platform/codegen/model/query/TablePageQuery.java similarity index 91% rename from src/main/java/com/youlai/boot/shared/codegen/model/query/TablePageQuery.java rename to src/main/java/com/youlai/boot/platform/codegen/model/query/TablePageQuery.java index f44cd8a3..4213560d 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/model/query/TablePageQuery.java +++ b/src/main/java/com/youlai/boot/platform/codegen/model/query/TablePageQuery.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.codegen.model.query; +package com.youlai.boot.platform.codegen.model.query; import com.fasterxml.jackson.annotation.JsonIgnore; import com.youlai.boot.common.base.BasePageQuery; diff --git a/src/main/java/com/youlai/boot/shared/codegen/model/vo/CodegenPreviewVO.java b/src/main/java/com/youlai/boot/platform/codegen/model/vo/CodegenPreviewVO.java similarity index 89% rename from src/main/java/com/youlai/boot/shared/codegen/model/vo/CodegenPreviewVO.java rename to src/main/java/com/youlai/boot/platform/codegen/model/vo/CodegenPreviewVO.java index 2b8e1a71..749d21bf 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/model/vo/CodegenPreviewVO.java +++ b/src/main/java/com/youlai/boot/platform/codegen/model/vo/CodegenPreviewVO.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.codegen.model.vo; +package com.youlai.boot.platform.codegen.model.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/src/main/java/com/youlai/boot/shared/codegen/model/vo/TablePageVO.java b/src/main/java/com/youlai/boot/platform/codegen/model/vo/TablePageVO.java similarity index 94% rename from src/main/java/com/youlai/boot/shared/codegen/model/vo/TablePageVO.java rename to src/main/java/com/youlai/boot/platform/codegen/model/vo/TablePageVO.java index 30ff28b8..0b011181 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/model/vo/TablePageVO.java +++ b/src/main/java/com/youlai/boot/platform/codegen/model/vo/TablePageVO.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.codegen.model.vo; +package com.youlai.boot.platform.codegen.model.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/src/main/java/com/youlai/boot/shared/codegen/service/CodegenService.java b/src/main/java/com/youlai/boot/platform/codegen/service/CodegenService.java similarity index 65% rename from src/main/java/com/youlai/boot/shared/codegen/service/CodegenService.java rename to src/main/java/com/youlai/boot/platform/codegen/service/CodegenService.java index ce48fde3..32c020d1 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/service/CodegenService.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/CodegenService.java @@ -1,9 +1,9 @@ -package com.youlai.boot.shared.codegen.service; +package com.youlai.boot.platform.codegen.service; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.youlai.boot.shared.codegen.model.query.TablePageQuery; -import com.youlai.boot.shared.codegen.model.vo.CodegenPreviewVO; -import com.youlai.boot.shared.codegen.model.vo.TablePageVO; +import com.youlai.boot.platform.codegen.model.query.TablePageQuery; +import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO; +import com.youlai.boot.platform.codegen.model.vo.TablePageVO; import java.util.List; @@ -29,12 +29,12 @@ public interface CodegenService { * @param tableName 表名 * @return */ - List getCodegenPreviewData(String tableName); + List getCodegenPreviewData(String tableName, String pageType); /** * 下载代码 * @param tableNames 表名 * @return */ - byte[] downloadCode(String[] tableNames); + byte[] downloadCode(String[] tableNames, String pageType); } diff --git a/src/main/java/com/youlai/boot/shared/codegen/service/GenConfigService.java b/src/main/java/com/youlai/boot/platform/codegen/service/GenConfigService.java similarity index 78% rename from src/main/java/com/youlai/boot/shared/codegen/service/GenConfigService.java rename to src/main/java/com/youlai/boot/platform/codegen/service/GenConfigService.java index 9b039b2c..2731260e 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/service/GenConfigService.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/GenConfigService.java @@ -1,8 +1,8 @@ -package com.youlai.boot.shared.codegen.service; +package com.youlai.boot.platform.codegen.service; import com.baomidou.mybatisplus.extension.service.IService; -import com.youlai.boot.shared.codegen.model.entity.GenConfig; -import com.youlai.boot.shared.codegen.model.form.GenConfigForm; +import com.youlai.boot.platform.codegen.model.entity.GenConfig; +import com.youlai.boot.platform.codegen.model.form.GenConfigForm; /** * 代码生成配置接口 diff --git a/src/main/java/com/youlai/boot/shared/codegen/service/GenFieldConfigService.java b/src/main/java/com/youlai/boot/platform/codegen/service/GenFieldConfigService.java similarity index 63% rename from src/main/java/com/youlai/boot/shared/codegen/service/GenFieldConfigService.java rename to src/main/java/com/youlai/boot/platform/codegen/service/GenFieldConfigService.java index 5433da03..fbfda4d4 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/service/GenFieldConfigService.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/GenFieldConfigService.java @@ -1,7 +1,7 @@ -package com.youlai.boot.shared.codegen.service; +package com.youlai.boot.platform.codegen.service; import com.baomidou.mybatisplus.extension.service.IService; -import com.youlai.boot.shared.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; /** * 代码生成配置接口 diff --git a/src/main/java/com/youlai/boot/shared/codegen/service/impl/CodegenServiceImpl.java b/src/main/java/com/youlai/boot/platform/codegen/service/impl/CodegenServiceImpl.java similarity index 86% rename from src/main/java/com/youlai/boot/shared/codegen/service/impl/CodegenServiceImpl.java rename to src/main/java/com/youlai/boot/platform/codegen/service/impl/CodegenServiceImpl.java index 6ae9e04e..aa4e507b 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/service/impl/CodegenServiceImpl.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/impl/CodegenServiceImpl.java @@ -1,7 +1,8 @@ -package com.youlai.boot.shared.codegen.service.impl; +package com.youlai.boot.platform.codegen.service.impl; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.file.FileNameUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.template.Template; @@ -10,18 +11,18 @@ import cn.hutool.extra.template.TemplateEngine; import cn.hutool.extra.template.TemplateUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.youlai.boot.shared.codegen.enums.JavaTypeEnum; +import com.youlai.boot.platform.codegen.enums.JavaTypeEnum; import com.youlai.boot.config.property.CodegenProperties; -import com.youlai.boot.shared.codegen.service.GenConfigService; -import com.youlai.boot.shared.codegen.service.GenFieldConfigService; -import com.youlai.boot.shared.codegen.service.CodegenService; -import com.youlai.boot.common.exception.BusinessException; -import com.youlai.boot.shared.codegen.mapper.DatabaseMapper; -import com.youlai.boot.shared.codegen.model.entity.GenConfig; -import com.youlai.boot.shared.codegen.model.entity.GenFieldConfig; -import com.youlai.boot.shared.codegen.model.query.TablePageQuery; -import com.youlai.boot.shared.codegen.model.vo.CodegenPreviewVO; -import com.youlai.boot.shared.codegen.model.vo.TablePageVO; +import com.youlai.boot.platform.codegen.service.GenConfigService; +import com.youlai.boot.platform.codegen.service.GenFieldConfigService; +import com.youlai.boot.platform.codegen.service.CodegenService; +import com.youlai.boot.core.exception.BusinessException; +import com.youlai.boot.platform.codegen.mapper.DatabaseMapper; +import com.youlai.boot.platform.codegen.model.entity.GenConfig; +import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.model.query.TablePageQuery; +import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO; +import com.youlai.boot.platform.codegen.model.vo.TablePageVO; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -72,7 +73,7 @@ public class CodegenServiceImpl implements CodegenService { * @return 预览数据 */ @Override - public List getCodegenPreviewData(String tableName) { + public List getCodegenPreviewData(String tableName, String pageType) { List list = new ArrayList<>(); @@ -124,7 +125,9 @@ public class CodegenServiceImpl implements CodegenService { /* 3. 生成文件内容 */ // 将模板文件中的变量替换为具体的值 生成代码内容 - String content = getCodeContent(templateConfig, genConfig, fieldConfigs); + // 优先使用保存的 ui,没有则使用请求参数 + String finalType = StrUtil.blankToDefault(genConfig.getPageType(), pageType); + String content = getCodeContent(templateConfig, genConfig, fieldConfigs, finalType); previewVO.setContent(content); list.add(previewVO); @@ -146,7 +149,8 @@ public class CodegenServiceImpl implements CodegenService { } else if ("MapperXml".equals(templateName)) { return entityName + "Mapper" + extension; } else if ("API".equals(templateName)) { - return StrUtil.toSymbolCase(entityName, '-') + extension; + // 生成 user-api.ts 命名 + return StrUtil.toSymbolCase(entityName, '-') + "-api" + extension; } else if ("VIEW".equals(templateName)) { return "index.vue"; } @@ -211,7 +215,7 @@ public class CodegenServiceImpl implements CodegenService { * @param fieldConfigs 字段配置 * @return 代码内容 */ - private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs) { + private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List fieldConfigs, String pageType) { Map bindMap = new HashMap<>(); @@ -252,7 +256,12 @@ public class CodegenServiceImpl implements CodegenService { bindMap.put("hasRequiredField", hasRequiredField); TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); - Template template = templateEngine.getTemplate(templateConfig.getTemplatePath()); + // 根据 ui 选择不同的前端页面模板:默认 index.vue.vm;封装版使用 index.curd.vue.vm + String path = templateConfig.getTemplatePath(); + if ("curd".equalsIgnoreCase(pageType) && path.endsWith("index.vue.vm")) { + path = path.replace("index.vue.vm", "index.curd.vue.vm"); + } + Template template = templateEngine.getTemplate(path); return template.render(bindMap); } @@ -264,13 +273,13 @@ public class CodegenServiceImpl implements CodegenService { * @return 压缩文件字节数组 */ @Override - public byte[] downloadCode(String[] tableNames) { + public byte[] downloadCode(String[] tableNames, String ui) { try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ZipOutputStream zip = new ZipOutputStream(outputStream)) { // 遍历每个表名,生成对应的代码并压缩到 zip 文件中 for (String tableName : tableNames) { - generateAndZipCode(tableName, zip); + generateAndZipCode(tableName, zip, ui); } // 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整 zip.finish(); @@ -288,8 +297,8 @@ public class CodegenServiceImpl implements CodegenService { * @param tableName 表名 * @param zip 压缩文件输出流 */ - private void generateAndZipCode(String tableName, ZipOutputStream zip) { - List codePreviewList = getCodegenPreviewData(tableName); + private void generateAndZipCode(String tableName, ZipOutputStream zip, String ui) { + List codePreviewList = getCodegenPreviewData(tableName, ui); for (CodegenPreviewVO codePreview : codePreviewList) { String fileName = codePreview.getFileName(); diff --git a/src/main/java/com/youlai/boot/shared/codegen/service/impl/GenConfigServiceImpl.java b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenConfigServiceImpl.java similarity index 85% rename from src/main/java/com/youlai/boot/shared/codegen/service/impl/GenConfigServiceImpl.java rename to src/main/java/com/youlai/boot/platform/codegen/service/impl/GenConfigServiceImpl.java index 906c2982..eaf59a40 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/service/impl/GenConfigServiceImpl.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenConfigServiceImpl.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.codegen.service.impl; +package com.youlai.boot.platform.codegen.service.impl; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.Assert; @@ -7,21 +7,21 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.youlai.boot.YouLaiBootApplication; import com.youlai.boot.common.enums.EnvEnum; -import com.youlai.boot.shared.codegen.enums.FormTypeEnum; -import com.youlai.boot.shared.codegen.enums.JavaTypeEnum; -import com.youlai.boot.shared.codegen.enums.QueryTypeEnum; -import com.youlai.boot.common.exception.BusinessException; +import com.youlai.boot.platform.codegen.enums.FormTypeEnum; +import com.youlai.boot.platform.codegen.enums.JavaTypeEnum; +import com.youlai.boot.platform.codegen.enums.QueryTypeEnum; +import com.youlai.boot.core.exception.BusinessException; import com.youlai.boot.config.property.CodegenProperties; -import com.youlai.boot.shared.codegen.converter.CodegenConverter; -import com.youlai.boot.shared.codegen.mapper.DatabaseMapper; -import com.youlai.boot.shared.codegen.mapper.GenConfigMapper; -import com.youlai.boot.shared.codegen.model.bo.ColumnMetaData; -import com.youlai.boot.shared.codegen.model.bo.TableMetaData; -import com.youlai.boot.shared.codegen.model.entity.GenConfig; -import com.youlai.boot.shared.codegen.model.entity.GenFieldConfig; -import com.youlai.boot.shared.codegen.model.form.GenConfigForm; -import com.youlai.boot.shared.codegen.service.GenConfigService; -import com.youlai.boot.shared.codegen.service.GenFieldConfigService; +import com.youlai.boot.platform.codegen.converter.CodegenConverter; +import com.youlai.boot.platform.codegen.mapper.DatabaseMapper; +import com.youlai.boot.platform.codegen.mapper.GenConfigMapper; +import com.youlai.boot.platform.codegen.model.bo.ColumnMetaData; +import com.youlai.boot.platform.codegen.model.bo.TableMetaData; +import com.youlai.boot.platform.codegen.model.entity.GenConfig; +import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.model.form.GenConfigForm; +import com.youlai.boot.platform.codegen.service.GenConfigService; +import com.youlai.boot.platform.codegen.service.GenFieldConfigService; import com.youlai.boot.system.service.MenuService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -83,8 +83,13 @@ public class GenConfigServiceImpl extends ServiceImpl SysUser - genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(tableName)))); + // 根据表名生成实体类名,支持去除前缀 例如:sys_user -> SysUser + String removePrefix = genConfig.getRemoveTablePrefix(); + String processedTable = tableName; + if (StrUtil.isNotBlank(removePrefix) && StrUtil.startWith(tableName, removePrefix)) { + processedTable = StrUtil.removePrefix(tableName, removePrefix); + } + genConfig.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(processedTable)))); genConfig.setPackageName(YouLaiBootApplication.class.getPackageName()); genConfig.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名 diff --git a/src/main/java/com/youlai/boot/shared/codegen/service/impl/GenFieldConfigServiceImpl.java b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenFieldConfigServiceImpl.java similarity index 60% rename from src/main/java/com/youlai/boot/shared/codegen/service/impl/GenFieldConfigServiceImpl.java rename to src/main/java/com/youlai/boot/platform/codegen/service/impl/GenFieldConfigServiceImpl.java index 948711e5..e6dd938b 100644 --- a/src/main/java/com/youlai/boot/shared/codegen/service/impl/GenFieldConfigServiceImpl.java +++ b/src/main/java/com/youlai/boot/platform/codegen/service/impl/GenFieldConfigServiceImpl.java @@ -1,9 +1,9 @@ -package com.youlai.boot.shared.codegen.service.impl; +package com.youlai.boot.platform.codegen.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.youlai.boot.shared.codegen.mapper.GenFieldConfigMapper; -import com.youlai.boot.shared.codegen.model.entity.GenFieldConfig; -import com.youlai.boot.shared.codegen.service.GenFieldConfigService; +import com.youlai.boot.platform.codegen.mapper.GenFieldConfigMapper; +import com.youlai.boot.platform.codegen.model.entity.GenFieldConfig; +import com.youlai.boot.platform.codegen.service.GenFieldConfigService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/youlai/boot/shared/file/controller/FileController.java b/src/main/java/com/youlai/boot/platform/file/controller/FileController.java similarity index 88% rename from src/main/java/com/youlai/boot/shared/file/controller/FileController.java rename to src/main/java/com/youlai/boot/platform/file/controller/FileController.java index b07a9c21..a6b63feb 100644 --- a/src/main/java/com/youlai/boot/shared/file/controller/FileController.java +++ b/src/main/java/com/youlai/boot/platform/file/controller/FileController.java @@ -1,8 +1,8 @@ -package com.youlai.boot.shared.file.controller; +package com.youlai.boot.platform.file.controller; -import com.youlai.boot.common.result.Result; -import com.youlai.boot.shared.file.service.FileService; -import com.youlai.boot.shared.file.model.FileInfo; +import com.youlai.boot.core.web.Result; +import com.youlai.boot.platform.file.service.FileService; +import com.youlai.boot.platform.file.model.FileInfo; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.enums.ParameterIn; import io.swagger.v3.oas.annotations.media.Schema; diff --git a/src/main/java/com/youlai/boot/shared/file/model/FileInfo.java b/src/main/java/com/youlai/boot/platform/file/model/FileInfo.java similarity index 88% rename from src/main/java/com/youlai/boot/shared/file/model/FileInfo.java rename to src/main/java/com/youlai/boot/platform/file/model/FileInfo.java index ec550a18..b50acc51 100644 --- a/src/main/java/com/youlai/boot/shared/file/model/FileInfo.java +++ b/src/main/java/com/youlai/boot/platform/file/model/FileInfo.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.file.model; +package com.youlai.boot.platform.file.model; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/src/main/java/com/youlai/boot/shared/file/service/FileService.java b/src/main/java/com/youlai/boot/platform/file/service/FileService.java similarity index 82% rename from src/main/java/com/youlai/boot/shared/file/service/FileService.java rename to src/main/java/com/youlai/boot/platform/file/service/FileService.java index 42bce9b4..501abfdc 100644 --- a/src/main/java/com/youlai/boot/shared/file/service/FileService.java +++ b/src/main/java/com/youlai/boot/platform/file/service/FileService.java @@ -1,6 +1,6 @@ -package com.youlai.boot.shared.file.service; +package com.youlai.boot.platform.file.service; -import com.youlai.boot.shared.file.model.FileInfo; +import com.youlai.boot.platform.file.model.FileInfo; import org.springframework.web.multipart.MultipartFile; /** diff --git a/src/main/java/com/youlai/boot/shared/file/service/impl/AliyunFileService.java b/src/main/java/com/youlai/boot/platform/file/service/impl/AliyunFileService.java similarity index 95% rename from src/main/java/com/youlai/boot/shared/file/service/impl/AliyunFileService.java rename to src/main/java/com/youlai/boot/platform/file/service/impl/AliyunFileService.java index 004475fd..4831cbc1 100644 --- a/src/main/java/com/youlai/boot/shared/file/service/impl/AliyunFileService.java +++ b/src/main/java/com/youlai/boot/platform/file/service/impl/AliyunFileService.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.file.service.impl; +package com.youlai.boot.platform.file.service.impl; import cn.hutool.core.date.DateUtil; import cn.hutool.core.io.FileUtil; @@ -8,8 +8,8 @@ import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.model.ObjectMetadata; import com.aliyun.oss.model.PutObjectRequest; -import com.youlai.boot.shared.file.service.FileService; -import com.youlai.boot.shared.file.model.FileInfo; +import com.youlai.boot.platform.file.service.FileService; +import com.youlai.boot.platform.file.model.FileInfo; import jakarta.annotation.PostConstruct; import lombok.Data; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/youlai/boot/shared/file/service/impl/LocalFileService.java b/src/main/java/com/youlai/boot/platform/file/service/impl/LocalFileService.java similarity index 94% rename from src/main/java/com/youlai/boot/shared/file/service/impl/LocalFileService.java rename to src/main/java/com/youlai/boot/platform/file/service/impl/LocalFileService.java index 5cd0b97f..15fb9761 100644 --- a/src/main/java/com/youlai/boot/shared/file/service/impl/LocalFileService.java +++ b/src/main/java/com/youlai/boot/platform/file/service/impl/LocalFileService.java @@ -1,11 +1,11 @@ -package com.youlai.boot.shared.file.service.impl; +package com.youlai.boot.platform.file.service.impl; import cn.hutool.core.date.DatePattern; import cn.hutool.core.date.DateUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.IdUtil; -import com.youlai.boot.shared.file.model.FileInfo; -import com.youlai.boot.shared.file.service.FileService; +import com.youlai.boot.platform.file.model.FileInfo; +import com.youlai.boot.platform.file.service.FileService; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/youlai/boot/shared/file/service/impl/MinioFileService.java b/src/main/java/com/youlai/boot/platform/file/service/impl/MinioFileService.java similarity index 96% rename from src/main/java/com/youlai/boot/shared/file/service/impl/MinioFileService.java rename to src/main/java/com/youlai/boot/platform/file/service/impl/MinioFileService.java index 030ff1aa..be2ecafd 100644 --- a/src/main/java/com/youlai/boot/shared/file/service/impl/MinioFileService.java +++ b/src/main/java/com/youlai/boot/platform/file/service/impl/MinioFileService.java @@ -1,14 +1,14 @@ -package com.youlai.boot.shared.file.service.impl; +package com.youlai.boot.platform.file.service.impl; import cn.hutool.core.date.DateUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; -import com.youlai.boot.common.exception.BusinessException; -import com.youlai.boot.common.result.ResultCode; -import com.youlai.boot.shared.file.model.FileInfo; -import com.youlai.boot.shared.file.service.FileService; +import com.youlai.boot.core.exception.BusinessException; +import com.youlai.boot.core.web.ResultCode; +import com.youlai.boot.platform.file.model.FileInfo; +import com.youlai.boot.platform.file.service.FileService; import io.minio.*; import io.minio.http.Method; import jakarta.annotation.PostConstruct; diff --git a/src/main/java/com/youlai/boot/shared/mail/controller/MailController.java b/src/main/java/com/youlai/boot/platform/mail/controller/MailController.java similarity index 68% rename from src/main/java/com/youlai/boot/shared/mail/controller/MailController.java rename to src/main/java/com/youlai/boot/platform/mail/controller/MailController.java index 7eb1acbc..f31c127b 100644 --- a/src/main/java/com/youlai/boot/shared/mail/controller/MailController.java +++ b/src/main/java/com/youlai/boot/platform/mail/controller/MailController.java @@ -1,11 +1,11 @@ -package com.youlai.boot.shared.mail.controller; +package com.youlai.boot.platform.mail.controller; import org.springframework.web.bind.annotation.*; /** * 邮件控制层 * - * @author Ray + * @author Ray.Hao * @since 2.10.0 */ @RestController diff --git a/src/main/java/com/youlai/boot/shared/mail/service/MailService.java b/src/main/java/com/youlai/boot/platform/mail/service/MailService.java similarity index 92% rename from src/main/java/com/youlai/boot/shared/mail/service/MailService.java rename to src/main/java/com/youlai/boot/platform/mail/service/MailService.java index 60f4d827..f1304cfd 100644 --- a/src/main/java/com/youlai/boot/shared/mail/service/MailService.java +++ b/src/main/java/com/youlai/boot/platform/mail/service/MailService.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.mail.service; +package com.youlai.boot.platform.mail.service; /** * 邮件服务接口层 diff --git a/src/main/java/com/youlai/boot/shared/mail/service/impl/MailServiceImpl.java b/src/main/java/com/youlai/boot/platform/mail/service/impl/MailServiceImpl.java similarity index 95% rename from src/main/java/com/youlai/boot/shared/mail/service/impl/MailServiceImpl.java rename to src/main/java/com/youlai/boot/platform/mail/service/impl/MailServiceImpl.java index 2373401c..9ffc74b7 100644 --- a/src/main/java/com/youlai/boot/shared/mail/service/impl/MailServiceImpl.java +++ b/src/main/java/com/youlai/boot/platform/mail/service/impl/MailServiceImpl.java @@ -1,7 +1,7 @@ -package com.youlai.boot.shared.mail.service.impl; +package com.youlai.boot.platform.mail.service.impl; import com.youlai.boot.config.property.MailProperties; -import com.youlai.boot.shared.mail.service.MailService; +import com.youlai.boot.platform.mail.service.MailService; import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/youlai/boot/shared/sms/controller/SmsController.java b/src/main/java/com/youlai/boot/platform/sms/controller/SmsController.java similarity index 66% rename from src/main/java/com/youlai/boot/shared/sms/controller/SmsController.java rename to src/main/java/com/youlai/boot/platform/sms/controller/SmsController.java index 710601bc..c88b3f29 100644 --- a/src/main/java/com/youlai/boot/shared/sms/controller/SmsController.java +++ b/src/main/java/com/youlai/boot/platform/sms/controller/SmsController.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.sms.controller; +package com.youlai.boot.platform.sms.controller; /** * 短信控制层 diff --git a/src/main/java/com/youlai/boot/shared/sms/enums/SmsTypeEnum.java b/src/main/java/com/youlai/boot/platform/sms/enums/SmsTypeEnum.java similarity index 94% rename from src/main/java/com/youlai/boot/shared/sms/enums/SmsTypeEnum.java rename to src/main/java/com/youlai/boot/platform/sms/enums/SmsTypeEnum.java index b852a797..ceede8a8 100644 --- a/src/main/java/com/youlai/boot/shared/sms/enums/SmsTypeEnum.java +++ b/src/main/java/com/youlai/boot/platform/sms/enums/SmsTypeEnum.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.sms.enums; +package com.youlai.boot.platform.sms.enums; import com.youlai.boot.common.base.IBaseEnum; import lombok.Getter; diff --git a/src/main/java/com/youlai/boot/shared/sms/service/SmsService.java b/src/main/java/com/youlai/boot/platform/sms/service/SmsService.java similarity index 85% rename from src/main/java/com/youlai/boot/shared/sms/service/SmsService.java rename to src/main/java/com/youlai/boot/platform/sms/service/SmsService.java index 3deb1157..d4097353 100644 --- a/src/main/java/com/youlai/boot/shared/sms/service/SmsService.java +++ b/src/main/java/com/youlai/boot/platform/sms/service/SmsService.java @@ -1,6 +1,6 @@ -package com.youlai.boot.shared.sms.service; +package com.youlai.boot.platform.sms.service; -import com.youlai.boot.shared.sms.enums.SmsTypeEnum; +import com.youlai.boot.platform.sms.enums.SmsTypeEnum; import java.util.Map; diff --git a/src/main/java/com/youlai/boot/shared/sms/service/impl/AliyunSmsService.java b/src/main/java/com/youlai/boot/platform/sms/service/impl/AliyunSmsService.java similarity index 94% rename from src/main/java/com/youlai/boot/shared/sms/service/impl/AliyunSmsService.java rename to src/main/java/com/youlai/boot/platform/sms/service/impl/AliyunSmsService.java index b37ed844..a9adac40 100644 --- a/src/main/java/com/youlai/boot/shared/sms/service/impl/AliyunSmsService.java +++ b/src/main/java/com/youlai/boot/platform/sms/service/impl/AliyunSmsService.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.sms.service.impl; +package com.youlai.boot.platform.sms.service.impl; import cn.hutool.json.JSONUtil; import com.aliyuncs.CommonRequest; @@ -9,8 +9,8 @@ import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.http.MethodType; import com.aliyuncs.profile.DefaultProfile; import com.youlai.boot.config.property.AliyunSmsProperties; -import com.youlai.boot.shared.sms.enums.SmsTypeEnum; -import com.youlai.boot.shared.sms.service.SmsService; +import com.youlai.boot.platform.sms.enums.SmsTypeEnum; +import com.youlai.boot.platform.sms.service.SmsService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/youlai/boot/shared/websocket/controller/WebsocketController.java b/src/main/java/com/youlai/boot/platform/websocket/controller/WebsocketController.java similarity index 94% rename from src/main/java/com/youlai/boot/shared/websocket/controller/WebsocketController.java rename to src/main/java/com/youlai/boot/platform/websocket/controller/WebsocketController.java index 610a2455..0a9f8966 100644 --- a/src/main/java/com/youlai/boot/shared/websocket/controller/WebsocketController.java +++ b/src/main/java/com/youlai/boot/platform/websocket/controller/WebsocketController.java @@ -1,6 +1,6 @@ -package com.youlai.boot.shared.websocket.controller; +package com.youlai.boot.platform.websocket.controller; -import com.youlai.boot.shared.websocket.model.ChatMessage; +import com.youlai.boot.platform.websocket.model.ChatMessage; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.DestinationVariable; diff --git a/src/main/java/com/youlai/boot/shared/websocket/model/ChatMessage.java b/src/main/java/com/youlai/boot/platform/websocket/model/ChatMessage.java similarity index 86% rename from src/main/java/com/youlai/boot/shared/websocket/model/ChatMessage.java rename to src/main/java/com/youlai/boot/platform/websocket/model/ChatMessage.java index dff4ffac..a94b0b11 100644 --- a/src/main/java/com/youlai/boot/shared/websocket/model/ChatMessage.java +++ b/src/main/java/com/youlai/boot/platform/websocket/model/ChatMessage.java @@ -1,4 +1,4 @@ -package com.youlai.boot.shared.websocket.model; +package com.youlai.boot.platform.websocket.model; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/com/youlai/boot/plugin/knife4j/Knife4jOpenApiCustomizer.java b/src/main/java/com/youlai/boot/plugin/knife4j/Knife4jOpenApiCustomizer.java new file mode 100644 index 00000000..401cb978 --- /dev/null +++ b/src/main/java/com/youlai/boot/plugin/knife4j/Knife4jOpenApiCustomizer.java @@ -0,0 +1,150 @@ +package com.youlai.boot.plugin.knife4j; + + +import com.github.xiaoymin.knife4j.annotations.ApiSupport; +import com.github.xiaoymin.knife4j.core.conf.ExtensionsConstants; +import com.github.xiaoymin.knife4j.core.conf.GlobalConstants; +import com.github.xiaoymin.knife4j.spring.configuration.Knife4jProperties; +import com.github.xiaoymin.knife4j.spring.configuration.Knife4jSetting; +import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.models.OpenAPI; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ArrayUtils; +import org.springdoc.core.customizers.GlobalOpenApiCustomizer; +import org.springdoc.core.properties.SpringDocConfigProperties; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.RestController; + +import java.lang.annotation.Annotation; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 增强扩展属性支持 + * @since 4.1.0 + * @author xiaoymin@foxmail.com + * 2022/12/11 22:40 + */ +@Primary +@Configuration +@Slf4j +public class Knife4jOpenApiCustomizer extends com.github.xiaoymin.knife4j.spring.extension.Knife4jOpenApiCustomizer implements GlobalOpenApiCustomizer { + final Knife4jProperties knife4jProperties; + final SpringDocConfigProperties properties; + public Knife4jOpenApiCustomizer(Knife4jProperties knife4jProperties, SpringDocConfigProperties properties) { + super(knife4jProperties,properties); + this.knife4jProperties = knife4jProperties; + this.properties = properties; + } + + @Override + public void customise(OpenAPI openApi) { + log.debug("Knife4j OpenApiCustomizer"); + if (knife4jProperties.isEnable()) { + Knife4jSetting setting = knife4jProperties.getSetting(); + OpenApiExtensionResolver openApiExtensionResolver = new OpenApiExtensionResolver(setting, knife4jProperties.getDocuments()); + // 解析初始化 + openApiExtensionResolver.start(); + Map objectMap = new HashMap<>(); + objectMap.put(GlobalConstants.EXTENSION_OPEN_SETTING_NAME, setting); + objectMap.put(GlobalConstants.EXTENSION_OPEN_MARKDOWN_NAME, openApiExtensionResolver.getMarkdownFiles()); + openApi.addExtension(GlobalConstants.EXTENSION_OPEN_API_NAME, objectMap); + addOrderExtension(openApi); + } + } + + /** + * 往OpenAPI内tags字段添加x-order属性 + * + * @param openApi openApi + */ + + private void addOrderExtension(OpenAPI openApi) { + if (CollectionUtils.isEmpty(properties.getGroupConfigs())) { + return; + } + // 获取包扫描路径 + Set packagesToScan = + properties.getGroupConfigs().stream() + .map(SpringDocConfigProperties.GroupConfig::getPackagesToScan) + .filter(toScan -> !CollectionUtils.isEmpty(toScan)) + .flatMap(List::stream) + .collect(Collectors.toSet()); + if (CollectionUtils.isEmpty(packagesToScan)) { + return; + } + // 扫描包下被ApiSupport注解的RestController Class + Set> classes = + packagesToScan.stream() + .map(packageToScan -> scanPackageByAnnotation(packageToScan, RestController.class)) + .flatMap(Set::stream) + .filter(clazz -> clazz.isAnnotationPresent(ApiSupport.class)) + .collect(Collectors.toSet()); + if (!CollectionUtils.isEmpty(classes)) { + // ApiSupport oder值存入tagSortMap + Map tagOrderMap = new HashMap<>(); + classes.forEach( + clazz -> { + Tag tag = getTag(clazz); + if (Objects.nonNull(tag)) { + ApiSupport apiSupport = clazz.getAnnotation(ApiSupport.class); + tagOrderMap.putIfAbsent(tag.name(), apiSupport.order()); + } + }); + // 往openApi tags字段添加x-order增强属性 + if (openApi.getTags() != null) { + openApi + .getTags() + .forEach( + tag -> { + if (tagOrderMap.containsKey(tag.getName())) { + tag.addExtension( + ExtensionsConstants.EXTENSION_ORDER, tagOrderMap.get(tag.getName())); + } + }); + } + } + } + + private Tag getTag(Class clazz) { + // 从类上获取 + Tag tag = clazz.getAnnotation(Tag.class); + if (Objects.isNull(tag)) { + // 从接口上获取 + Class[] interfaces = clazz.getInterfaces(); + if (ArrayUtils.isNotEmpty(interfaces)) { + for (Class interfaceClazz : interfaces) { + Tag anno = interfaceClazz.getAnnotation(Tag.class); + if (Objects.nonNull(anno)) { + tag = anno; + break; + } + } + } + } + return tag; + } + + private Set> scanPackageByAnnotation( + String packageName, final Class annotationClass) { + ClassPathScanningCandidateComponentProvider scanner = + new ClassPathScanningCandidateComponentProvider(false); + scanner.addIncludeFilter(new AnnotationTypeFilter(annotationClass)); + Set> classes = new HashSet<>(); + for (BeanDefinition beanDefinition : scanner.findCandidateComponents(packageName)) { + try { + Class clazz = Class.forName(beanDefinition.getBeanClassName()); + classes.add(clazz); + } catch (ClassNotFoundException ignore) { + + } + } + return classes; + } +} \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/core/handler/MyDataPermissionHandler.java b/src/main/java/com/youlai/boot/plugin/mybatis/MyDataPermissionHandler.java similarity index 97% rename from src/main/java/com/youlai/boot/core/handler/MyDataPermissionHandler.java rename to src/main/java/com/youlai/boot/plugin/mybatis/MyDataPermissionHandler.java index b9703448..f1d14579 100644 --- a/src/main/java/com/youlai/boot/core/handler/MyDataPermissionHandler.java +++ b/src/main/java/com/youlai/boot/plugin/mybatis/MyDataPermissionHandler.java @@ -1,4 +1,4 @@ -package com.youlai.boot.core.handler; +package com.youlai.boot.plugin.mybatis; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.toolkit.StringPool; @@ -6,7 +6,7 @@ import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; import com.youlai.boot.common.annotation.DataPermission; import com.youlai.boot.common.base.IBaseEnum; import com.youlai.boot.common.enums.DataScopeEnum; -import com.youlai.boot.core.security.util.SecurityUtils; +import com.youlai.boot.security.util.SecurityUtils; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import net.sf.jsqlparser.expression.Expression; diff --git a/src/main/java/com/youlai/boot/core/handler/MyMetaObjectHandler.java b/src/main/java/com/youlai/boot/plugin/mybatis/MyMetaObjectHandler.java similarity index 96% rename from src/main/java/com/youlai/boot/core/handler/MyMetaObjectHandler.java rename to src/main/java/com/youlai/boot/plugin/mybatis/MyMetaObjectHandler.java index 22973a52..76e73681 100644 --- a/src/main/java/com/youlai/boot/core/handler/MyMetaObjectHandler.java +++ b/src/main/java/com/youlai/boot/plugin/mybatis/MyMetaObjectHandler.java @@ -1,4 +1,4 @@ -package com.youlai.boot.core.handler; +package com.youlai.boot.plugin.mybatis; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; diff --git a/src/main/java/com/youlai/boot/core/security/exception/CaptchaValidationException.java b/src/main/java/com/youlai/boot/security/exception/CaptchaValidationException.java similarity index 85% rename from src/main/java/com/youlai/boot/core/security/exception/CaptchaValidationException.java rename to src/main/java/com/youlai/boot/security/exception/CaptchaValidationException.java index 7b498a1a..23b5bf8e 100644 --- a/src/main/java/com/youlai/boot/core/security/exception/CaptchaValidationException.java +++ b/src/main/java/com/youlai/boot/security/exception/CaptchaValidationException.java @@ -1,4 +1,4 @@ -package com.youlai.boot.core.security.exception; +package com.youlai.boot.security.exception; import org.springframework.security.core.AuthenticationException; diff --git a/src/main/java/com/youlai/boot/core/security/filter/CaptchaValidationFilter.java b/src/main/java/com/youlai/boot/security/filter/CaptchaValidationFilter.java similarity index 79% rename from src/main/java/com/youlai/boot/core/security/filter/CaptchaValidationFilter.java rename to src/main/java/com/youlai/boot/security/filter/CaptchaValidationFilter.java index 8589a6e0..f0a08976 100644 --- a/src/main/java/com/youlai/boot/core/security/filter/CaptchaValidationFilter.java +++ b/src/main/java/com/youlai/boot/security/filter/CaptchaValidationFilter.java @@ -1,18 +1,19 @@ -package com.youlai.boot.core.security.filter; +package com.youlai.boot.security.filter; import cn.hutool.captcha.generator.CodeGenerator; import cn.hutool.core.util.StrUtil; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.SecurityConstants; -import com.youlai.boot.common.result.ResultCode; -import com.youlai.boot.common.util.ResponseUtils; +import com.youlai.boot.core.web.ResultCode; +import com.youlai.boot.core.web.WebResponseHelper; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.HttpMethod; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @@ -26,7 +27,7 @@ import java.io.IOException; */ public class CaptchaValidationFilter extends OncePerRequestFilter { - private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, HttpMethod.POST.name()); + private static final RequestMatcher LOGIN_PATH_REQUEST_MATCHER = PathPatternRequestMatcher.withDefaults().matcher(HttpMethod.POST,SecurityConstants.LOGIN_PATH); public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode"; public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey"; @@ -58,13 +59,13 @@ public class CaptchaValidationFilter extends OncePerRequestFilter { StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, verifyCodeKey) ); if (cacheVerifyCode == null) { - ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); + WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_EXPIRED); } else { // 验证码比对 if (codeGenerator.verify(cacheVerifyCode, captchaCode)) { chain.doFilter(request, response); } else { - ResponseUtils.writeErrMsg(response, ResultCode.USER_VERIFICATION_CODE_ERROR); + WebResponseHelper.writeError(response, ResultCode.USER_VERIFICATION_CODE_ERROR); } } } else { diff --git a/src/main/java/com/youlai/boot/core/security/filter/TokenAuthenticationFilter.java b/src/main/java/com/youlai/boot/security/filter/TokenAuthenticationFilter.java similarity index 86% rename from src/main/java/com/youlai/boot/core/security/filter/TokenAuthenticationFilter.java rename to src/main/java/com/youlai/boot/security/filter/TokenAuthenticationFilter.java index b71e86ce..91712520 100644 --- a/src/main/java/com/youlai/boot/core/security/filter/TokenAuthenticationFilter.java +++ b/src/main/java/com/youlai/boot/security/filter/TokenAuthenticationFilter.java @@ -1,10 +1,10 @@ -package com.youlai.boot.core.security.filter; +package com.youlai.boot.security.filter; import cn.hutool.core.util.StrUtil; import com.youlai.boot.common.constant.SecurityConstants; -import com.youlai.boot.common.result.ResultCode; -import com.youlai.boot.common.util.ResponseUtils; -import com.youlai.boot.core.security.token.TokenManager; +import com.youlai.boot.core.web.ResultCode; +import com.youlai.boot.core.web.WebResponseHelper; +import com.youlai.boot.security.token.TokenManager; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -52,7 +52,7 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { // 执行令牌有效性检查(包含密码学验签和过期时间验证) boolean isValidToken = tokenManager.validateToken(rawToken); if (!isValidToken) { - ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_TOKEN_INVALID); + WebResponseHelper.writeError(response, ResultCode.ACCESS_TOKEN_INVALID); return; } @@ -63,7 +63,7 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { } catch (Exception ex) { // 安全上下文清除保障(防止上下文残留) SecurityContextHolder.clearContext(); - ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_TOKEN_INVALID); + WebResponseHelper.writeError(response, ResultCode.ACCESS_TOKEN_INVALID); return; } diff --git a/src/main/java/com/youlai/boot/core/security/exception/MyAccessDeniedHandler.java b/src/main/java/com/youlai/boot/security/handler/MyAccessDeniedHandler.java similarity index 72% rename from src/main/java/com/youlai/boot/core/security/exception/MyAccessDeniedHandler.java rename to src/main/java/com/youlai/boot/security/handler/MyAccessDeniedHandler.java index 5efbfbff..e1e2a861 100644 --- a/src/main/java/com/youlai/boot/core/security/exception/MyAccessDeniedHandler.java +++ b/src/main/java/com/youlai/boot/security/handler/MyAccessDeniedHandler.java @@ -1,7 +1,7 @@ -package com.youlai.boot.core.security.exception; +package com.youlai.boot.security.handler; -import com.youlai.boot.common.result.ResultCode; -import com.youlai.boot.common.util.ResponseUtils; +import com.youlai.boot.core.web.ResultCode; +import com.youlai.boot.core.web.WebResponseHelper; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; @@ -20,7 +20,7 @@ public class MyAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) { - ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_UNAUTHORIZED); + WebResponseHelper.writeError(response, ResultCode.ACCESS_UNAUTHORIZED); } } diff --git a/src/main/java/com/youlai/boot/core/security/exception/MyAuthenticationEntryPoint.java b/src/main/java/com/youlai/boot/security/handler/MyAuthenticationEntryPoint.java similarity index 78% rename from src/main/java/com/youlai/boot/core/security/exception/MyAuthenticationEntryPoint.java rename to src/main/java/com/youlai/boot/security/handler/MyAuthenticationEntryPoint.java index ff5f041e..09d0780c 100644 --- a/src/main/java/com/youlai/boot/core/security/exception/MyAuthenticationEntryPoint.java +++ b/src/main/java/com/youlai/boot/security/handler/MyAuthenticationEntryPoint.java @@ -1,7 +1,7 @@ -package com.youlai.boot.core.security.exception; +package com.youlai.boot.security.handler; -import com.youlai.boot.common.result.ResultCode; -import com.youlai.boot.common.util.ResponseUtils; +import com.youlai.boot.core.web.ResultCode; +import com.youlai.boot.core.web.WebResponseHelper; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.AuthenticationException; @@ -32,13 +32,13 @@ public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint { public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { if (authException instanceof BadCredentialsException) { // 用户名或密码错误 - ResponseUtils.writeErrMsg(response, ResultCode.USER_PASSWORD_ERROR); + WebResponseHelper.writeError(response, ResultCode.USER_PASSWORD_ERROR); } else if(authException instanceof InsufficientAuthenticationException){ // 请求头缺失Authorization、Token格式错误、Token过期、签名验证失败 - ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_TOKEN_INVALID); + WebResponseHelper.writeError(response, ResultCode.ACCESS_TOKEN_INVALID); } else { // 其他未明确处理的认证异常(如账户被锁定、账户禁用等) - ResponseUtils.writeErrMsg(response, ResultCode.USER_LOGIN_EXCEPTION, authException.getMessage()); + WebResponseHelper.writeError(response, ResultCode.USER_LOGIN_EXCEPTION, authException.getMessage()); } } } diff --git a/src/main/java/com/youlai/boot/core/security/model/AuthenticationToken.java b/src/main/java/com/youlai/boot/security/model/AuthenticationToken.java similarity index 93% rename from src/main/java/com/youlai/boot/core/security/model/AuthenticationToken.java rename to src/main/java/com/youlai/boot/security/model/AuthenticationToken.java index aaa352dd..43ca5cb2 100644 --- a/src/main/java/com/youlai/boot/core/security/model/AuthenticationToken.java +++ b/src/main/java/com/youlai/boot/security/model/AuthenticationToken.java @@ -1,4 +1,4 @@ -package com.youlai.boot.core.security.model; +package com.youlai.boot.security.model; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; diff --git a/src/main/java/com/youlai/boot/core/security/model/OnlineUser.java b/src/main/java/com/youlai/boot/security/model/OnlineUser.java similarity index 93% rename from src/main/java/com/youlai/boot/core/security/model/OnlineUser.java rename to src/main/java/com/youlai/boot/security/model/OnlineUser.java index 095a2912..6dd72608 100644 --- a/src/main/java/com/youlai/boot/core/security/model/OnlineUser.java +++ b/src/main/java/com/youlai/boot/security/model/OnlineUser.java @@ -1,4 +1,4 @@ -package com.youlai.boot.core.security.model; +package com.youlai.boot.security.model; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/com/youlai/boot/core/security/extension/sms/SmsAuthenticationToken.java b/src/main/java/com/youlai/boot/security/model/SmsAuthenticationToken.java similarity index 97% rename from src/main/java/com/youlai/boot/core/security/extension/sms/SmsAuthenticationToken.java rename to src/main/java/com/youlai/boot/security/model/SmsAuthenticationToken.java index 54cb026c..d2aec807 100644 --- a/src/main/java/com/youlai/boot/core/security/extension/sms/SmsAuthenticationToken.java +++ b/src/main/java/com/youlai/boot/security/model/SmsAuthenticationToken.java @@ -1,4 +1,4 @@ -package com.youlai.boot.core.security.extension.sms; +package com.youlai.boot.security.model; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; diff --git a/src/main/java/com/youlai/boot/core/security/model/SysUserDetails.java b/src/main/java/com/youlai/boot/security/model/SysUserDetails.java similarity index 98% rename from src/main/java/com/youlai/boot/core/security/model/SysUserDetails.java rename to src/main/java/com/youlai/boot/security/model/SysUserDetails.java index 4037bc8c..8a79831c 100644 --- a/src/main/java/com/youlai/boot/core/security/model/SysUserDetails.java +++ b/src/main/java/com/youlai/boot/security/model/SysUserDetails.java @@ -1,4 +1,4 @@ -package com.youlai.boot.core.security.model; +package com.youlai.boot.security.model; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.ObjectUtil; diff --git a/src/main/java/com/youlai/boot/core/security/model/UserAuthCredentials.java b/src/main/java/com/youlai/boot/security/model/UserAuthCredentials.java similarity index 94% rename from src/main/java/com/youlai/boot/core/security/model/UserAuthCredentials.java rename to src/main/java/com/youlai/boot/security/model/UserAuthCredentials.java index aa064ee2..e68d119d 100644 --- a/src/main/java/com/youlai/boot/core/security/model/UserAuthCredentials.java +++ b/src/main/java/com/youlai/boot/security/model/UserAuthCredentials.java @@ -1,4 +1,4 @@ -package com.youlai.boot.core.security.model; +package com.youlai.boot.security.model; import lombok.Data; import java.util.Set; diff --git a/src/main/java/com/youlai/boot/core/security/extension/wechat/WechatAuthenticationToken.java b/src/main/java/com/youlai/boot/security/model/WxMiniAppCodeAuthenticationToken.java similarity index 58% rename from src/main/java/com/youlai/boot/core/security/extension/wechat/WechatAuthenticationToken.java rename to src/main/java/com/youlai/boot/security/model/WxMiniAppCodeAuthenticationToken.java index fc8bb2be..675e9db6 100644 --- a/src/main/java/com/youlai/boot/core/security/extension/wechat/WechatAuthenticationToken.java +++ b/src/main/java/com/youlai/boot/security/model/WxMiniAppCodeAuthenticationToken.java @@ -1,4 +1,4 @@ -package com.youlai.boot.core.security.extension.wechat; +package com.youlai.boot.security.model; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; @@ -7,22 +7,22 @@ import java.io.Serial; import java.util.Collection; /** - * 微信认证 Token + * 微信小程序Code认证Token * - * @author Ray.Hao - * @since 2024/12/2 + * @author 有来技术团队 + * @since 2.0.0 */ -public class WechatAuthenticationToken extends AbstractAuthenticationToken { +public class WxMiniAppCodeAuthenticationToken extends AbstractAuthenticationToken { @Serial private static final long serialVersionUID = 621L; private final Object principal; /** - * 微信认证 Token (未认证) + * 微信小程序Code认证Token (未认证) * - * @param principal 微信用户信息 + * @param principal 微信code */ - public WechatAuthenticationToken(Object principal) { + public WxMiniAppCodeAuthenticationToken(Object principal) { // 没有授权信息时,设置为 null super(null); this.principal = principal; @@ -32,12 +32,12 @@ public class WechatAuthenticationToken extends AbstractAuthenticationToken { /** - * 微信认证 Token (已认证) + * 微信小程序Code认证Token (已认证) * * @param principal 微信用户信息 * @param authorities 授权信息 */ - public WechatAuthenticationToken(Object principal, Collection authorities) { + public WxMiniAppCodeAuthenticationToken(Object principal, Collection authorities) { super(authorities); this.principal = principal; // 认证通过 @@ -50,10 +50,10 @@ public class WechatAuthenticationToken extends AbstractAuthenticationToken { * * @param principal 微信用户信息 * @param authorities 授权信息 - * @return + * @return 已认证的Token */ - public static WechatAuthenticationToken authenticated(Object principal, Collection authorities) { - return new WechatAuthenticationToken(principal, authorities); + public static WxMiniAppCodeAuthenticationToken authenticated(Object principal, Collection authorities) { + return new WxMiniAppCodeAuthenticationToken(principal, authorities); } @Override @@ -66,4 +66,4 @@ public class WechatAuthenticationToken extends AbstractAuthenticationToken { public Object getPrincipal() { return this.principal; } -} +} \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/security/model/WxMiniAppPhoneAuthenticationToken.java b/src/main/java/com/youlai/boot/security/model/WxMiniAppPhoneAuthenticationToken.java new file mode 100644 index 00000000..697e8941 --- /dev/null +++ b/src/main/java/com/youlai/boot/security/model/WxMiniAppPhoneAuthenticationToken.java @@ -0,0 +1,89 @@ +package com.youlai.boot.security.model; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.io.Serial; +import java.util.Collection; + +/** + * 微信小程序手机号认证Token + * + * @author 有来技术团队 + * @since 2.0.0 + */ +public class WxMiniAppPhoneAuthenticationToken extends AbstractAuthenticationToken { + @Serial + private static final long serialVersionUID = 622L; + + private final Object principal; // code + private String encryptedData; + private String iv; + + /** + * 微信小程序手机号认证Token (未认证) + * + * @param code 微信登录code + * @param encryptedData 加密数据 + * @param iv 初始向量 + */ + public WxMiniAppPhoneAuthenticationToken(String code, String encryptedData, String iv) { + super(null); + this.principal = code; + this.encryptedData = encryptedData; + this.iv = iv; + this.setAuthenticated(false); + } + + /** + * 微信小程序手机号认证Token (已认证) + * + * @param principal 用户信息 + * @param authorities 授权信息 + */ + public WxMiniAppPhoneAuthenticationToken(Object principal, Collection authorities) { + super(authorities); + this.principal = principal; + super.setAuthenticated(true); + } + + /** + * 认证通过 + * + * @param principal 用户信息 + * @param authorities 授权信息 + * @return 认证通过的Token + */ + public static WxMiniAppPhoneAuthenticationToken authenticated(Object principal, Collection authorities) { + return new WxMiniAppPhoneAuthenticationToken(principal, authorities); + } + + @Override + public Object getCredentials() { + // 微信小程序手机号认证不需要密码 + return null; + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + /** + * 获取加密数据 + * + * @return 加密数据 + */ + public String getEncryptedData() { + return encryptedData; + } + + /** + * 获取初始向量 + * + * @return 初始向量 + */ + public String getIv() { + return iv; + } +} diff --git a/src/main/java/com/youlai/boot/core/security/extension/sms/SmsAuthenticationProvider.java b/src/main/java/com/youlai/boot/security/provider/SmsAuthenticationProvider.java similarity index 91% rename from src/main/java/com/youlai/boot/core/security/extension/sms/SmsAuthenticationProvider.java rename to src/main/java/com/youlai/boot/security/provider/SmsAuthenticationProvider.java index 74c98cda..681f31e8 100644 --- a/src/main/java/com/youlai/boot/core/security/extension/sms/SmsAuthenticationProvider.java +++ b/src/main/java/com/youlai/boot/security/provider/SmsAuthenticationProvider.java @@ -1,11 +1,12 @@ -package com.youlai.boot.core.security.extension.sms; +package com.youlai.boot.security.provider; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import com.youlai.boot.common.constant.RedisConstants; -import com.youlai.boot.core.security.exception.CaptchaValidationException; -import com.youlai.boot.core.security.model.SysUserDetails; -import com.youlai.boot.core.security.model.UserAuthCredentials; +import com.youlai.boot.security.exception.CaptchaValidationException; +import com.youlai.boot.security.model.SmsAuthenticationToken; +import com.youlai.boot.security.model.SysUserDetails; +import com.youlai.boot.security.model.UserAuthCredentials; import com.youlai.boot.system.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; diff --git a/src/main/java/com/youlai/boot/core/security/extension/wechat/WechatAuthenticationProvider.java b/src/main/java/com/youlai/boot/security/provider/WxMiniAppCodeAuthenticationProvider.java similarity index 78% rename from src/main/java/com/youlai/boot/core/security/extension/wechat/WechatAuthenticationProvider.java rename to src/main/java/com/youlai/boot/security/provider/WxMiniAppCodeAuthenticationProvider.java index 4c8b1c61..3214df81 100644 --- a/src/main/java/com/youlai/boot/core/security/extension/wechat/WechatAuthenticationProvider.java +++ b/src/main/java/com/youlai/boot/security/provider/WxMiniAppCodeAuthenticationProvider.java @@ -1,11 +1,12 @@ -package com.youlai.boot.core.security.extension.wechat; +package com.youlai.boot.security.provider; import cn.binarywang.wx.miniapp.api.WxMaService; import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; -import com.youlai.boot.core.security.model.SysUserDetails; -import com.youlai.boot.core.security.model.UserAuthCredentials; +import com.youlai.boot.security.model.SysUserDetails; +import com.youlai.boot.security.model.UserAuthCredentials; +import com.youlai.boot.security.model.WxMiniAppCodeAuthenticationToken; import com.youlai.boot.system.service.UserService; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.error.WxErrorException; @@ -18,20 +19,19 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; /** - * 微信认证 Provider + * 微信小程序Code认证Provider * - * @author Ray.Hao - * @since 2.17.0 + * @author 有来技术团队 + * @since 2.0.0 */ @Slf4j -public class WechatAuthenticationProvider implements AuthenticationProvider { +public class WxMiniAppCodeAuthenticationProvider implements AuthenticationProvider { private final UserService userService; - private final WxMaService wxMaService; - public WechatAuthenticationProvider(UserService userService, WxMaService wxMaService) { + public WxMiniAppCodeAuthenticationProvider(UserService userService, WxMaService wxMaService) { this.userService = userService; this.wxMaService = wxMaService; } @@ -66,7 +66,7 @@ public class WechatAuthenticationProvider implements AuthenticationProvider { UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByOpenId(openId); if (userAuthCredentials == null) { - // TODO: 用户不存在则注册,这里需要获取用户手机号并与现有用户绑定 + // 用户不存在则注册 userService.registerOrBindWechatUser(openId); // 再次查询用户信息,确保用户注册成功 @@ -80,13 +80,12 @@ public class WechatAuthenticationProvider implements AuthenticationProvider { if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) { throw new DisabledException("用户已被禁用"); } - // 这里因为已经根据 code 从微信小程序获取到 openid 不需要再经过系统认证,所以直接生成 // 构建认证后的用户详情信息 SysUserDetails userDetails = new SysUserDetails(userAuthCredentials); - // 创建已认证的 WeChatAuthenticationToken - return WechatAuthenticationToken.authenticated( + // 创建已认证的Token + return WxMiniAppCodeAuthenticationToken.authenticated( userDetails, userDetails.getAuthorities() ); @@ -94,6 +93,6 @@ public class WechatAuthenticationProvider implements AuthenticationProvider { @Override public boolean supports(Class authentication) { - return WechatAuthenticationToken.class.isAssignableFrom(authentication); + return WxMiniAppCodeAuthenticationToken.class.isAssignableFrom(authentication); } -} +} \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/security/provider/WxMiniAppPhoneAuthenticationProvider.java b/src/main/java/com/youlai/boot/security/provider/WxMiniAppPhoneAuthenticationProvider.java new file mode 100644 index 00000000..2bcc933a --- /dev/null +++ b/src/main/java/com/youlai/boot/security/provider/WxMiniAppPhoneAuthenticationProvider.java @@ -0,0 +1,115 @@ +package com.youlai.boot.security.provider; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; +import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.youlai.boot.security.model.SysUserDetails; +import com.youlai.boot.security.model.UserAuthCredentials; +import com.youlai.boot.security.model.WxMiniAppPhoneAuthenticationToken; +import com.youlai.boot.system.service.UserService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.CredentialsExpiredException; +import org.springframework.security.authentication.DisabledException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +/** + * 微信小程序手机号认证Provider + * + * @author 有来技术团队 + * @since 2.0.0 + */ +@Slf4j +public class WxMiniAppPhoneAuthenticationProvider implements AuthenticationProvider { + + private final UserService userService; + private final WxMaService wxMaService; + + public WxMiniAppPhoneAuthenticationProvider(UserService userService, WxMaService wxMaService) { + this.userService = userService; + this.wxMaService = wxMaService; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + WxMiniAppPhoneAuthenticationToken authenticationToken = (WxMiniAppPhoneAuthenticationToken) authentication; + String code = (String) authenticationToken.getPrincipal(); + String encryptedData = authenticationToken.getEncryptedData(); + String iv = authenticationToken.getIv(); + + // 1. 通过code获取session_key + WxMaJscode2SessionResult sessionInfo; + try { + sessionInfo = wxMaService.getUserService().getSessionInfo(code); + } catch (WxErrorException e) { + log.error("获取微信session_key失败", e); + throw new CredentialsExpiredException("微信登录code无效或已过期"); + } + + String sessionKey = sessionInfo.getSessionKey(); + String openId = sessionInfo.getOpenid(); + + if (StrUtil.isBlank(sessionKey) || StrUtil.isBlank(openId)) { + throw new CredentialsExpiredException("获取微信会话信息失败"); + } + + // 2. 解密手机号信息 + WxMaPhoneNumberInfo phoneNumberInfo; + try { + if (StrUtil.isNotBlank(encryptedData) && StrUtil.isNotBlank(iv)) { + phoneNumberInfo = wxMaService.getUserService().getPhoneNoInfo(sessionKey, encryptedData, iv); + } else { + throw new IllegalArgumentException("缺少手机号加密数据"); + } + } catch (Exception e) { + log.error("解密微信手机号失败", e); + throw new CredentialsExpiredException("解密手机号信息失败"); + } + + if (phoneNumberInfo == null || StrUtil.isBlank(phoneNumberInfo.getPhoneNumber())) { + throw new CredentialsExpiredException("获取手机号失败"); + } + + String phoneNumber = phoneNumberInfo.getPhoneNumber(); + + // 3. 根据手机号查询用户,不存在则创建新用户 + UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByMobile(phoneNumber); + + if (userAuthCredentials == null) { + // 用户不存在,注册新用户 + boolean registered = userService.registerUserByMobileAndOpenId(phoneNumber, openId); + if (!registered) { + throw new UsernameNotFoundException("用户注册失败"); + } + // 重新获取用户信息 + userAuthCredentials = userService.getAuthCredentialsByMobile(phoneNumber); + } else { + // 用户存在,绑定openId(如果未绑定) + userService.bindUserOpenId(userAuthCredentials.getUserId(), openId); + } + + // 4. 检查用户状态 + if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) { + throw new DisabledException("用户已被禁用"); + } + + // 5. 构建认证后的用户详情 + SysUserDetails userDetails = new SysUserDetails(userAuthCredentials); + + // 6. 创建已认证的Token + return WxMiniAppPhoneAuthenticationToken.authenticated( + userDetails, + userDetails.getAuthorities() + ); + } + + @Override + public boolean supports(Class authentication) { + return WxMiniAppPhoneAuthenticationToken.class.isAssignableFrom(authentication); + } +} \ No newline at end of file diff --git a/src/main/java/com/youlai/boot/core/security/service/PermissionService.java b/src/main/java/com/youlai/boot/security/service/PermissionService.java similarity index 96% rename from src/main/java/com/youlai/boot/core/security/service/PermissionService.java rename to src/main/java/com/youlai/boot/security/service/PermissionService.java index 0f7448c9..8b11ad4e 100644 --- a/src/main/java/com/youlai/boot/core/security/service/PermissionService.java +++ b/src/main/java/com/youlai/boot/security/service/PermissionService.java @@ -1,9 +1,9 @@ -package com.youlai.boot.core.security.service; +package com.youlai.boot.security.service; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; import com.youlai.boot.common.constant.RedisConstants; -import com.youlai.boot.core.security.util.SecurityUtils; +import com.youlai.boot.security.util.SecurityUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; diff --git a/src/main/java/com/youlai/boot/core/security/service/SysUserDetailsService.java b/src/main/java/com/youlai/boot/security/service/SysUserDetailsService.java similarity index 89% rename from src/main/java/com/youlai/boot/core/security/service/SysUserDetailsService.java rename to src/main/java/com/youlai/boot/security/service/SysUserDetailsService.java index 86b70db8..213b698f 100644 --- a/src/main/java/com/youlai/boot/core/security/service/SysUserDetailsService.java +++ b/src/main/java/com/youlai/boot/security/service/SysUserDetailsService.java @@ -1,7 +1,7 @@ -package com.youlai.boot.core.security.service; +package com.youlai.boot.security.service; -import com.youlai.boot.core.security.model.SysUserDetails; -import com.youlai.boot.core.security.model.UserAuthCredentials; +import com.youlai.boot.security.model.SysUserDetails; +import com.youlai.boot.security.model.UserAuthCredentials; import com.youlai.boot.system.service.UserService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/youlai/boot/core/security/token/JwtTokenManager.java b/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java similarity index 73% rename from src/main/java/com/youlai/boot/core/security/token/JwtTokenManager.java rename to src/main/java/com/youlai/boot/security/token/JwtTokenManager.java index 9df71182..627a533d 100644 --- a/src/main/java/com/youlai/boot/core/security/token/JwtTokenManager.java +++ b/src/main/java/com/youlai/boot/security/token/JwtTokenManager.java @@ -1,4 +1,4 @@ -package com.youlai.boot.core.security.token; +package com.youlai.boot.security.token; import cn.hutool.core.convert.Convert; import cn.hutool.core.date.DateUtil; @@ -11,11 +11,12 @@ import cn.hutool.jwt.JWTUtil; import com.youlai.boot.common.constant.JwtClaimConstants; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.SecurityConstants; -import com.youlai.boot.common.exception.BusinessException; -import com.youlai.boot.common.result.ResultCode; +import com.youlai.boot.core.exception.BusinessException; +import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.config.property.SecurityProperties; -import com.youlai.boot.core.security.model.SysUserDetails; -import com.youlai.boot.core.security.model.AuthenticationToken; +import com.youlai.boot.security.model.AuthenticationToken; +import org.apache.commons.lang3.StringUtils; +import com.youlai.boot.security.model.SysUserDetails; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -65,7 +66,7 @@ public class JwtTokenManager implements TokenManager { int refreshTokenTimeToLive = securityProperties.getSession().getRefreshTokenTimeToLive(); String accessToken = generateToken(authentication, accessTokenTimeToLive); - String refreshToken = generateToken(authentication, refreshTokenTimeToLive); + String refreshToken = generateToken(authentication, refreshTokenTimeToLive, true); return AuthenticationToken.builder() .accessToken(accessToken) @@ -109,26 +110,54 @@ public class JwtTokenManager implements TokenManager { */ @Override public boolean validateToken(String token) { - JWT jwt = JWTUtil.parseToken(token); - // 检查 Token 是否有效(验签 + 是否过期) - boolean isValid = jwt.setKey(secretKey).validate(0); - - if (isValid) { - // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) - JSONObject payloads = jwt.getPayloads(); - String jti = payloads.getStr(JWTPayload.JWT_ID); - - // 判断是否在黑名单中,如果在,则返回 false 标识Token无效 - if (Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, jti)))) { - return false; - } - } - return isValid; + return validateToken(token,false); } + /** + * 校验刷新令牌 + * + * @param refreshToken JWT Token + * @return 验证结果 + */ @Override public boolean validateRefreshToken(String refreshToken) { - return this.validateToken(refreshToken); + return validateToken(refreshToken,true); + } + + /** + * 校验令牌 + * + * @param token JWT Token + * @param validateRefreshToken 是否校验刷新令牌 + * @return 是否有效 + */ + private boolean validateToken(String token, boolean validateRefreshToken) { + try { + JWT jwt = JWTUtil.parseToken(token); + // 检查 Token 是否有效(验签 + 是否过期) + boolean isValid = jwt.setKey(secretKey).validate(0); + + if (isValid) { + // 检查 Token 是否已被加入黑名单(注销、修改密码等场景) + JSONObject payloads = jwt.getPayloads(); + String jti = payloads.getStr(JWTPayload.JWT_ID); + if(validateRefreshToken) { + //刷新token需要校验token类别 + boolean isRefreshToken = payloads.getBool(JwtClaimConstants.TOKEN_TYPE); + if (!isRefreshToken) { + return false; + } + } + // 判断是否在黑名单中,如果在,则返回 false 标识Token无效 + if (Boolean.TRUE.equals(redisTemplate.hasKey(StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, jti)))) { + return false; + } + } + return isValid; + } catch (Exception gitignore) { + // token 验证 + } + return false; } /** @@ -138,15 +167,16 @@ public class JwtTokenManager implements TokenManager { */ @Override public void invalidateToken(String token) { + if(StringUtils.isBlank(token)) { + return; + } + if (token.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { token = token.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); } - JWT jwt = JWTUtil.parseToken(token); JSONObject payloads = jwt.getPayloads(); - Integer expirationAt = payloads.getInt(JWTPayload.EXPIRES_AT); - // 黑名单Token Key String blacklistTokenKey = StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, payloads.getStr(JWTPayload.JWT_ID)); @@ -174,16 +204,13 @@ public class JwtTokenManager implements TokenManager { */ @Override public AuthenticationToken refreshToken(String refreshToken) { - - boolean isValid = validateToken(refreshToken); + boolean isValid = validateRefreshToken(refreshToken); if (!isValid) { throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID); } - Authentication authentication = parseToken(refreshToken); int accessTokenExpiration = securityProperties.getSession().getAccessTokenTimeToLive(); String newAccessToken = generateToken(authentication, accessTokenExpiration); - return AuthenticationToken.builder() .accessToken(newAccessToken) .refreshToken(refreshToken) @@ -196,13 +223,24 @@ public class JwtTokenManager implements TokenManager { * 生成 JWT Token * * @param authentication 认证信息 - * @param ttl 过期时间 + * @param ttl 过期时间 * @return JWT Token */ private String generateToken(Authentication authentication, int ttl) { + return generateToken(authentication, ttl, false); + } + + /** + * 生成 JWT Token + * + * @param authentication 认证信息 + * @param ttl 过期时间 + * @param isRefreshToken 类型是否为刷新token + * @return JWT Token + */ + private String generateToken(Authentication authentication, int ttl, boolean isRefreshToken) { SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); - Map payload = new HashMap<>(); payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID @@ -216,6 +254,10 @@ public class JwtTokenManager implements TokenManager { Date now = new Date(); payload.put(JWTPayload.ISSUED_AT, now); + payload.put(JwtClaimConstants.TOKEN_TYPE, false); + if (isRefreshToken) { + payload.put(JwtClaimConstants.TOKEN_TYPE, true); + } // 设置过期时间 -1 表示永不过期 if (ttl != -1) { @@ -227,4 +269,5 @@ public class JwtTokenManager implements TokenManager { return JWTUtil.createToken(payload, secretKey); } + } diff --git a/src/main/java/com/youlai/boot/core/security/token/RedisTokenManager.java b/src/main/java/com/youlai/boot/security/token/RedisTokenManager.java similarity index 93% rename from src/main/java/com/youlai/boot/core/security/token/RedisTokenManager.java rename to src/main/java/com/youlai/boot/security/token/RedisTokenManager.java index dcb871f3..a2dff3a5 100644 --- a/src/main/java/com/youlai/boot/core/security/token/RedisTokenManager.java +++ b/src/main/java/com/youlai/boot/security/token/RedisTokenManager.java @@ -1,15 +1,15 @@ -package com.youlai.boot.core.security.token; +package com.youlai.boot.security.token; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.IdUtil; import cn.hutool.core.util.StrUtil; import com.youlai.boot.common.constant.RedisConstants; -import com.youlai.boot.common.exception.BusinessException; -import com.youlai.boot.common.result.ResultCode; +import com.youlai.boot.core.exception.BusinessException; +import com.youlai.boot.core.web.ResultCode; import com.youlai.boot.config.property.SecurityProperties; -import com.youlai.boot.core.security.model.AuthenticationToken; -import com.youlai.boot.core.security.model.OnlineUser; -import com.youlai.boot.core.security.model.SysUserDetails; +import com.youlai.boot.security.model.AuthenticationToken; +import com.youlai.boot.security.model.OnlineUser; +import com.youlai.boot.security.model.SysUserDetails; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -25,7 +25,7 @@ import java.util.stream.Collectors; /** * Redis Token 管理器 *

- * 用于生成、解析、校验、刷新 JWT Token + * 用于生成、解析、校验、刷新 Redis Token * * @author Ray.Hao * @since 2024/11/15 @@ -81,7 +81,7 @@ public class RedisTokenManager implements TokenManager { /** * 根据 token 解析用户信息 * - * @param token JWT Token + * @param token Redis Token * @return 构建的 Authentication 对象 */ @Override @@ -107,7 +107,7 @@ public class RedisTokenManager implements TokenManager { /** * 校验 Token 是否有效 * - * @param token 访问令牌 + * @param token 访问令牌 * @return 是否有效 */ @Override @@ -118,7 +118,7 @@ public class RedisTokenManager implements TokenManager { /** * 校验 RefreshToken 是否有效 * - * @param refreshToken 访问令牌 + * @param refreshToken 访问令牌 * @return 是否有效 */ @Override @@ -189,9 +189,9 @@ public class RedisTokenManager implements TokenManager { /** * 将访问令牌和刷新令牌存储至 Redis * - * @param accessToken 访问令牌 + * @param accessToken 访问令牌 * @param refreshToken 刷新令牌 - * @param onlineUser 在线用户信息 + * @param onlineUser 在线用户信息 */ private void storeTokensInRedis(String accessToken, String refreshToken, OnlineUser onlineUser) { // 访问令牌 -> 用户信息 @@ -210,7 +210,7 @@ public class RedisTokenManager implements TokenManager { /** * 处理单设备登录控制 * - * @param userId 用户ID + * @param userId 用户ID * @param accessToken 新生成的访问令牌 */ private void handleSingleDeviceLogin(Long userId, String accessToken) { @@ -231,7 +231,7 @@ public class RedisTokenManager implements TokenManager { * 存储新的访问令牌 * * @param newAccessToken 新访问令牌 - * @param onlineUser 在线用户信息 + * @param onlineUser 在线用户信息 */ private void storeAccessToken(String newAccessToken, OnlineUser onlineUser) { setRedisValue(StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, newAccessToken), onlineUser, securityProperties.getSession().getAccessTokenTimeToLive()); @@ -242,7 +242,7 @@ public class RedisTokenManager implements TokenManager { /** * 构建用户详情对象 * - * @param onlineUser 在线用户信息 + * @param onlineUser 在线用户信息 * @param authorities 权限集合 * @return SysUserDetails 用户详情 */ diff --git a/src/main/java/com/youlai/boot/core/security/token/TokenManager.java b/src/main/java/com/youlai/boot/security/token/TokenManager.java similarity index 90% rename from src/main/java/com/youlai/boot/core/security/token/TokenManager.java rename to src/main/java/com/youlai/boot/security/token/TokenManager.java index bc722d2e..3c051dc4 100644 --- a/src/main/java/com/youlai/boot/core/security/token/TokenManager.java +++ b/src/main/java/com/youlai/boot/security/token/TokenManager.java @@ -1,7 +1,7 @@ -package com.youlai.boot.core.security.token; +package com.youlai.boot.security.token; -import com.youlai.boot.core.security.model.AuthenticationToken; +import com.youlai.boot.security.model.AuthenticationToken; import org.springframework.security.core.Authentication; /** @@ -57,7 +57,7 @@ public interface TokenManager { /** * 令 Token 失效 * - * @param token JWT Token + * @param token Token */ default void invalidateToken(String token) { // 默认实现可以是空的,或者抛出不支持的操作异常 diff --git a/src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java b/src/main/java/com/youlai/boot/security/util/SecurityUtils.java similarity index 88% rename from src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java rename to src/main/java/com/youlai/boot/security/util/SecurityUtils.java index 12e41e18..48372e53 100644 --- a/src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java +++ b/src/main/java/com/youlai/boot/security/util/SecurityUtils.java @@ -1,10 +1,10 @@ -package com.youlai.boot.core.security.util; +package com.youlai.boot.security.util; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.util.StrUtil; import com.youlai.boot.common.constant.SecurityConstants; import com.youlai.boot.common.constant.SystemConstants; -import com.youlai.boot.core.security.model.SysUserDetails; +import com.youlai.boot.security.model.SysUserDetails; import jakarta.servlet.http.HttpServletRequest; import org.springframework.http.HttpHeaders; import org.springframework.security.core.Authentication; @@ -13,10 +13,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; /** @@ -117,7 +114,11 @@ public class SecurityUtils { * @return Token 字符串 */ public static String getTokenFromRequest() { - HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + ServletRequestAttributes servletRequestAttributes = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()); + if(Objects.isNull(servletRequestAttributes)) { + return null; + } + HttpServletRequest request = servletRequestAttributes.getRequest(); return request.getHeader(HttpHeaders.AUTHORIZATION); } diff --git a/src/main/java/com/youlai/boot/shared/websocket/listener/OnlineUserListener.java b/src/main/java/com/youlai/boot/shared/websocket/listener/OnlineUserListener.java deleted file mode 100644 index 54fd3481..00000000 --- a/src/main/java/com/youlai/boot/shared/websocket/listener/OnlineUserListener.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.youlai.boot.shared.websocket.listener; - -import com.youlai.boot.shared.websocket.service.OnlineUserService; -import com.youlai.boot.system.event.UserConnectionEvent; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.context.event.EventListener; -import org.springframework.messaging.simp.SimpMessagingTemplate; -import org.springframework.stereotype.Component; - -/** - * 在线用户监听器 - * - * @author haoxr - * @since 2024/9/25 - */ -@Component -@RequiredArgsConstructor -@Slf4j -public class OnlineUserListener { - - private final SimpMessagingTemplate messagingTemplate; - private final OnlineUserService onlineUserService; - - /** - * 用户连接事件处理 - * - * @param event 用户连接事件 - */ - @EventListener - public void handleUserConnectionEvent(UserConnectionEvent event) { - String username = event.getUsername(); - if (event.isConnected()) { - onlineUserService.addOnlineUser(username); - log.info("User connected: {}", username); - } else { - onlineUserService.removeOnlineUser(username); - log.info("User disconnected: {}", username); - } - // 推送在线用户人数 - messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); - } - -} diff --git a/src/main/java/com/youlai/boot/shared/websocket/service/OnlineUserService.java b/src/main/java/com/youlai/boot/shared/websocket/service/OnlineUserService.java deleted file mode 100644 index 6ef06a57..00000000 --- a/src/main/java/com/youlai/boot/shared/websocket/service/OnlineUserService.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.youlai.boot.shared.websocket.service; - -import org.springframework.stereotype.Service; - -import java.util.Collections; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; - -/** - * 在线用户服务 - * - * @author haoxr - * @since 2024/9/26 - */ - -@Service -public class OnlineUserService { - - private final Set onlineUsers = ConcurrentHashMap.newKeySet(); - - /** - * 添加用户到在线用户集合 - * - * @param username 用户名 - */ - public void addOnlineUser(String username) { - onlineUsers.add(username); - } - - /** - * 从在线用户集合移除用户 - * - * @param username 用户名 - */ - public void removeOnlineUser(String username) { - onlineUsers.remove(username); - } - - /** - * 获取所有在线用户 - * - * @return 在线用户集合 - */ - public Set getAllOnlineUsers() { - return Collections.unmodifiableSet(onlineUsers); - } - - /** - * 获取在线的接收者 - * 从所有接收者中过滤出在线的接收者 - * - * @param receivers 接收者 - * @return 在线的接收者集合 - */ - public Set getOnlineReceivers(Set receivers) { - return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet()); - } - - /** - * 获取在线用户数量 - * - * @return 在线用户数量 - */ - public int getOnlineUserCount() { - return onlineUsers.size(); - } - - -} diff --git a/src/main/java/com/youlai/boot/system/controller/ConfigController.java b/src/main/java/com/youlai/boot/system/controller/ConfigController.java index dd384d9c..1e8d446b 100644 --- a/src/main/java/com/youlai/boot/system/controller/ConfigController.java +++ b/src/main/java/com/youlai/boot/system/controller/ConfigController.java @@ -2,8 +2,8 @@ package com.youlai.boot.system.controller; import com.baomidou.mybatisplus.core.metadata.IPage; import com.youlai.boot.common.enums.LogModuleEnum; -import com.youlai.boot.common.result.PageResult; -import com.youlai.boot.common.result.Result; +import com.youlai.boot.core.web.PageResult; +import com.youlai.boot.core.web.Result; import com.youlai.boot.common.annotation.Log; import com.youlai.boot.system.model.form.ConfigForm; import com.youlai.boot.system.model.query.ConfigPageQuery; diff --git a/src/main/java/com/youlai/boot/system/controller/DeptController.java b/src/main/java/com/youlai/boot/system/controller/DeptController.java index 68b62ecf..f1064353 100644 --- a/src/main/java/com/youlai/boot/system/controller/DeptController.java +++ b/src/main/java/com/youlai/boot/system/controller/DeptController.java @@ -3,7 +3,7 @@ package com.youlai.boot.system.controller; import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.annotation.RepeatSubmit; import com.youlai.boot.common.model.Option; -import com.youlai.boot.common.result.Result; +import com.youlai.boot.core.web.Result; import com.youlai.boot.system.model.form.DeptForm; import com.youlai.boot.system.model.query.DeptQuery; import com.youlai.boot.system.model.vo.DeptVO; @@ -22,7 +22,7 @@ import java.util.List; /** * 部门控制器 * - * @author haoxr + * @author Ray.Hao * @since 2020/11/6 */ @Tag(name = "05.部门接口") diff --git a/src/main/java/com/youlai/boot/system/controller/DictController.java b/src/main/java/com/youlai/boot/system/controller/DictController.java index 0e47bb54..c9dcac51 100644 --- a/src/main/java/com/youlai/boot/system/controller/DictController.java +++ b/src/main/java/com/youlai/boot/system/controller/DictController.java @@ -2,8 +2,8 @@ package com.youlai.boot.system.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.common.model.Option; -import com.youlai.boot.common.result.PageResult; -import com.youlai.boot.common.result.Result; +import com.youlai.boot.core.web.PageResult; +import com.youlai.boot.core.web.Result; import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.system.model.form.DictItemForm; import com.youlai.boot.system.model.query.DictItemPageQuery; @@ -16,7 +16,11 @@ import com.youlai.boot.system.model.form.DictForm; import com.youlai.boot.common.annotation.Log; import com.youlai.boot.system.service.DictItemService; import com.youlai.boot.system.service.DictService; +<<<<<<< HEAD import com.youlai.boot.system.service.WebSocketMessageService; +======= +import com.youlai.boot.system.service.WebSocketService; +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Operation; @@ -43,7 +47,11 @@ public class DictController { private final DictService dictService; private final DictItemService dictItemService; +<<<<<<< HEAD private final WebSocketMessageService webSocketMessageService; +======= + private final WebSocketService webSocketService; +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d //--------------------------------------------------- // 字典相关接口 @@ -66,7 +74,7 @@ public class DictController { return Result.success(list); } - @Operation(summary = "字典表单数据") + @Operation(summary = "获取字典表单数据") @GetMapping("/{id}/form") public Result getDictForm( @Parameter(description = "字典ID") @PathVariable Long id @@ -82,8 +90,13 @@ public class DictController { public Result saveDict(@Valid @RequestBody DictForm formData) { boolean result = dictService.saveDict(formData); // 发送字典更新通知 +<<<<<<< HEAD if (result && formData.getCode() != null) { webSocketMessageService.sendDictUpdatedEvent(formData.getCode()); +======= + if (result) { + webSocketService.broadcastDictChange(formData.getDictCode()); +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d } return Result.judge(result); } @@ -97,8 +110,13 @@ public class DictController { ) { boolean status = dictService.updateDict(id, dictForm); // 发送字典更新通知 +<<<<<<< HEAD if (status && dictForm.getCode() != null) { webSocketMessageService.sendDictUpdatedEvent(dictForm.getCode()); +======= + if (status && dictForm.getDictCode() != null) { + webSocketService.broadcastDictChange(dictForm.getDictCode()); +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d } return Result.judge(status); } @@ -116,7 +134,11 @@ public class DictController { // 发送字典删除通知 for (String dictCode : dictCodes) { +<<<<<<< HEAD webSocketMessageService.sendDictDeletedEvent(dictCode); +======= + webSocketService.broadcastDictChange(dictCode); +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d } return Result.success(); @@ -159,7 +181,11 @@ public class DictController { // 发送字典更新通知 if (result) { +<<<<<<< HEAD webSocketMessageService.sendDictUpdatedEvent(dictCode); +======= + webSocketService.broadcastDictChange(dictCode); +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d } return Result.judge(result); @@ -190,7 +216,11 @@ public class DictController { // 发送字典更新通知 if (status) { +<<<<<<< HEAD webSocketMessageService.sendDictUpdatedEvent(dictCode); +======= + webSocketService.broadcastDictChange(dictCode); +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d } return Result.judge(status); @@ -206,7 +236,11 @@ public class DictController { dictItemService.deleteDictItemByIds(itemIds); // 发送字典更新通知 +<<<<<<< HEAD webSocketMessageService.sendDictUpdatedEvent(dictCode); +======= + webSocketService.broadcastDictChange(dictCode); +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d return Result.success(); } diff --git a/src/main/java/com/youlai/boot/system/controller/LogController.java b/src/main/java/com/youlai/boot/system/controller/LogController.java index 1db2613c..46e312c1 100644 --- a/src/main/java/com/youlai/boot/system/controller/LogController.java +++ b/src/main/java/com/youlai/boot/system/controller/LogController.java @@ -1,8 +1,8 @@ package com.youlai.boot.system.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.youlai.boot.common.result.PageResult; -import com.youlai.boot.common.result.Result; +import com.youlai.boot.core.web.PageResult; +import com.youlai.boot.core.web.Result; import com.youlai.boot.system.model.query.LogPageQuery; import com.youlai.boot.system.model.vo.LogPageVO; import com.youlai.boot.system.model.vo.VisitStatsVO; diff --git a/src/main/java/com/youlai/boot/system/controller/MenuController.java b/src/main/java/com/youlai/boot/system/controller/MenuController.java index f5e2f62a..c35199f4 100644 --- a/src/main/java/com/youlai/boot/system/controller/MenuController.java +++ b/src/main/java/com/youlai/boot/system/controller/MenuController.java @@ -1,15 +1,14 @@ package com.youlai.boot.system.controller; -import com.youlai.boot.common.result.Result; -import com.youlai.boot.common.enums.LogModuleEnum; +import com.youlai.boot.common.annotation.Log; import com.youlai.boot.common.annotation.RepeatSubmit; +import com.youlai.boot.common.enums.LogModuleEnum; +import com.youlai.boot.common.model.Option; +import com.youlai.boot.core.web.Result; import com.youlai.boot.system.model.form.MenuForm; import com.youlai.boot.system.model.query.MenuQuery; import com.youlai.boot.system.model.vo.MenuVO; -import com.youlai.boot.common.model.Option; import com.youlai.boot.system.model.vo.RouteVO; -import com.youlai.boot.common.annotation.Log; -import com.youlai.boot.core.security.util.SecurityUtils; import com.youlai.boot.system.service.MenuService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -20,7 +19,6 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.List; -import java.util.Set; /** * 菜单控制层 @@ -39,31 +37,32 @@ public class MenuController { @Operation(summary = "菜单列表") @GetMapping - @Log( value = "菜单列表",module = LogModuleEnum.MENU) - public Result> listMenus(MenuQuery queryParams) { + @Log(value = "菜单列表", module = LogModuleEnum.MENU) + public Result> getMenus(MenuQuery queryParams) { List menuList = menuService.listMenus(queryParams); return Result.success(menuList); } @Operation(summary = "菜单下拉列表") @GetMapping("/options") - public Result>> listMenuOptions( - @Parameter(description = "是否只查询父级菜单") - @RequestParam(required = false, defaultValue = "false") boolean onlyParent + public Result>> getMenuOptions( + @Parameter(description = "是否只查询父级菜单") + @RequestParam(required = false, defaultValue = "false") boolean onlyParent ) { List> menus = menuService.listMenuOptions(onlyParent); return Result.success(menus); } - @Operation(summary = "菜单路由列表") + @Operation(summary = "当前用户菜单路由列表") @GetMapping("/routes") public Result> getCurrentUserRoutes() { - List routeList = menuService.getCurrentUserRoutes(); + List routeList = menuService.listCurrentUserRoutes(); return Result.success(routeList); } @Operation(summary = "菜单表单数据") @GetMapping("/{id}/form") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") public Result getMenuForm( @Parameter(description = "菜单ID") @PathVariable Long id ) { @@ -102,6 +101,7 @@ public class MenuController { @Operation(summary = "修改菜单显示状态") @PatchMapping("/{menuId}") + @PreAuthorize("@ss.hasPerm('sys:menu:edit')") public Result updateMenuVisible( @Parameter(description = "菜单ID") @PathVariable Long menuId, @Parameter(description = "显示状态(1:显示;0:隐藏)") Integer visible diff --git a/src/main/java/com/youlai/boot/system/controller/NoticeController.java b/src/main/java/com/youlai/boot/system/controller/NoticeController.java index f08198e0..3d38a08a 100644 --- a/src/main/java/com/youlai/boot/system/controller/NoticeController.java +++ b/src/main/java/com/youlai/boot/system/controller/NoticeController.java @@ -1,8 +1,8 @@ package com.youlai.boot.system.controller; import com.baomidou.mybatisplus.core.metadata.IPage; -import com.youlai.boot.common.result.PageResult; -import com.youlai.boot.common.result.Result; +import com.youlai.boot.core.web.PageResult; +import com.youlai.boot.core.web.Result; import com.youlai.boot.system.model.form.NoticeForm; import com.youlai.boot.system.model.query.NoticePageQuery; import com.youlai.boot.system.model.vo.NoticeDetailVO; diff --git a/src/main/java/com/youlai/boot/system/controller/RoleController.java b/src/main/java/com/youlai/boot/system/controller/RoleController.java index b5b7419a..fef84c40 100644 --- a/src/main/java/com/youlai/boot/system/controller/RoleController.java +++ b/src/main/java/com/youlai/boot/system/controller/RoleController.java @@ -4,8 +4,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.annotation.RepeatSubmit; import com.youlai.boot.common.model.Option; -import com.youlai.boot.common.result.PageResult; -import com.youlai.boot.common.result.Result; +import com.youlai.boot.core.web.PageResult; +import com.youlai.boot.core.web.Result; import com.youlai.boot.system.model.form.RoleForm; import com.youlai.boot.system.model.query.RolePageQuery; import com.youlai.boot.system.model.vo.RolePageVO; @@ -62,8 +62,9 @@ public class RoleController { return Result.judge(result); } - @Operation(summary = "角色表单数据") + @Operation(summary = "获取角色表单数据") @GetMapping("/{roleId}/form") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") public Result getRoleForm( @Parameter(description = "角色ID") @PathVariable Long roleId ) { @@ -91,6 +92,7 @@ public class RoleController { @Operation(summary = "修改角色状态") @PutMapping(value = "/{roleId}/status") + @PreAuthorize("@ss.hasPerm('sys:role:edit')") public Result updateRoleStatus( @Parameter(description = "角色ID") @PathVariable Long roleId, @Parameter(description = "状态(1:启用;0:禁用)") @RequestParam Integer status @@ -108,7 +110,7 @@ public class RoleController { return Result.success(menuIds); } - @Operation(summary = "分配菜单(包括按钮权限)给角色") + @Operation(summary = "角色分配菜单权限") @PutMapping("/{roleId}/menus") public Result assignMenusToRole( @PathVariable Long roleId, diff --git a/src/main/java/com/youlai/boot/system/controller/UserController.java b/src/main/java/com/youlai/boot/system/controller/UserController.java index c9080fd1..7b7d98ec 100644 --- a/src/main/java/com/youlai/boot/system/controller/UserController.java +++ b/src/main/java/com/youlai/boot/system/controller/UserController.java @@ -8,11 +8,11 @@ import com.youlai.boot.common.annotation.Log; import com.youlai.boot.common.annotation.RepeatSubmit; import com.youlai.boot.common.enums.LogModuleEnum; import com.youlai.boot.common.model.Option; -import com.youlai.boot.common.result.ExcelResult; -import com.youlai.boot.common.result.PageResult; -import com.youlai.boot.common.result.Result; +import com.youlai.boot.core.web.ExcelResult; +import com.youlai.boot.core.web.PageResult; +import com.youlai.boot.core.web.Result; import com.youlai.boot.common.util.ExcelUtils; -import com.youlai.boot.core.security.util.SecurityUtils; +import com.youlai.boot.security.util.SecurityUtils; import com.youlai.boot.system.listener.UserImportListener; import com.youlai.boot.system.model.dto.UserExportDTO; import com.youlai.boot.system.model.dto.UserImportDTO; @@ -78,8 +78,9 @@ public class UserController { return Result.judge(result); } - @Operation(summary = "用户表单数据") + @Operation(summary = "获取用户表单数据") @GetMapping("/{userId}/form") + @PreAuthorize("@ss.hasPerm('sys:user:edit')") @Log(value = "用户表单数据", module = LogModuleEnum.USER) public Result getUserForm( @Parameter(description = "用户ID") @PathVariable Long userId @@ -113,6 +114,7 @@ public class UserController { @Operation(summary = "修改用户状态") @PatchMapping(value = "/{userId}/status") + @PreAuthorize("@ss.hasPerm('sys:user:edit')") @Log(value = "修改用户状态", module = LogModuleEnum.USER) public Result updateUserStatus( @Parameter(description = "用户ID") @PathVariable Long userId, @@ -154,6 +156,7 @@ public class UserController { @Operation(summary = "导入用户") @PostMapping("/import") + @PreAuthorize("@ss.hasPerm('sys:user:import')") @Log(value = "导入用户", module = LogModuleEnum.USER) public Result importUsers(MultipartFile file) throws IOException { UserImportListener listener = new UserImportListener(); @@ -163,6 +166,7 @@ public class UserController { @Operation(summary = "导出用户") @GetMapping("/export") + @PreAuthorize("@ss.hasPerm('sys:user:export')") @Log(value = "导出用户", module = LogModuleEnum.USER) public void exportUsers(UserPageQuery queryParams, HttpServletResponse response) throws IOException { String fileName = "用户列表.xlsx"; @@ -248,7 +252,7 @@ public class UserController { return Result.judge(result); } - @Operation(summary = "用户下拉选项") + @Operation(summary = "获取用户下拉选项") @GetMapping("/options") public Result>> listUserOptions() { List> list = userService.listUserOptions(); diff --git a/src/main/java/com/youlai/boot/system/converter/DictDataConverter.java b/src/main/java/com/youlai/boot/system/converter/DictItemConverter.java similarity index 89% rename from src/main/java/com/youlai/boot/system/converter/DictDataConverter.java rename to src/main/java/com/youlai/boot/system/converter/DictItemConverter.java index c2480f78..99a354b0 100644 --- a/src/main/java/com/youlai/boot/system/converter/DictDataConverter.java +++ b/src/main/java/com/youlai/boot/system/converter/DictItemConverter.java @@ -10,13 +10,13 @@ import org.mapstruct.Mapper; import java.util.List; /** - * 字典项 对象转换器 + * 字典项对象转换器 * - * @author Ray + * @author Ray.Hao * @since 2022/6/8 */ @Mapper(componentModel = "spring") -public interface DictDataConverter { +public interface DictItemConverter { Page toPageVo(Page page); diff --git a/src/main/java/com/youlai/boot/system/event/UserConnectionEvent.java b/src/main/java/com/youlai/boot/system/event/UserConnectionEvent.java deleted file mode 100644 index dadea814..00000000 --- a/src/main/java/com/youlai/boot/system/event/UserConnectionEvent.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.youlai.boot.system.event; - -import lombok.Getter; -import org.springframework.context.ApplicationEvent; - - -/** - * 用户连接事件 - * - * @author Ray - * @since 2.3.0 - */ -@Getter -public class UserConnectionEvent extends ApplicationEvent { - - /** - * 用户名 - */ - private final String username; - - /** - * 是否连接 - */ - private final boolean connected; - - /** - * 用户连接事件 - * - * @param source 事件源 - * @param username 用户名 - * @param connected 是否连接 - */ - public UserConnectionEvent(Object source, String username, boolean connected) { - super(source); - this.username = username; - this.connected = connected; - } -} diff --git a/src/main/java/com/youlai/boot/shared/websocket/handler/OnlineUserJobHandler.java b/src/main/java/com/youlai/boot/system/handler/OnlineUserJobHandler.java similarity index 55% rename from src/main/java/com/youlai/boot/shared/websocket/handler/OnlineUserJobHandler.java rename to src/main/java/com/youlai/boot/system/handler/OnlineUserJobHandler.java index 75b148e2..39d1b00f 100644 --- a/src/main/java/com/youlai/boot/shared/websocket/handler/OnlineUserJobHandler.java +++ b/src/main/java/com/youlai/boot/system/handler/OnlineUserJobHandler.java @@ -1,7 +1,7 @@ -package com.youlai.boot.shared.websocket.handler; +package com.youlai.boot.system.handler; -import com.youlai.boot.shared.websocket.service.OnlineUserService; +import com.youlai.boot.system.service.UserOnlineService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.simp.SimpMessagingTemplate; @@ -19,15 +19,16 @@ import org.springframework.stereotype.Component; @RequiredArgsConstructor public class OnlineUserJobHandler { - private final OnlineUserService onlineUserService; + private final UserOnlineService userOnlineService; private final SimpMessagingTemplate messagingTemplate; - // 每分钟统计一次在线用户数 - @Scheduled(cron = "0 * * * * ?") + // 每3分钟统计一次在线用户数,减少服务器压力 + @Scheduled(cron = "0 */3 * * * ?") public void execute() { log.info("定时任务:统计在线用户数"); - // 推送在线用户人数 - messagingTemplate.convertAndSend("/topic/onlineUserCount", onlineUserService.getOnlineUserCount()); + // 推送在线用户数量到新主题 + int count = userOnlineService.getOnlineUserCount(); + messagingTemplate.convertAndSend("/topic/online-count", count); } } diff --git a/src/main/java/com/youlai/boot/system/listener/UserImportListener.java b/src/main/java/com/youlai/boot/system/listener/UserImportListener.java index 806d7e60..a699fb54 100644 --- a/src/main/java/com/youlai/boot/system/listener/UserImportListener.java +++ b/src/main/java/com/youlai/boot/system/listener/UserImportListener.java @@ -11,7 +11,7 @@ import cn.idev.excel.event.AnalysisEventListener; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.youlai.boot.common.constant.SystemConstants; import com.youlai.boot.common.enums.StatusEnum; -import com.youlai.boot.common.result.ExcelResult; +import com.youlai.boot.core.web.ExcelResult; import com.youlai.boot.system.converter.UserConverter; import com.youlai.boot.system.enums.DictCodeEnum; import com.youlai.boot.system.model.dto.UserImportDTO; diff --git a/src/main/java/com/youlai/boot/system/mapper/UserMapper.java b/src/main/java/com/youlai/boot/system/mapper/UserMapper.java index 74d3388f..74af1bd5 100644 --- a/src/main/java/com/youlai/boot/system/mapper/UserMapper.java +++ b/src/main/java/com/youlai/boot/system/mapper/UserMapper.java @@ -7,7 +7,7 @@ import com.youlai.boot.system.model.entity.User; import com.youlai.boot.system.model.query.UserPageQuery; import com.youlai.boot.system.model.form.UserForm; import com.youlai.boot.common.annotation.DataPermission; -import com.youlai.boot.core.security.model.UserAuthCredentials; +import com.youlai.boot.security.model.UserAuthCredentials; import com.youlai.boot.system.model.dto.UserExportDTO; import org.apache.ibatis.annotations.Mapper; diff --git a/src/main/java/com/youlai/boot/system/model/dto/DictEventDTO.java b/src/main/java/com/youlai/boot/system/model/dto/DictEventDTO.java new file mode 100644 index 00000000..b3cae343 --- /dev/null +++ b/src/main/java/com/youlai/boot/system/model/dto/DictEventDTO.java @@ -0,0 +1,28 @@ +package com.youlai.boot.system.model.dto; + +import lombok.Data; + +/** + * 字典更新事件消息 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +public class DictEventDTO { + /** + * 字典编码 + */ + private String dictCode; + + /** + * 时间戳 + */ + private long timestamp; + + public DictEventDTO(String dictCode) { + this.dictCode = dictCode; + this.timestamp = System.currentTimeMillis(); + } +} + diff --git a/src/main/java/com/youlai/boot/system/model/dto/UserSessionDTO.java b/src/main/java/com/youlai/boot/system/model/dto/UserSessionDTO.java new file mode 100644 index 00000000..80cb44c1 --- /dev/null +++ b/src/main/java/com/youlai/boot/system/model/dto/UserSessionDTO.java @@ -0,0 +1,37 @@ +package com.youlai.boot.system.model.dto; + +import lombok.Data; + +import java.util.HashSet; +import java.util.Set; + +/** + * 用户会话DTO + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Data +public class UserSessionDTO { + + /** + * 用户名 + */ + private String username; + + /** + * 用户会话ID集合 + */ + private Set sessionIds; + + /** + * 最后活动时间 + */ + private long lastActiveTime; + + public UserSessionDTO(String username) { + this.username = username; + this.sessionIds = new HashSet<>(); + this.lastActiveTime = System.currentTimeMillis(); + } +} diff --git a/src/main/java/com/youlai/boot/system/model/form/DictForm.java b/src/main/java/com/youlai/boot/system/model/form/DictForm.java index 31e5a996..b17d320a 100644 --- a/src/main/java/com/youlai/boot/system/model/form/DictForm.java +++ b/src/main/java/com/youlai/boot/system/model/form/DictForm.java @@ -2,6 +2,7 @@ package com.youlai.boot.system.model.form; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; import lombok.Data; import lombok.Getter; import lombok.Setter; @@ -26,6 +27,7 @@ public class DictForm { private String name; @Schema(description = "字典编码", example ="gender") + @NotBlank(message = "字典编码不能为空") private String dictCode; @Schema(description = "备注") diff --git a/src/main/java/com/youlai/boot/system/model/form/PasswordUpdateForm.java b/src/main/java/com/youlai/boot/system/model/form/PasswordUpdateForm.java index 453207a2..aaab86d7 100644 --- a/src/main/java/com/youlai/boot/system/model/form/PasswordUpdateForm.java +++ b/src/main/java/com/youlai/boot/system/model/form/PasswordUpdateForm.java @@ -19,4 +19,6 @@ public class PasswordUpdateForm { @Schema(description = "新密码") private String newPassword; + @Schema(description = "确认密码") + private String confirmPassword; } diff --git a/src/main/java/com/youlai/boot/system/service/MenuService.java b/src/main/java/com/youlai/boot/system/service/MenuService.java index 41ba6f15..6fbff3f3 100644 --- a/src/main/java/com/youlai/boot/system/service/MenuService.java +++ b/src/main/java/com/youlai/boot/system/service/MenuService.java @@ -1,7 +1,7 @@ package com.youlai.boot.system.service; import com.baomidou.mybatisplus.extension.service.IService; -import com.youlai.boot.shared.codegen.model.entity.GenConfig; +import com.youlai.boot.platform.codegen.model.entity.GenConfig; import com.youlai.boot.system.model.form.MenuForm; import com.youlai.boot.common.model.Option; import com.youlai.boot.system.model.entity.Menu; @@ -40,9 +40,16 @@ public interface MenuService extends IService

{ boolean saveMenu(MenuForm menuForm); /** - * 获取路由列表 + * 获取当前用户的菜单路由列表 */ - List getCurrentUserRoutes(); + List listCurrentUserRoutes(); + + /** + * 获取当前用户的菜单路由列表(指定数据源) + * + * @param datasource 数据源名称,如:master(主库)、naiveui(NaiveUI数据库)、template(模板数据库) + */ + List listCurrentUserRoutes(String datasource); /** * 修改菜单显示状态 diff --git a/src/main/java/com/youlai/boot/system/service/UserOnlineService.java b/src/main/java/com/youlai/boot/system/service/UserOnlineService.java index 2bab17a0..747aeb61 100644 --- a/src/main/java/com/youlai/boot/system/service/UserOnlineService.java +++ b/src/main/java/com/youlai/boot/system/service/UserOnlineService.java @@ -2,10 +2,18 @@ package com.youlai.boot.system.service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +<<<<<<< HEAD import com.youlai.boot.core.security.model.SysUserDetails; import lombok.Data; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +======= +import com.youlai.boot.security.model.SysUserDetails; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Service; @@ -18,28 +26,60 @@ import java.util.stream.Collectors; * 用户在线状态服务 * 负责维护用户的在线状态和相关统计 * +<<<<<<< HEAD * @author You Lai * @since 3.0.0 */ @Service @RequiredArgsConstructor +======= + * @author Ray.Hao + * @since 3.0.0 + */ +@Service +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d @Slf4j public class UserOnlineService { // 在线用户映射表,key为用户名,value为用户在线信息 private final Map onlineUsers = new ConcurrentHashMap<>(); +<<<<<<< HEAD private final SimpMessagingTemplate messagingTemplate; private final ObjectMapper objectMapper; +======= + private SimpMessagingTemplate messagingTemplate; + private final ObjectMapper objectMapper; + + @Autowired + public UserOnlineService(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Autowired(required = false) + public void setMessagingTemplate(SimpMessagingTemplate messagingTemplate) { + this.messagingTemplate = messagingTemplate; + } + +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d /** * 用户上线 * * @param username 用户名 +<<<<<<< HEAD * @param sessionId WebSocket会话ID */ public void userConnected(String username, String sessionId) { UserOnlineInfo info = new UserOnlineInfo(username, sessionId, System.currentTimeMillis()); +======= + * @param sessionId WebSocket会话ID(可选) + */ + public void userConnected(String username, String sessionId) { + // 生成会话ID(如果未提供) + String actualSessionId = sessionId != null ? sessionId : "session-" + System.nanoTime(); + UserOnlineInfo info = new UserOnlineInfo(username, actualSessionId, System.currentTimeMillis()); +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d onlineUsers.put(username, info); log.info("用户[{}]上线,当前在线用户数:{}", username, onlineUsers.size()); @@ -94,6 +134,7 @@ public class UserOnlineService { * 通知所有客户端在线用户变更 */ private void notifyOnlineUsersChange() { +<<<<<<< HEAD try { OnlineUsersChangeEvent event = new OnlineUsersChangeEvent(); event.setType("ONLINE_USERS_CHANGE"); @@ -105,6 +146,33 @@ public class UserOnlineService { messagingTemplate.convertAndSend("/topic/online-users", message); } catch (JsonProcessingException e) { log.error("Failed to send online users change event", e); +======= + if (messagingTemplate == null) { + log.warn("消息模板尚未初始化,无法发送在线用户数量"); + return; + } + + // 发送简化版数据(仅数量) + sendOnlineUserCount(); + } + + /** + * 发送在线用户数量(简化版,不包含用户详情) + */ + private void sendOnlineUserCount() { + if (messagingTemplate == null) { + log.warn("消息模板尚未初始化,无法发送在线用户数量"); + return; + } + + try { + // 直接发送数量,更轻量 + int count = onlineUsers.size(); + messagingTemplate.convertAndSend("/topic/online-count", count); + log.debug("已发送在线用户数量: {}", count); + } catch (Exception e) { + log.error("发送在线用户数量失败", e); +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d } } diff --git a/src/main/java/com/youlai/boot/system/service/UserRoleService.java b/src/main/java/com/youlai/boot/system/service/UserRoleService.java index 697b86ca..0d42915b 100644 --- a/src/main/java/com/youlai/boot/system/service/UserRoleService.java +++ b/src/main/java/com/youlai/boot/system/service/UserRoleService.java @@ -15,7 +15,7 @@ public interface UserRoleService extends IService { * @param roleIds * @return */ - boolean saveUserRoles(Long userId, List roleIds); + void saveUserRoles(Long userId, List roleIds); /** * 判断角色是否存在绑定的用户 diff --git a/src/main/java/com/youlai/boot/system/service/UserService.java b/src/main/java/com/youlai/boot/system/service/UserService.java index 1ddd2c12..9b8660ba 100644 --- a/src/main/java/com/youlai/boot/system/service/UserService.java +++ b/src/main/java/com/youlai/boot/system/service/UserService.java @@ -3,7 +3,7 @@ package com.youlai.boot.system.service; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.IService; import com.youlai.boot.common.model.Option; -import com.youlai.boot.core.security.model.UserAuthCredentials; +import com.youlai.boot.security.model.UserAuthCredentials; import com.youlai.boot.system.model.dto.CurrentUserDTO; import com.youlai.boot.system.model.dto.UserExportDTO; import com.youlai.boot.system.model.entity.User; @@ -165,18 +165,18 @@ public interface UserService extends IService { /** * 根据 openid 获取用户认证信息 * - * @param username 用户名 + * @param openId 用户名 * @return {@link UserAuthCredentials} */ - UserAuthCredentials getAuthCredentialsByOpenId(String username); + UserAuthCredentials getAuthCredentialsByOpenId(String openId); /** * 根据微信 OpenID 注册或绑定用户 * * @param openId 微信 OpenID */ - void registerOrBindWechatUser(String openId); + boolean registerOrBindWechatUser(String openId); /** * 根据手机号获取用户认证信息 @@ -186,5 +186,22 @@ public interface UserService extends IService { */ UserAuthCredentials getAuthCredentialsByMobile(String mobile); + /** + * 根据手机号和OpenID注册用户 + * + * @param mobile 手机号 + * @param openId 微信OpenID + * @return 是否成功 + */ + boolean registerUserByMobileAndOpenId(String mobile, String openId); + + /** + * 绑定用户微信OpenID + * + * @param userId 用户ID + * @param openId 微信OpenID + * @return 是否成功 + */ + boolean bindUserOpenId(Long userId, String openId); } diff --git a/src/main/java/com/youlai/boot/system/service/WebSocketService.java b/src/main/java/com/youlai/boot/system/service/WebSocketService.java new file mode 100644 index 00000000..487412a8 --- /dev/null +++ b/src/main/java/com/youlai/boot/system/service/WebSocketService.java @@ -0,0 +1,46 @@ +package com.youlai.boot.system.service; + +/** + * WebSocket服务接口 + *

+ * 提供与WebSocket连接管理相关的功能,包括: + * - 用户连接/断开事件处理 + * - 字典数据变更通知 + * - 系统消息推送 + *

+ * + * @author Ray.Hao + * @since 3.0.0 + */ +public interface WebSocketService { + + /** + * 处理用户连接事件 + * + * @param username 用户名 + * @param sessionId WebSocket会话ID + */ + void userConnected(String username, String sessionId); + + /** + * 处理用户断开连接事件 + * + * @param username 用户名 + */ + void userDisconnected(String username); + + /** + * 广播字典数据变更通知 + * + * @param dictCode 字典编码 + */ + void broadcastDictChange(String dictCode); + + /** + * 发送系统通知给特定用户 + * + * @param username 目标用户名 + * @param message 通知消息内容 + */ + void sendNotification(String username, Object message); +} diff --git a/src/main/java/com/youlai/boot/system/service/impl/ConfigServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/ConfigServiceImpl.java index 3c939dca..e4e12c26 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/ConfigServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/ConfigServiceImpl.java @@ -13,7 +13,7 @@ import com.youlai.boot.system.model.form.ConfigForm; import com.youlai.boot.system.model.query.ConfigPageQuery; import com.youlai.boot.system.model.vo.ConfigVO; import com.youlai.boot.system.service.ConfigService; -import com.youlai.boot.core.security.util.SecurityUtils; +import com.youlai.boot.security.util.SecurityUtils; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; diff --git a/src/main/java/com/youlai/boot/system/service/impl/DeptServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/DeptServiceImpl.java index ee7e76e1..39a60deb 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/DeptServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/DeptServiceImpl.java @@ -6,7 +6,7 @@ import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.youlai.boot.core.security.util.SecurityUtils; +import com.youlai.boot.security.util.SecurityUtils; import com.youlai.boot.system.converter.DeptConverter; import com.youlai.boot.system.mapper.DeptMapper; import com.youlai.boot.system.model.entity.Dept; diff --git a/src/main/java/com/youlai/boot/system/service/impl/DictItemServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/DictItemServiceImpl.java index 620a53cf..7c1d8058 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/DictItemServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/DictItemServiceImpl.java @@ -3,7 +3,7 @@ package com.youlai.boot.system.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.youlai.boot.system.converter.DictDataConverter; +import com.youlai.boot.system.converter.DictItemConverter; import com.youlai.boot.system.mapper.DictItemMapper; import com.youlai.boot.system.model.entity.DictItem; import com.youlai.boot.system.model.form.DictItemForm; @@ -27,7 +27,7 @@ import java.util.List; @RequiredArgsConstructor public class DictItemServiceImpl extends ServiceImpl implements DictItemService { - private final DictDataConverter dictDataConverter; + private final DictItemConverter dictItemConverter; /** * 获取字典项分页列表 @@ -78,7 +78,7 @@ public class DictItemServiceImpl extends ServiceImpl i @Override public DictItemForm getDictItemForm( Long itemId) { DictItem entity = this.getById(itemId); - return dictDataConverter.toForm(entity); + return dictItemConverter.toForm(entity); } /** @@ -89,7 +89,7 @@ public class DictItemServiceImpl extends ServiceImpl i */ @Override public boolean saveDictItem(DictItemForm formData) { - DictItem entity = dictDataConverter.toEntity(formData); + DictItem entity = dictItemConverter.toEntity(formData); return this.save(entity); } @@ -101,7 +101,7 @@ public class DictItemServiceImpl extends ServiceImpl i */ @Override public boolean updateDictItem(DictItemForm formData) { - DictItem entity = dictDataConverter.toEntity(formData); + DictItem entity = dictItemConverter.toEntity(formData); return this.updateById(entity); } @@ -112,7 +112,9 @@ public class DictItemServiceImpl extends ServiceImpl i */ @Override public void deleteDictItemByIds(String ids) { - List idList = Arrays.stream(ids.split(",")).map(Long::parseLong).toList(); + List idList = Arrays.stream(ids.split(",")) + .map(Long::parseLong) + .toList(); this.removeByIds(idList); } diff --git a/src/main/java/com/youlai/boot/system/service/impl/DictServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/DictServiceImpl.java index 4dfb2a2d..a2a04c6f 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/DictServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/DictServiceImpl.java @@ -4,7 +4,7 @@ import cn.hutool.core.lang.Assert; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.youlai.boot.common.exception.BusinessException; +import com.youlai.boot.core.exception.BusinessException; import com.youlai.boot.common.model.Option; import com.youlai.boot.system.converter.DictConverter; import com.youlai.boot.system.mapper.DictMapper; @@ -22,7 +22,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; /** - * 数据字典业务实现类 + * 字典业务实现类 * * @author haoxr * @since 2022/10/12 @@ -108,6 +108,7 @@ public class DictServiceImpl extends ServiceImpl implements Di * @param dictForm 字典表单 */ @Override + @Transactional public boolean updateDict(Long id, DictForm dictForm) { // 获取字典 Dict entity = this.getById(id); @@ -115,7 +116,11 @@ public class DictServiceImpl extends ServiceImpl implements Di throw new BusinessException("字典不存在"); } // 校验 code 是否唯一 +<<<<<<< HEAD String dictCode = dictForm.getCode(); +======= + String dictCode = dictForm.getDictCode(); +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d if (!entity.getDictCode().equals(dictCode)) { long count = this.count(new LambdaQueryWrapper() .eq(Dict::getDictCode, dictCode) @@ -125,7 +130,29 @@ public class DictServiceImpl extends ServiceImpl implements Di // 更新字典 Dict dict = dictConverter.toEntity(dictForm); dict.setId(id); +<<<<<<< HEAD return this.updateById(dict); +======= + boolean result = this.updateById(dict); + if (result) { + // 更新字典数据 + List dictItemList = dictItemService.list( + new LambdaQueryWrapper() + .eq(DictItem::getDictCode, entity.getDictCode()) + .select(DictItem::getId) + ); + if (!dictItemList.isEmpty()){ + List dictItemIds = dictItemList.stream().map(DictItem::getId).toList(); + DictItem dictItem = new DictItem(); + dictItem.setDictCode(dict.getDictCode()); + dictItemService.update(dictItem, + new LambdaQueryWrapper() + .in(DictItem::getId, dictItemIds) + ); + } + } + return result; +>>>>>>> 95412501fc69777ad7db6fef970b479c9651984d } /** diff --git a/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java index 22ac3b9d..4b2a644e 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/MenuServiceImpl.java @@ -10,10 +10,10 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.youlai.boot.core.security.util.SecurityUtils; +import com.youlai.boot.platform.codegen.model.entity.GenConfig; +import com.youlai.boot.security.util.SecurityUtils; import com.youlai.boot.system.converter.MenuConverter; import com.youlai.boot.system.mapper.MenuMapper; -import com.youlai.boot.shared.codegen.model.entity.GenConfig; import com.youlai.boot.system.model.entity.Menu; import com.youlai.boot.system.model.form.MenuForm; import com.youlai.boot.system.model.query.MenuQuery; @@ -139,16 +139,16 @@ public class MenuServiceImpl extends ServiceImpl implements Me } /** - * 获取菜单路由列表 + * 获取当前用户的菜单路由列表 */ @Override - public List getCurrentUserRoutes() { - + public List listCurrentUserRoutes() { Set roleCodes = SecurityUtils.getRoles(); if (CollectionUtil.isEmpty(roleCodes)) { return Collections.emptyList(); } + List menuList; if (SecurityUtils.isRoot()) { // 超级管理员获取所有菜单 @@ -162,6 +162,20 @@ public class MenuServiceImpl extends ServiceImpl implements Me return buildRoutes(SystemConstants.ROOT_NODE_ID, menuList); } + /** + * 获取当前用户的菜单路由列表(指定数据源) + * + * @param datasource 数据源名称 + * - master: 主库菜单数据 + * - naiveui: NaiveUI项目菜单数据 + * - template: 模板项目菜单数据 + */ + @Override + public List listCurrentUserRoutes(String datasource) { + return listCurrentUserRoutes(); + } + + /** * 递归生成菜单路由层级列表 * @@ -448,7 +462,7 @@ public class MenuServiceImpl extends ServiceImpl implements Me this.updateById(menu); // 生成CURD按钮权限 - String permPrefix = genConfig.getModuleName() + ":" + StrUtil.lowerFirst(entityName) + ":"; + String permPrefix = genConfig.getModuleName() + ":" + genConfig.getTableName().replace("_", "-") + ":"; String[] actions = {"查询", "新增", "编辑", "删除"}; String[] perms = {"query", "add", "edit", "delete"}; diff --git a/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java index a05f3051..b4d70ea5 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/NoticeServiceImpl.java @@ -7,9 +7,8 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.youlai.boot.common.exception.BusinessException; -import com.youlai.boot.core.security.util.SecurityUtils; -import com.youlai.boot.shared.websocket.service.OnlineUserService; +import com.youlai.boot.core.exception.BusinessException; +import com.youlai.boot.security.util.SecurityUtils; import com.youlai.boot.system.converter.NoticeConverter; import com.youlai.boot.system.enums.NoticePublishStatusEnum; import com.youlai.boot.system.enums.NoticeTargetEnum; @@ -26,6 +25,7 @@ import com.youlai.boot.system.model.vo.UserNoticePageVO; import com.youlai.boot.system.model.vo.NoticeDetailVO; import com.youlai.boot.system.service.NoticeService; import com.youlai.boot.system.service.UserNoticeService; +import com.youlai.boot.system.service.UserOnlineService; import com.youlai.boot.system.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.messaging.simp.SimpMessagingTemplate; @@ -53,7 +53,7 @@ public class NoticeServiceImpl extends ServiceImpl impleme private final UserNoticeService userNoticeService; private final UserService userService; private final SimpMessagingTemplate messagingTemplate; - private final OnlineUserService onlineUserService; + private final UserOnlineService userOnlineService; /** * 获取通知公告分页列表 @@ -213,7 +213,9 @@ public class NoticeServiceImpl extends ServiceImpl impleme Set receivers = targetUserList.stream().map(User::getUsername).collect(Collectors.toSet()); - Set allOnlineUsers = onlineUserService.getAllOnlineUsers(); + Set allOnlineUsers = userOnlineService.getOnlineUsers().stream() + .map(UserOnlineService.UserOnlineDTO::getUsername) + .collect(Collectors.toSet()); // 找出在线用户的通知接收者 Set onlineReceivers = new HashSet<>(CollectionUtil.intersection(receivers, allOnlineUsers)); diff --git a/src/main/java/com/youlai/boot/system/service/impl/RoleServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/RoleServiceImpl.java index e56a0271..0efab740 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/RoleServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/RoleServiceImpl.java @@ -7,7 +7,7 @@ import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.youlai.boot.common.exception.BusinessException; +import com.youlai.boot.core.exception.BusinessException; import com.youlai.boot.system.converter.RoleConverter; import com.youlai.boot.system.mapper.RoleMapper; import com.youlai.boot.system.model.entity.Role; @@ -17,7 +17,7 @@ import com.youlai.boot.system.model.query.RolePageQuery; import com.youlai.boot.system.model.vo.RolePageVO; import com.youlai.boot.common.constant.SystemConstants; import com.youlai.boot.common.model.Option; -import com.youlai.boot.core.security.util.SecurityUtils; +import com.youlai.boot.security.util.SecurityUtils; import com.youlai.boot.system.service.RoleMenuService; import com.youlai.boot.system.service.RoleService; import com.youlai.boot.system.service.UserRoleService; diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserNoticeServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserNoticeServiceImpl.java index f44489ba..aa82529c 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserNoticeServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserNoticeServiceImpl.java @@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; -import com.youlai.boot.core.security.util.SecurityUtils; +import com.youlai.boot.security.util.SecurityUtils; import com.youlai.boot.system.mapper.UserNoticeMapper; import com.youlai.boot.system.model.entity.UserNotice; import com.youlai.boot.system.model.query.NoticePageQuery; diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserRoleServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserRoleServiceImpl.java index ce747648..148ea570 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserRoleServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserRoleServiceImpl.java @@ -3,80 +3,92 @@ package com.youlai.boot.system.service.impl; import cn.hutool.core.collection.CollectionUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.youlai.boot.security.token.TokenManager; +import com.youlai.boot.security.util.SecurityUtils; import com.youlai.boot.system.mapper.UserRoleMapper; import com.youlai.boot.system.model.entity.UserRole; import com.youlai.boot.system.service.UserRoleService; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @Service +@RequiredArgsConstructor public class UserRoleServiceImpl extends ServiceImpl implements UserRoleService { - /** - * 保存用户角色 - * - * @param userId - * @param roleIds - * @return - */ - @Override - public boolean saveUserRoles(Long userId, List roleIds) { - if (userId == null || CollectionUtil.isEmpty(roleIds)) { - return false; - } - - // 用户原角色ID集合 - List userRoleIds = this.list(new LambdaQueryWrapper() - .eq(UserRole::getUserId, userId)) - .stream() - .map(UserRole::getRoleId) - .collect(Collectors.toList()); - - // 新增用户角色 - List saveRoleIds; - if (CollectionUtil.isEmpty(userRoleIds)) { - saveRoleIds = roleIds; - } else { - saveRoleIds = roleIds.stream() - .filter(roleId -> !userRoleIds.contains(roleId)) - .collect(Collectors.toList()); - } - - List saveUserRoles = saveRoleIds - .stream() - .map(roleId -> new UserRole(userId, roleId)) - .collect(Collectors.toList()); - this.saveBatch(saveUserRoles); - - // 删除用户角色 - if (CollectionUtil.isNotEmpty(userRoleIds)) { - List removeRoleIds = userRoleIds.stream() - .filter(roleId -> !roleIds.contains(roleId)) - .collect(Collectors.toList()); - - if (CollectionUtil.isNotEmpty(removeRoleIds)) { - this.remove(new LambdaQueryWrapper() - .eq(UserRole::getUserId, userId) - .in(UserRole::getRoleId, removeRoleIds) - ); - } - } - return true; + private final TokenManager tokenManager; + /** + * 保存用户角色 + * + * @param userId 用户ID + * @param roleIds 选择的角色ID集合 + * @return + */ + @Override + public void saveUserRoles(Long userId, List roleIds) { + if (userId == null || CollectionUtil.isEmpty(roleIds)) { + return ; } - /** - * 判断角色是否存在绑定的用户 - * - * @param roleId 角色ID - * @return true:已分配 false:未分配 - */ - @Override - public boolean hasAssignedUsers(Long roleId) { - int count = this.baseMapper.countUsersForRole(roleId); - return count > 0; + // 获取现有角色 + List userRoleIds = this.list(new LambdaQueryWrapper() + .select(UserRole::getRoleId) + .eq(UserRole::getUserId, userId)) + .parallelStream() + .map(UserRole::getRoleId) + .toList(); + + // 使用Set提升对比效率 + Set oldRoles = new HashSet<>(userRoleIds); + Set newRoles = new HashSet<>(roleIds); + + // 计算变更集 + Set addedRoles = new HashSet<>(newRoles); + addedRoles.removeAll(oldRoles); + + Set removedRoles = new HashSet<>(oldRoles); + removedRoles.removeAll(newRoles); + + boolean rolesChanged = !addedRoles.isEmpty() || !removedRoles.isEmpty(); + + // 批量保存新增角色 + if (!addedRoles.isEmpty()) { + this.saveBatch(addedRoles.stream() + .map(roleId -> new UserRole(userId, roleId)) + .collect(Collectors.toList())); } + + // 删除废弃角色 + if (!removedRoles.isEmpty()) { + this.remove(new LambdaQueryWrapper() + .eq(UserRole::getUserId, userId) + .in(UserRole::getRoleId, removedRoles)); + } + + // 当权限变更时清除登录态 + if (rolesChanged) { + // 获取用户所有有效token(根据实际token存储实现) + String accessToken = SecurityUtils.getTokenFromRequest(); + tokenManager.invalidateToken(accessToken); + } + } + + /** + * 判断角色是否存在绑定的用户 + * + * @param roleId 角色ID + * @return true:已分配 false:未分配 + */ + @Override + public boolean hasAssignedUsers(Long roleId) { + int count = this.baseMapper.countUsersForRole(roleId); + return count > 0; + } } diff --git a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java index d950ffa9..827779c4 100644 --- a/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/youlai/boot/system/service/impl/UserServiceImpl.java @@ -10,19 +10,19 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.youlai.boot.common.constant.RedisConstants; import com.youlai.boot.common.constant.SystemConstants; -import com.youlai.boot.common.exception.BusinessException; +import com.youlai.boot.core.exception.BusinessException; import com.youlai.boot.common.model.Option; -import com.youlai.boot.core.security.token.TokenManager; -import com.youlai.boot.core.security.service.PermissionService; -import com.youlai.boot.core.security.util.SecurityUtils; -import com.youlai.boot.shared.mail.service.MailService; -import com.youlai.boot.shared.sms.enums.SmsTypeEnum; -import com.youlai.boot.shared.sms.service.SmsService; +import com.youlai.boot.platform.sms.enums.SmsTypeEnum; +import com.youlai.boot.platform.sms.service.SmsService; +import com.youlai.boot.security.model.UserAuthCredentials; +import com.youlai.boot.security.service.PermissionService; +import com.youlai.boot.security.token.TokenManager; +import com.youlai.boot.security.util.SecurityUtils; +import com.youlai.boot.platform.mail.service.MailService; import com.youlai.boot.system.converter.UserConverter; import com.youlai.boot.system.enums.DictCodeEnum; import com.youlai.boot.system.mapper.UserMapper; import com.youlai.boot.system.model.bo.UserBO; -import com.youlai.boot.core.security.model.UserAuthCredentials; import com.youlai.boot.system.model.dto.CurrentUserDTO; import com.youlai.boot.system.model.dto.UserExportDTO; import com.youlai.boot.system.model.entity.DictItem; @@ -34,11 +34,13 @@ import com.youlai.boot.system.model.vo.UserPageVO; import com.youlai.boot.system.model.vo.UserProfileVO; import com.youlai.boot.system.service.*; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -51,6 +53,7 @@ import java.util.stream.Collectors; */ @Service @RequiredArgsConstructor +@Slf4j public class UserServiceImpl extends ServiceImpl implements UserService { private final PasswordEncoder passwordEncoder; @@ -128,6 +131,7 @@ public class UserServiceImpl extends ServiceImpl implements Us // 设置默认加密密码 String defaultEncryptPwd = passwordEncoder.encode(SystemConstants.DEFAULT_PASSWORD); entity.setPassword(defaultEncryptPwd); + entity.setCreateBy(SecurityUtils.getUserId()); // 新增用户 boolean result = this.save(entity); @@ -160,6 +164,7 @@ public class UserServiceImpl extends ServiceImpl implements Us // form -> entity User entity = userConverter.toEntity(userForm); + entity.setUpdateBy(SecurityUtils.getUserId()); // 修改用户 boolean result = this.updateById(entity); @@ -207,14 +212,17 @@ public class UserServiceImpl extends ServiceImpl implements Us } /** - * 根据 openid 获取用户认证信息 + * 根据OpenID获取用户认证信息 * - * @param openid 微信 OpenId - * @return {@link UserAuthCredentials} + * @param openId 微信OpenID + * @return 用户认证信息 */ @Override - public UserAuthCredentials getAuthCredentialsByOpenId(String openid) { - UserAuthCredentials userAuthCredentials = this.baseMapper.getAuthCredentialsByOpenId(openid); + public UserAuthCredentials getAuthCredentialsByOpenId(String openId) { + if (StrUtil.isBlank(openId)) { + return null; + } + UserAuthCredentials userAuthCredentials = this.baseMapper.getAuthCredentialsByOpenId(openId); if (userAuthCredentials != null) { Set roles = userAuthCredentials.getRoles(); // 获取最大范围的数据权限 @@ -225,13 +233,16 @@ public class UserServiceImpl extends ServiceImpl implements Us } /** - * 根据手机号获取用户认证凭证信息 + * 根据手机号获取用户认证信息 * * @param mobile 手机号 - * @return {@link UserAuthCredentials} + * @return 用户认证信息 */ @Override public UserAuthCredentials getAuthCredentialsByMobile(String mobile) { + if (StrUtil.isBlank(mobile)) { + return null; + } UserAuthCredentials userAuthCredentials = this.baseMapper.getAuthCredentialsByMobile(mobile); if (userAuthCredentials != null) { Set roles = userAuthCredentials.getRoles(); @@ -242,34 +253,135 @@ public class UserServiceImpl extends ServiceImpl implements Us return userAuthCredentials; } - /** - * 根据微信 OpenID 注册或绑定用户 - *

- * TODO 根据手机号绑定用户 + * 注册或绑定微信用户 * - * @param openId 微信 OpenID + * @param openId 微信OpenID + * @return 是否成功 */ @Override - public void registerOrBindWechatUser(String openId) { - User user = this.getOne( - new LambdaQueryWrapper().eq(User::getOpenid, openId) - ); - if (user == null) { - user = new User(); - user.setNickname("微信用户"); // 默认昵称 - user.setUsername(openId); // TODO 后续替换为手机号 - user.setOpenid(openId); - user.setGender(0); // 保密 - user.setUpdateBy(SecurityUtils.getUserId()); - user.setPassword(SystemConstants.DEFAULT_PASSWORD); - this.save(user); - // 为了默认系统管理员角色,这里按需调整,实际情况绑定已存在的系统用户,另一种情况是给默认游客角色,然后由系统管理员设置用户的角色 - UserRole userRole = new UserRole(); - userRole.setUserId(user.getId()); - userRole.setRoleId(1L); // TODO 系统管理员 - userRoleService.save(userRole); + @Transactional(rollbackFor = Exception.class) + public boolean registerOrBindWechatUser(String openId) { + if (StrUtil.isBlank(openId)) { + return false; } + + // 查询是否已存在该openId的用户 + User existUser = this.getOne( + new LambdaQueryWrapper() + .eq(User::getOpenid, openId) + ); + + if (existUser != null) { + // 用户已存在,不需要注册 + return true; + } + + // 创建新用户 + User newUser = new User(); + newUser.setNickname("微信用户"); // 默认昵称 + newUser.setUsername(openId); // TODO 后续替换为手机号 + newUser.setOpenid(openId); + newUser.setGender(0); // 保密 + newUser.setUpdateBy(SecurityUtils.getUserId()); + newUser.setPassword(SystemConstants.DEFAULT_PASSWORD); + newUser.setCreateTime(LocalDateTime.now()); + newUser.setUpdateTime(LocalDateTime.now()); + this.save(newUser); + // 为了默认系统管理员角色,这里按需调整,实际情况绑定已存在的系统用户,另一种情况是给默认游客角色,然后由系统管理员设置用户的角色 + UserRole userRole = new UserRole(); + userRole.setUserId(newUser.getId()); + userRole.setRoleId(1L); // TODO 系统管理员 + userRoleService.save(userRole); + return true; + } + + /** + * 根据手机号和OpenID注册用户 + * + * @param mobile 手机号 + * @param openId 微信OpenID + * @return 是否成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean registerUserByMobileAndOpenId(String mobile, String openId) { + if (StrUtil.isBlank(mobile) || StrUtil.isBlank(openId)) { + return false; + } + + // 先查询是否已存在手机号对应的用户 + User existingUser = this.getOne( + new LambdaQueryWrapper() + .eq(User::getMobile, mobile) + ); + + if (existingUser != null) { + // 如果存在用户但没绑定openId,则绑定openId + if (StrUtil.isBlank(existingUser.getOpenid())) { + return bindUserOpenId(existingUser.getId(), openId); + } + // 如果已经绑定了其他openId,则判断是否需要更新 + else if (!openId.equals(existingUser.getOpenid())) { + return bindUserOpenId(existingUser.getId(), openId); + } + // 如果已经绑定了相同的openId,则不需要任何操作 + return true; + } + + // 不存在用户,创建新用户 + User newUser = new User(); + newUser.setMobile(mobile); + newUser.setOpenid(openId); + newUser.setUsername(mobile); // 使用手机号作为用户名 + newUser.setNickname("微信用户_" + mobile.substring(mobile.length() - 4)); // 使用手机号后4位作为昵称 + newUser.setPassword(SystemConstants.DEFAULT_PASSWORD); // 使用加密的openId作为初始密码 + newUser.setGender(0); // 保密 + newUser.setCreateTime(LocalDateTime.now()); + newUser.setUpdateTime(LocalDateTime.now()); + this.save(newUser); + // 为了默认系统管理员角色,这里按需调整,实际情况绑定已存在的系统用户,另一种情况是给默认游客角色,然后由系统管理员设置用户的角色 + UserRole userRole = new UserRole(); + userRole.setUserId(newUser.getId()); + userRole.setRoleId(1L); // TODO 系统管理员 + userRoleService.save(userRole); + return true; + } + + /** + * 绑定用户微信OpenID + * + * @param userId 用户ID + * @param openId 微信OpenID + * @return 是否成功 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public boolean bindUserOpenId(Long userId, String openId) { + if (userId == null || StrUtil.isBlank(openId)) { + return false; + } + + // 检查是否已有其他用户绑定了此openId + User existingUser = this.getOne( + new LambdaQueryWrapper() + .eq(User::getOpenid, openId) + .ne(User::getId, userId) + ); + + if (existingUser != null) { + log.warn("OpenID {} 已被用户 {} 绑定,无法为用户 {} 绑定", openId, existingUser.getId(), userId); + return false; + } + + // 更新用户openId + boolean updated = this.update( + new LambdaUpdateWrapper() + .eq(User::getId, userId) + .set(User::getOpenid, openId) + .set(User::getUpdateTime, LocalDateTime.now()) + ); + return updated ; } /** @@ -398,6 +510,11 @@ public class UserServiceImpl extends ServiceImpl implements Us throw new BusinessException("新密码不能与原密码相同"); } + // 判断新密码和确认密码是否一致 + if (passwordEncoder.matches(data.getNewPassword(), data.getConfirmPassword())) { + throw new BusinessException("新密码和确认密码不一致"); + } + String newPassword = data.getNewPassword(); boolean result = this.update(new LambdaUpdateWrapper() .eq(User::getId, userId) @@ -531,7 +648,7 @@ public class UserServiceImpl extends ServiceImpl implements Us // 获取缓存的验证码 String email = form.getEmail(); - String redisCacheKey = RedisConstants.Captcha.EMAIL_CODE + email; + String redisCacheKey = StrUtil.format(RedisConstants.Captcha.EMAIL_CODE, email); String cachedVerifyCode = redisTemplate.opsForValue().get(redisCacheKey); if (StrUtil.isBlank(cachedVerifyCode)) { diff --git a/src/main/java/com/youlai/boot/system/service/impl/WebSocketServiceImpl.java b/src/main/java/com/youlai/boot/system/service/impl/WebSocketServiceImpl.java new file mode 100644 index 00000000..d2fedfed --- /dev/null +++ b/src/main/java/com/youlai/boot/system/service/impl/WebSocketServiceImpl.java @@ -0,0 +1,415 @@ +package com.youlai.boot.system.service.impl; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.youlai.boot.system.model.dto.DictEventDTO; +import com.youlai.boot.system.service.WebSocketService; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * WebSocket 服务实现类 + * + * 核心功能: + * - 用户在线状态管理(支持多设备登录) + * - 消息推送(广播、点对点) + * - 字典变更通知 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Service +@Slf4j +public class WebSocketServiceImpl implements WebSocketService { + + // ==================== 在线用户管理 ==================== + + /** + * 用户在线会话映射表 + * Key: 用户名 + * Value: 该用户的所有会话 ID 集合(支持多设备登录) + */ + private final Map> userSessionsMap = new ConcurrentHashMap<>(); + + /** + * 会话详情映射表 + * Key: 会话 ID + * Value: 会话详细信息 + */ + private final Map sessionDetailsMap = new ConcurrentHashMap<>(); + + // ==================== 依赖注入 ==================== + + private SimpMessagingTemplate messagingTemplate; + private final ObjectMapper objectMapper; + + @Autowired + public WebSocketServiceImpl(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + /** + * 延迟注入 SimpMessagingTemplate,避免循环依赖 + */ + @Autowired(required = false) + public void setMessagingTemplate(SimpMessagingTemplate messagingTemplate) { + this.messagingTemplate = messagingTemplate; + log.info("✓ WebSocket 消息模板已初始化"); + } + + // ==================== 用户在线状态管理 ==================== + + /** + * 处理用户连接事件 + * + * @param username 用户名 + * @param sessionId WebSocket 会话 ID + */ + @Override + public void userConnected(String username, String sessionId) { + if (username == null || username.isEmpty()) { + log.warn("用户连接失败:用户名为空"); + return; + } + + if (sessionId == null || sessionId.isEmpty()) { + log.warn("用户[{}]连接失败:会话 ID 为空", username); + return; + } + + // 添加会话到用户的会话集合中(支持多设备登录) + userSessionsMap.computeIfAbsent(username, k -> ConcurrentHashMap.newKeySet()) + .add(sessionId); + + // 保存会话详情 + SessionInfo sessionInfo = new SessionInfo(username, sessionId, System.currentTimeMillis()); + sessionDetailsMap.put(sessionId, sessionInfo); + + int sessionCount = userSessionsMap.get(username).size(); + int totalOnlineUsers = userSessionsMap.size(); + + log.info("✓ 用户[{}]会话[{}]上线(该用户共 {} 个会话,系统总在线用户数:{})", + username, sessionId, sessionCount, totalOnlineUsers); + + // 广播在线用户数变更 + broadcastOnlineUserCount(); + } + + /** + * 处理用户断开连接事件 + * + * @param username 用户名 + */ + @Override + public void userDisconnected(String username) { + if (username == null || username.isEmpty()) { + return; + } + + // 获取该用户的所有会话 + Set sessions = userSessionsMap.get(username); + if (sessions == null || sessions.isEmpty()) { + log.warn("用户[{}]下线:未找到会话记录", username); + return; + } + + // 移除所有会话详情(通常一次只断开一个会话,但这里做全量清理) + sessions.forEach(sessionDetailsMap::remove); + + // 移除用户的会话记录 + userSessionsMap.remove(username); + + int totalOnlineUsers = userSessionsMap.size(); + log.info("✓ 用户[{}]下线(系统总在线用户数:{})", username, totalOnlineUsers); + + // 广播在线用户数变更 + broadcastOnlineUserCount(); + } + + /** + * 移除指定会话(单个设备下线) + * + * @param sessionId 会话 ID + */ + public void removeSession(String sessionId) { + SessionInfo sessionInfo = sessionDetailsMap.remove(sessionId); + if (sessionInfo == null) { + return; + } + + String username = sessionInfo.getUsername(); + Set sessions = userSessionsMap.get(username); + + if (sessions != null) { + sessions.remove(sessionId); + + // 如果该用户没有其他会话了,移除用户记录 + if (sessions.isEmpty()) { + userSessionsMap.remove(username); + log.info("✓ 用户[{}]最后一个会话[{}]下线", username, sessionId); + } else { + log.info("✓ 用户[{}]会话[{}]下线(还剩 {} 个会话)", + username, sessionId, sessions.size()); + } + + // 广播在线用户数变更 + broadcastOnlineUserCount(); + } + } + + /** + * 获取在线用户列表 + * + * @return 在线用户信息列表 + */ + public List getOnlineUsers() { + return userSessionsMap.entrySet().stream() + .map(entry -> { + String username = entry.getKey(); + Set sessions = entry.getValue(); + + // 获取该用户最早的登录时间 + long earliestLoginTime = sessions.stream() + .map(sessionDetailsMap::get) + .filter(info -> info != null) + .mapToLong(SessionInfo::getConnectTime) + .min() + .orElse(System.currentTimeMillis()); + + return new OnlineUserDTO(username, sessions.size(), earliestLoginTime); + }) + .collect(Collectors.toList()); + } + + /** + * 获取在线用户数量 + * + * @return 在线用户数(不是会话数) + */ + public int getOnlineUserCount() { + return userSessionsMap.size(); + } + + /** + * 获取在线会话总数 + * + * @return 所有在线会话的总数 + */ + public int getTotalSessionCount() { + return sessionDetailsMap.size(); + } + + /** + * 检查用户是否在线 + * + * @param username 用户名 + * @return 是否在线 + */ + public boolean isUserOnline(String username) { + Set sessions = userSessionsMap.get(username); + return sessions != null && !sessions.isEmpty(); + } + + /** + * 获取指定用户的会话数量 + * + * @param username 用户名 + * @return 会话数量 + */ + public int getUserSessionCount(String username) { + Set sessions = userSessionsMap.get(username); + return sessions != null ? sessions.size() : 0; + } + + /** + * 手动触发在线用户数量广播 + * + * 供外部服务(如定时任务)调用 + */ + public void notifyOnlineUsersChange() { + log.info("手动触发在线用户数量通知,当前在线用户数:{}", getOnlineUserCount()); + broadcastOnlineUserCount(); + } + + /** + * 广播在线用户数量变更(内部方法) + */ + private void broadcastOnlineUserCount() { + if (messagingTemplate == null) { + log.warn("消息模板尚未初始化,无法发送在线用户数量"); + return; + } + + try { + int count = getOnlineUserCount(); + messagingTemplate.convertAndSend("/topic/online-count", count); + log.debug("✓ 已广播在线用户数量: {}", count); + } catch (Exception e) { + log.error("广播在线用户数量失败", e); + } + } + + // ==================== 消息推送功能 ==================== + + /** + * 向所有客户端广播字典更新事件 + * + * @param dictCode 字典编码 + */ + @Override + public void broadcastDictChange(String dictCode) { + if (dictCode == null || dictCode.isEmpty()) { + log.warn("字典编码为空,跳过广播"); + return; + } + + DictEventDTO event = new DictEventDTO(dictCode); + sendDictChangeEvent(event); + } + + /** + * 发送字典变更事件 + * + * @param event 字典事件 + */ + private void sendDictChangeEvent(DictEventDTO event) { + if (messagingTemplate == null) { + log.warn("消息模板尚未初始化,无法发送字典更新通知"); + return; + } + + try { + String message = objectMapper.writeValueAsString(event); + messagingTemplate.convertAndSend("/topic/dict", message); + log.info("✓ 已广播字典变更通知: dictCode={}", event.getDictCode()); + } catch (JsonProcessingException e) { + log.error("字典事件序列化失败: dictCode={}", event.getDictCode(), e); + } catch (Exception e) { + log.error("发送字典变更通知失败: dictCode={}", event.getDictCode(), e); + } + } + + /** + * 向特定用户发送通知消息 + * + * @param username 目标用户名 + * @param message 消息内容 + */ + @Override + public void sendNotification(String username, Object message) { + if (username == null || username.isEmpty()) { + log.warn("用户名为空,无法发送通知"); + return; + } + + if (message == null) { + log.warn("消息内容为空,无法发送给用户[{}]", username); + return; + } + + if (messagingTemplate == null) { + log.warn("消息模板尚未初始化,无法发送用户消息"); + return; + } + + try { + String messageJson = objectMapper.writeValueAsString(message); + messagingTemplate.convertAndSendToUser(username, "/queue/messages", messageJson); + log.info("✓ 已向用户[{}]发送通知", username); + } catch (JsonProcessingException e) { + log.error("消息序列化失败: username={}", username, e); + } catch (Exception e) { + log.error("向用户[{}]发送通知失败", username, e); + } + } + + /** + * 广播系统消息给所有用户 + * + * @param message 消息内容 + */ + public void broadcastSystemMessage(String message) { + if (message == null || message.isEmpty()) { + log.warn("消息内容为空,无法广播"); + return; + } + + if (messagingTemplate == null) { + log.warn("消息模板尚未初始化,无法发送广播消息"); + return; + } + + try { + SystemMessage systemMessage = new SystemMessage( + "系统通知", + message, + System.currentTimeMillis() + ); + String messageJson = objectMapper.writeValueAsString(systemMessage); + messagingTemplate.convertAndSend("/topic/public", messageJson); + log.info("✓ 已广播系统消息: {}", message); + } catch (JsonProcessingException e) { + log.error("系统消息序列化失败", e); + } catch (Exception e) { + log.error("广播系统消息失败", e); + } + } + + // ==================== 内部数据类 ==================== + + /** + * 会话信息 + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + private static class SessionInfo { + /** 用户名 */ + private String username; + /** 会话 ID */ + private String sessionId; + /** 连接时间戳 */ + private long connectTime; + } + + /** + * 在线用户 DTO + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class OnlineUserDTO { + /** 用户名 */ + private String username; + /** 会话数量 */ + private int sessionCount; + /** 首次登录时间 */ + private long loginTime; + } + + /** + * 系统消息 + */ + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class SystemMessage { + /** 发送者 */ + private String sender; + /** 消息内容 */ + private String content; + /** 时间戳 */ + private long timestamp; + } +} diff --git a/src/main/java/com/youlai/boot/system/service/impl/WebSocketSessionCleanupService.java b/src/main/java/com/youlai/boot/system/service/impl/WebSocketSessionCleanupService.java new file mode 100644 index 00000000..f1f2042b --- /dev/null +++ b/src/main/java/com/youlai/boot/system/service/impl/WebSocketSessionCleanupService.java @@ -0,0 +1,94 @@ +package com.youlai.boot.system.service.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +/** + * WebSocket 会话清理服务 + * + * 功能: + * - 定时清理僵尸会话 + * - 监控会话状态 + * - 输出统计信息 + * + * @author Ray.Hao + * @since 3.0.0 + */ +@Service +@Slf4j +@RequiredArgsConstructor +@ConditionalOnProperty( + prefix = "websocket.session-cleanup", + name = "enabled", + havingValue = "true", + matchIfMissing = true +) +public class WebSocketSessionCleanupService { + + private final WebSocketServiceImpl webSocketService; + + /** + * 定时输出 WebSocket 会话统计信息 + * + * 每 5 分钟执行一次 + */ + @Scheduled(fixedRate = 300000, initialDelay = 60000) + public void logSessionStatistics() { + try { + int onlineUserCount = webSocketService.getOnlineUserCount(); + int totalSessionCount = webSocketService.getTotalSessionCount(); + + log.info("📊 WebSocket 统计 - 在线用户数: {}, 活跃会话数: {}", + onlineUserCount, totalSessionCount); + + // 详细信息(仅在有用户在线时输出) + if (onlineUserCount > 0) { + var onlineUsers = webSocketService.getOnlineUsers(); + onlineUsers.forEach(user -> { + log.debug(" - 用户[{}]: {} 个会话", user.getUsername(), user.getSessionCount()); + }); + } + } catch (Exception ex) { + log.error("❌ 输出会话统计信息失败", ex); + } + } + + /** + * 健康检查 + * + * 每 30 秒执行一次,用于监控服务状态 + */ + @Scheduled(fixedRate = 30000, initialDelay = 10000) + public void healthCheck() { + try { + int onlineUserCount = webSocketService.getOnlineUserCount(); + int sessionCount = webSocketService.getTotalSessionCount(); + + // 异常检测:如果会话数远大于用户数,可能存在会话泄漏 + if (sessionCount > onlineUserCount * 10 && onlineUserCount > 0) { + log.warn("⚠ 检测到异常:会话数({})远大于用户数({}×10),可能存在会话泄漏", + sessionCount, onlineUserCount); + } + } catch (Exception ex) { + log.error("❌ 健康检查失败", ex); + } + } + + /** + * 手动触发在线用户数广播 + * + * 可用于系统启动后的初始化或手动刷新 + */ + public void triggerOnlineCountBroadcast() { + try { + webSocketService.notifyOnlineUsersChange(); + log.info("✓ 手动触发在线用户数广播成功"); + } catch (Exception ex) { + log.error("❌ 手动触发在线用户数广播失败", ex); + } + } +} + diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 125adea9..da7979d9 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -6,8 +6,8 @@ spring: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://www.youlai.tech:3306/youlai_boot?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true - username: youlai - password: 123456 + username: root + password: Youlai@2025 data: redis: database: 0 @@ -74,7 +74,7 @@ mybatis-plus: security: session: type: jwt # 会话方式 jwt/redis-token - access-token-time-to-live: 7200 # 访问令牌 有效期(单位:秒),默认 2 小时,-1 表示永不过期 + access-token-time-to-live: 7200 # 访问令牌 有效期(单位:秒),默认 2 小时,-1 表示永不过期 refresh-token-time-to-live: 604800 # 刷新令牌有效期(单位:秒),默认 7 天,-1 表示永不过期 jwt: secret-key: SecretKey012345678901234567890123456789012345678901234567890123456789 # JWT密钥(HS256算法至少32字符) @@ -82,11 +82,12 @@ security: allow-multi-login: true # 是否允许多设备登录 # 安全白名单路径,仅跳过 AuthorizationFilter 过滤器,还是会走 Spring Security 的其他过滤器(CSRF、CORS等) ignore-urls: - - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录) - - /api/v1/auth/captcha # 验证码获取接口 - - /api/v1/auth/refresh-token # 刷新令牌接口 - - /api/v1/auth/logout # 开放退出登录 - - /ws/** # WebSocket接口 + - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录) + - /api/v1/auth/captcha # 验证码获取接口 + - /api/v1/auth/refresh-token # 刷新令牌接口 + - /api/v1/auth/logout # 开放退出登录 + - /api/v1/auth/wx/miniapp/code-login # 微信小程序code登陆 + - /ws/** # WebSocket接口 # 非安全端点路径,完全绕过 Spring Security 的安全控制 unsecured-urls: - ${springdoc.swagger-ui.path} @@ -152,21 +153,21 @@ springdoc: api-docs: path: /v3/api-docs group-configs: - - group: '系统管理' + - group: "系统管理" paths-to-match: "/**" packages-to-scan: + - com.youlai.boot.auth.controller - com.youlai.boot.system.controller - - com.youlai.boot.shared.auth.controller - - com.youlai.boot.shared.file.controller - - com.youlai.boot.shared.codegen.controller + - com.youlai.boot.platform.file.controller + - com.youlai.boot.platform.codegen.controller default-flat-param-object: true # knife4j 接口文档配置 knife4j: # 是否开启 Knife4j 增强功能 - enable: true # 设置为 true 表示开启增强功能 + enable: true # 设置为 true 表示开启增强功能 # 生产环境配置 - production: false # 设置为 true 表示在生产环境中不显示文档,为 false 表示显示文档(通常在开发环境中使用) + production: false # 设置为 true 表示在生产环境中不显示文档,为 false 表示显示文档(通常在开发环境中使用) setting: language: zh_cn @@ -222,3 +223,66 @@ wx: miniapp: app-id: xxxxxx app-secret: xxxxxx + +# ==================== AI 命令系统配置 ==================== +ai: + # 是否启用 AI 功能 + enabled: false + + # 当前使用的提供商:qwen、deepseek、openai + provider: qwen + + # 所有提供商配置(统一管理,扩展性强) + providers: + # 阿里通义千问(推荐:有免费额度) + qwen: + # API Key(https://bailian.console.aliyun.com/ 获取) + api-key: ${QWEN_API_KEY:sk-c2941d05bf2f411ca80424fcxxxxxxxx} + + # Base URL(OpenAI 兼容端点) + base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 + + # 模型:qwen-plus(推荐)、qwen-turbo、qwen-max、qwen-long + model: qwen-plus + + # 显示名称 + display-name: 阿里通义千问 + + # 超时时间(秒) + timeout: 30 + + # DeepSeek + deepseek: + api-key: ${DEEPSEEK_API_KEY:} + base-url: https://api.deepseek.com/v1 + model: deepseek-chat + display-name: DeepSeek + timeout: 30 + + # OpenAI(添加新提供商只需配置,无需修改代码) + openai: + api-key: ${OPENAI_API_KEY:} + base-url: https://api.openai.com/v1 + model: gpt-4 + display-name: OpenAI GPT-4 + timeout: 60 + + # 安全配置 + security: + enable-audit: true + dangerous-operations-confirm: true + function-whitelist: + - deleteUser + - updateUser + - queryUsers + - assignRole + sensitive-params: + - password + - idCard + - bankCard + - token + + # 限流配置 + rate-limit: + max-executions-per-minute: 10 + max-executions-per-day: 100 diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index c43b5e7f..def022f8 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -83,6 +83,7 @@ security: - /api/v1/auth/login/** # 登录接口(账号密码登录、手机验证码登录和微信登录) - /api/v1/auth/captcha # 验证码获取接口 - /api/v1/auth/refresh-token # 刷新令牌接口 + - //api/v1/auth/wx/miniapp/code-login # 微信小程序code登陆 - /ws/** # WebSocket接口 # 非安全端点路径,完全绕过 Spring Security 的过滤器 unsecured-urls: @@ -152,10 +153,10 @@ springdoc: - group: '系统管理' paths-to-match: "/**" packages-to-scan: + - com.youlai.boot.auth.controller - com.youlai.boot.system.controller - - com.youlai.boot.shared.auth.controller - - com.youlai.boot.shared.file.controller - - com.youlai.boot.shared.codegen.controller + - com.youlai.boot.platform.file.controller + - com.youlai.boot.platform.codegen.controller default-flat-param-object: true # knife4j 接口文档配置 @@ -217,4 +218,68 @@ captcha: wx: miniapp: app-id: xxxxxx - app-secret: xxxxxx \ No newline at end of file + app-secret: xxxxxx + + +# ==================== AI 命令系统配置 ==================== +ai: + # 是否启用 AI 功能 + enabled: false + + # 当前使用的提供商:qwen、deepseek、openai + provider: qwen + + # 所有提供商配置(统一管理,扩展性强) + providers: + # 阿里通义千问(推荐:有免费额度) + qwen: + # API Key(https://bailian.console.aliyun.com/ 获取) + api-key: ${QWEN_API_KEY:sk-c2941d05bf2f411ca80424fcxxxxxxxx} + + # Base URL(OpenAI 兼容端点) + base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 + + # 模型:qwen-plus(推荐)、qwen-turbo、qwen-max、qwen-long + model: qwen-plus + + # 显示名称 + display-name: 阿里通义千问 + + # 超时时间(秒) + timeout: 30 + + # DeepSeek + deepseek: + api-key: ${DEEPSEEK_API_KEY:} + base-url: https://api.deepseek.com/v1 + model: deepseek-chat + display-name: DeepSeek + timeout: 30 + + # OpenAI(添加新提供商只需配置,无需修改代码) + openai: + api-key: ${OPENAI_API_KEY:} + base-url: https://api.openai.com/v1 + model: gpt-4 + display-name: OpenAI GPT-4 + timeout: 60 + + # 安全配置 + security: + enable-audit: true + dangerous-operations-confirm: true + function-whitelist: + - deleteUser + - updateUser + - queryUsers + - assignRole + sensitive-params: + - password + - idCard + - bankCard + - token + + # 限流配置 + rate-limit: + max-executions-per-minute: 10 + max-executions-per-day: 100 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 46d81bf5..d1ce1eb8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,7 +5,10 @@ spring: active: dev config: import: classpath:codegen.yml - + servlet: + multipart: + max-file-size: 50MB + max-request-size: 50MB # 在 banner.txt 中显示项目版本,使用 @project.version@ 从 pom.xml 获取 project: version: @project.version@ diff --git a/src/main/resources/mapper/codegen/DatabaseMapper.xml b/src/main/resources/mapper/codegen/DatabaseMapper.xml index 1fd0376a..d923000c 100644 --- a/src/main/resources/mapper/codegen/DatabaseMapper.xml +++ b/src/main/resources/mapper/codegen/DatabaseMapper.xml @@ -2,10 +2,10 @@ - + - SELECT t1.TABLE_NAME , t1.TABLE_COMMENT , @@ -33,7 +33,7 @@ CREATE_TIME DESC - SELECT TABLE_NAME , TABLE_COMMENT , @@ -47,7 +47,7 @@ AND TABLE_NAME = #{tableName} - SELECT COLUMN_NAME, DATA_TYPE, diff --git a/src/main/resources/mapper/codegen/GenConfigMapper.xml b/src/main/resources/mapper/codegen/GenConfigMapper.xml index 996587bd..02709a91 100644 --- a/src/main/resources/mapper/codegen/GenConfigMapper.xml +++ b/src/main/resources/mapper/codegen/GenConfigMapper.xml @@ -2,6 +2,6 @@ - + diff --git a/src/main/resources/mapper/codegen/GenFieldConfigMapper.xml b/src/main/resources/mapper/codegen/GenFieldConfigMapper.xml index 287e9634..0bd4db31 100644 --- a/src/main/resources/mapper/codegen/GenFieldConfigMapper.xml +++ b/src/main/resources/mapper/codegen/GenFieldConfigMapper.xml @@ -2,6 +2,6 @@ - + diff --git a/src/main/resources/mapper/system/LogMapper.xml b/src/main/resources/mapper/system/LogMapper.xml index 9169981b..29d8c504 100644 --- a/src/main/resources/mapper/system/LogMapper.xml +++ b/src/main/resources/mapper/system/LogMapper.xml @@ -55,7 +55,8 @@ FROM sys_log WHERE - is_deleted = 0 + create_time BETWEEN #{startDate} AND #{endDate} + AND is_deleted = 0 GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') @@ -68,7 +69,8 @@ FROM sys_log WHERE - is_deleted = 0 + create_time BETWEEN #{startDate} AND #{endDate} + AND is_deleted = 0 GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') diff --git a/src/main/resources/mapper/system/RoleMenuMapper.xml b/src/main/resources/mapper/system/RoleMenuMapper.xml index c6654581..11fc527d 100644 --- a/src/main/resources/mapper/system/RoleMenuMapper.xml +++ b/src/main/resources/mapper/system/RoleMenuMapper.xml @@ -33,7 +33,7 @@ INNER JOIN sys_role t2 ON t1.role_id = t2.id AND t2.is_deleted = 0 AND t2.`status` = 1 INNER JOIN sys_menu t3 ON t1.menu_id = t3.id WHERE - type = '${@com.youlai.boot.system.enums.MenuTypeEnum@BUTTON.getValue()}' + t3.type = '${@com.youlai.boot.system.enums.MenuTypeEnum@BUTTON.getValue()}' AND t2.`code` = #{roleCode} diff --git a/src/main/resources/mapper/system/UserMapper.xml b/src/main/resources/mapper/system/UserMapper.xml index 751147da..4e281a17 100644 --- a/src/main/resources/mapper/system/UserMapper.xml +++ b/src/main/resources/mapper/system/UserMapper.xml @@ -119,7 +119,7 @@ - + diff --git a/src/main/resources/mapper/system/UserNoticeMapper.xml b/src/main/resources/mapper/system/UserNoticeMapper.xml index 46332903..c3364cb0 100644 --- a/src/main/resources/mapper/system/UserNoticeMapper.xml +++ b/src/main/resources/mapper/system/UserNoticeMapper.xml @@ -21,6 +21,9 @@ AND t2.title LIKE CONCAT('%',#{queryParams.title},'%') + + AND t1.is_read = #{queryParams.isRead} + ORDER BY t2.publish_time DESC, t2.create_time DESC diff --git a/src/main/resources/templates/codegen/api.ts.vm b/src/main/resources/templates/codegen/api.ts.vm index afddcc9c..a80445e8 100644 --- a/src/main/resources/templates/codegen/api.ts.vm +++ b/src/main/resources/templates/codegen/api.ts.vm @@ -29,11 +29,11 @@ const ${entityName}API = { * * @param data ${businessName}表单数据 */ - add(data: ${entityName}Form) { + create(data: ${entityName}Form) { return request({ url: `${${entityName.toUpperCase()}_BASE_URL}`, method: "post", - data: data, + data, }); }, @@ -43,11 +43,11 @@ const ${entityName}API = { * @param id ${businessName}ID * @param data ${businessName}表单数据 */ - update(id: number, data: ${entityName}Form) { + update(id: string, data: ${entityName}Form) { return request({ url: `${${entityName.toUpperCase()}_BASE_URL}/${id}`, method: "put", - data: data, + data, }); }, diff --git a/src/main/resources/templates/codegen/controller.java.vm b/src/main/resources/templates/codegen/controller.java.vm index 21c3b718..bdadcc47 100644 --- a/src/main/resources/templates/codegen/controller.java.vm +++ b/src/main/resources/templates/codegen/controller.java.vm @@ -8,8 +8,8 @@ import ${packageName}.${moduleName}.model.form.${entityName}Form; import ${packageName}.${moduleName}.model.query.${entityName}Query; import ${packageName}.${moduleName}.model.vo.${entityName}VO; import com.baomidou.mybatisplus.core.metadata.IPage; -import com.youlai.boot.common.result.PageResult; -import com.youlai.boot.common.result.Result; +import com.youlai.boot.core.web.PageResult; +import com.youlai.boot.core.web.Result; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Operation; diff --git a/src/main/resources/templates/codegen/index.curd.vue.vm b/src/main/resources/templates/codegen/index.curd.vue.vm new file mode 100644 index 00000000..193d106d --- /dev/null +++ b/src/main/resources/templates/codegen/index.curd.vue.vm @@ -0,0 +1,370 @@ + + + diff --git a/src/main/resources/templates/codegen/index.vue.vm b/src/main/resources/templates/codegen/index.vue.vm index b7c4021a..a398e4c3 100644 --- a/src/main/resources/templates/codegen/index.vue.vm +++ b/src/main/resources/templates/codegen/index.vue.vm @@ -1,10 +1,10 @@