91 Commits

Author SHA1 Message Date
Ray.Hao
690265b177 build(pom): 升级项目版本并更新 Spring Boot 依赖- 将项目版本从 3.1.0 修改为 3.2.0
- 将 Spring Boot 的版本从 3.5.0 升级到3.5.5
2025-09-17 15:32:34 +08:00
Ray.Hao
62ca74c66a fix(codegen): 修复 CURD 模板已知问题 2025-09-17 15:09:00 +08:00
theo
3ecb28549f feat(dict): 添加事务支持并在更新时更新相关的字典项 2025-09-10 10:58:46 +08:00
theo
a2633b67c9 refactor(menu): 调整多级菜单结构和布局
- 将菜单一级和菜单二级的类型从 1 修改为 2,表示它们是目录而不是菜单- 修改菜单一级和菜单二级的组件为 Layout,以适应多级菜单的布局结构
- 保持其他菜单项不变
2025-09-04 17:11:38 +08:00
Ray.Hao
abf7f42773 build(docker): 更新 MinIO镜像版本 2025-09-01 13:42:50 +08:00
Ray.Hao
4473e721f8 fix: curd 代码生成模板与前端不适配问题修复 2025-09-01 13:42:28 +08:00
Ray.Hao
9152ab654d feat(code): 代码生成新增 curd 页面模板和优化 2025-08-21 18:09:07 +08:00
Ray.Hao
2accc6a489 chore: SQL 脚本新增菜单和权限过渡 2025-08-12 17:30:49 +08:00
Theo
1c14e278d5 !38 update src/main/resources/application-dev.yml.
Merge pull request !38 from superboer/N/A
2025-08-11 01:55:45 +00:00
Theo
2c5141bad6 !39 update src/main/resources/application-prod.yml.
Merge pull request !39 from superboer/N/A
2025-08-11 01:55:30 +00:00
Theo
cf09b39fa0 feat(config): 设置文件上传最大尺寸
- 在 application.yml 中添加 servlet 配置
- 设置最大文件大小为 50MB
- 设置最大请求大小为 50MB
2025-07-16 16:49:24 +08:00
Theo
5646f46c58 refactor(security): 更新 Token相关术语和注释
- 优化部分方法和参数的命名
2025-07-10 15:28:49 +08:00
Theo
9197065102 feat(system): 添加菜单、角色和用户相关操作的权限控制
- 在 MenuController、RoleController 和 UserController 中添加了 @PreAuthorize 注解
- 新增了对菜单编辑、角色编辑、用户导入和用户导出的权限控制
2025-07-02 09:05:57 +08:00
superboer
3e71dd7cae update src/main/resources/application-prod.yml.
微信小程序登陆接口 暴露

Signed-off-by: superboer <25535518@qq.com>
2025-06-28 03:34:34 +00:00
superboer
1a032365c1 update src/main/resources/application-dev.yml.
小程序接口暴露。

Signed-off-by: superboer <25535518@qq.com>
2025-06-28 03:32:56 +00:00
Ray.Hao
42e5499119 !37 解决RequestContextHolder.getRequestAttributes()为空导致异常的问题
Merge pull request !37 from 太空眼睛/tkyj-jwt
2025-06-26 08:20:17 +00:00
太空眼睛
567446d967 Merge branch 'master' into tkyj-jwt 2025-06-26 16:11:40 +08:00
太空眼睛
18d6a63b18 解决RequestContextHolder.getRequestAttributes()为空导致异常的问题 2025-06-26 16:11:35 +08:00
Ray.Hao
c01e54b9e3 !36 解决token为null时抛空指针异常的问题
Merge pull request !36 from 太空眼睛/tkyj-jwt
2025-06-25 07:23:41 +00:00
太空眼睛
a10e12f753 Merge branch 'master' of gitee.com:youlaiorg/youlai-boot into tkyj-jwt
Signed-off-by: 太空眼睛 <best5721@sina.com>
2025-06-25 07:21:52 +00:00
tangheng
7ffad2d71c 解决token为null时抛空指针异常的问题 2025-06-25 15:14:53 +08:00
yms
86a9b3e212 修复普通用户或其他权限用户能在swagger下更改系统管理员角色状态,获取用户表单数据,更改菜单显示状态的安全漏洞 2025-06-18 17:24:15 +08:00
yms
48ec38e076 修复普通用户或其他权限用户能在swagger下更改系统管理员角色状态的安全漏洞 2025-06-18 17:01:08 +08:00
Ray.Hao
02f835e59e build(Dockerfile): 更新 Docker 的JDK镜像构建适配 SpringBoot 3.5.0
Closes #ICF6LW
2025-06-14 21:04:48 +08:00
Theo
2af4581f2d feat(auth): 改进刷新令牌机制
- 在 JWT 中添加 tokenType 字段,用于区分访问令牌和刷新令牌
- 重新实现刷新令牌验证逻辑,增加 tokenType 校验
- 优化 refreshToken 方法,直接使用 validateRefreshToken进行验证
- 移除不必要的代码,提高代码可读性和维护性
2025-06-13 17:15:31 +08:00
Theo
7ecf34cf43 refactor(sql): 调整用户信息表索引并优化 SQL 脚本
- 将用户信息表中的 UNIQUE INDEX `login_name` 修改为非唯一索引 KEY `login_name`
2025-06-13 16:09:23 +08:00
Ray.Hao
395c0f8bfa chore: bump version to 3.0.0 2025-06-11 20:49:15 +08:00
Ray.Hao
af8b5c847d refactor: 升级 SpringBoot 3.5.0 版本和代码结构优化 2025-06-11 20:47:02 +08:00
坏小村
4c492784f3 !32 fix:在个人中心修改密码确认密码字段不存在
Merge pull request !32 from end/master
2025-06-06 08:45:43 +00:00
Ray.Hao
d257cb6b7d !35 fix: 修复sys_menu菜单表当数据库表名带有下滑线时的【按钮】权限标识列字段生成问题
Merge pull request !35 from 坏小村/master
2025-06-04 14:08:38 +00:00
huaixiaocun
a4c2048fef fix: 修复sys_menu菜单表当数据库表名带有下滑线时的【按钮】权限标识列字段生成问题 2025-06-04 21:56:33 +08:00
Theo
13ac1dac04 !34 fix: 查询访问量和IP日统计列表时根据传入的时间范围查询
Merge pull request !34 from 坏小村/master
2025-06-04 09:20:38 +00:00
huaixiaocun
b7c4d861f8 fix: 查询访问量和IP日统计列表时根据传入的时间范围查询 2025-06-03 23:20:40 +08:00
Ray.Hao
f62dcb57e3 fix: 已知问题修复 2025-06-01 18:20:20 +08:00
Ray.Hao
194f3e7ca8 refactor: 微信小程序授权登录重构 2025-06-01 17:32:46 +08:00
Ray.Hao
be9faa2445 wip: 微信登录重构临时提交 2025-05-30 00:01:07 +08:00
Ray.Hao
7d5b7f0a63 fix: 用户新增添加创建人 2025-05-28 23:28:42 +08:00
Ray.Hao
8934d2da99 refactor: 优化微信小程序授权登录命名 2025-05-28 23:09:30 +08:00
Ray.Hao
1ff53a1c96 fix: search-bar → search-container 2025-05-28 18:27:30 +08:00
Ray.Hao
8f9d828205 fix: 用户角色变化强制用户退出 2025-05-27 18:29:44 +08:00
Ray.Hao
3d3e7f8c92 Merge branch 'master' of https://gitee.com/youlaiorg/youlai-boot 2025-05-21 15:45:35 +08:00
Ray.Hao
ecc5f6f780 docs: 完善注释 2025-05-21 15:44:52 +08:00
Ray.Hao
ea7eba05e1 refactor: 优化无权操作数据库的提示 2025-05-21 15:41:35 +08:00
Ray.Hao
fa86addc49 fix(log): 修复退出登录日志记录无法获取操作人的问题 2025-05-21 15:39:24 +08:00
wx
42e25a0e58 fix:在个人中心修改密码确认密码字段不存在 2025-05-16 16:27:48 +08:00
Ray.Hao
4dfa6ae1d8 wip: 优化临时提交 2025-04-25 17:39:11 +08:00
Ray.Hao
5aff74d36f feat: 字典实时同步和 websocket 重构优化 2025-04-24 23:45:26 +08:00
Ray.Hao
f06fe3ee01 update README.md.
Signed-off-by: Ray.Hao <1490493387@qq.com>
2025-04-20 02:03:05 +00:00
Ray.Hao
819f4ce587 update README.md.
Signed-off-by: Ray.Hao <1490493387@qq.com>
2025-04-17 06:50:40 +00:00
Ray.Hao
44a62c8d33 Merge branch 'master' of github.com:haoxianrui/youlai-boot 2025-04-15 22:30:22 +08:00
Ray Hao
24bb6bc9a3 Merge pull request #11 from slience-me/dev
刷新Token补充更新
2025-04-15 21:57:58 +08:00
slience-me
9bb4b8cb27 刷新Token补充更新 2025-04-15 16:49:50 +08:00
Ray Hao
210c6dee00 Merge pull request #10 from slience-me/dev
刷新Token相关问题反馈
2025-04-15 14:52:52 +08:00
slience-me
f03e6fe98f 刷新Token相关问题反馈
关联Issue9:https://github.com/haoxianrui/youlai-boot/issues/9
2025-04-15 14:47:03 +08:00
Ray.Hao
4499cc03c4 docs: 接口文档描述调整 2025-04-14 17:36:32 +08:00
Ray.Hao
a0ffdb26b8 Merge branch 'master' of https://gitee.com/youlaiorg/youlai-boot 2025-04-11 17:17:29 +08:00
Ray.Hao
cb6b2a4c13 refactor: websocket 解析令牌支持 Redis+Token 会话 2025-04-11 17:16:55 +08:00
Ray.Hao
3ba704171d chore: 完善白名单配置注释 2025-04-11 17:09:23 +08:00
Ray.Hao
94f7a54e5c chore: 删除 xxl-job 的 SQL 脚本 2025-04-11 17:07:59 +08:00
Ray.Hao
42abc4fb6c fix: 修复 Redis 缓存 Key 拼接错误问题 2025-04-09 07:29:42 +08:00
Ray.Hao
86c31498ea chore: JWT 访问令牌和刷新令牌合理调整 2025-04-03 11:22:22 +08:00
Ray.Hao
e7dfdabe3c docs: 文档文案优化 2025-04-02 19:11:45 +08:00
Ray.Hao
a27f7426d9 Merge branch 'master' of https://gitee.com/youlaiorg/youlai-boot 2025-04-02 15:26:06 +08:00
Ray.Hao
a0a11658de docs: 优化排版 2025-04-02 15:25:16 +08:00
Theo
abb67a0bf9 refactor(codegen): 优化代码生成模板
- 将 API路径和权限标识改为使用 kebab-case 风格- 优化了部分代码注释和命名
- 优化代码生成的注释,增加返回值解释
2025-04-02 15:21:55 +08:00
Ray.Hao
ded1a527ea docs: 优化和完善文档 2025-04-02 14:17:44 +08:00
Ray.Hao
dc2016d3c7 docs: 优化和完善文档 2025-04-02 14:15:57 +08:00
Ray.Hao
c12f770c71 docs: 优化和完善文档 2025-04-02 14:14:28 +08:00
Ray.Hao
b57853477d docs: 注释优化 2025-04-02 09:51:53 +08:00
Ray.Hao
4451c170c8 refactor: 获取用户认证凭证信息方法命名合理调整 2025-04-02 09:49:53 +08:00
Ray.Hao
11603bd864 docs: 优化项目介绍、文档地址和加群说明 2025-04-02 09:47:49 +08:00
Ray.Hao
b26accaaa2 fix: 拖拽组件和滚动文本菜单路径错误修正 2025-03-31 17:18:30 +08:00
Ray.Hao
63b48bc225 chore: 添加拖拽和滚动文本组件演示菜单 2025-03-31 14:56:28 +08:00
Ray.Hao
482bd16f62 refactor: 用户导入模板下载使用 try-with-resources 释放资源 2025-03-31 14:54:26 +08:00
Ray.Hao
8cfa734238 fix: 获取当前登录用户信息方法名错误 2025-03-31 11:59:09 +08:00
Ray.Hao
04fba012b5 fix: 合并冲突解决 2025-03-31 08:21:58 +08:00
Ray.Hao
60cfa78a52 refactor: 获取用户认证凭证优化 2025-03-31 08:18:46 +08:00
Ray.Hao
594eb2befb refactor: 枚举值不以0作为值调整 2025-03-31 08:18:00 +08:00
Ray.Hao
594a704df3 feat: 添加 favicon.ico 至忽略认证路径 2025-03-31 08:16:11 +08:00
Theo
354b9f3c49 !29 update src/main/java/com/youlai/boot/system/controller/DictController.java.
Merge pull request !29 from metorplex/N/A
2025-03-26 15:42:55 +00:00
metorplex
f924d32417 update src/main/java/com/youlai/boot/system/controller/DictController.java.
源参数是ids 与@DeleteMapping("/{dictCode}/items/{itemIds}")中参数名itemIds 不一致,提交时会报错

Signed-off-by: metorplex <527073700@qq.com>
2025-03-26 11:09:27 +00:00
Ray.Hao
505dcf1a73 refactor: 获取当前登录用户信息优化 2025-03-25 23:20:00 +08:00
Ray.Hao
3244af424d chore: 依赖顺序优化和完善注释 2025-03-25 23:17:18 +08:00
Ray.Hao
7aef158dca refactor: Update EasyExcel to FastExcel (easyexcel-plus) 2025-03-24 12:52:46 +08:00
Ray.Hao
490a03e85d merge: 合并冲突解决 2025-03-24 10:41:53 +08:00
Ray.Hao
03f383c6bb feat: 添加字典列表接口 2025-03-24 10:32:19 +08:00
Ray.Hao
b05adf6549 refactor: 移除菜单类型枚举编码转换逻辑,直接接受原始值(接口参数标准化) 2025-03-24 10:03:01 +08:00
Ray.Hao
f47c56bef2 chore: bump version to 2.22.0 2025-03-24 07:27:35 +08:00
Ray.Hao
311434b40b chore: 字典表调整和完善菜单数据 2025-03-24 07:27:09 +08:00
Ray.Hao
007d44e4ae style: 代码格式化 2025-03-24 07:25:36 +08:00
Ray.Hao
60f94fdf7f refactor: 字典模块重构 2025-03-24 07:24:32 +08:00
143 changed files with 3037 additions and 1527 deletions

View File

@@ -1,25 +1,23 @@
# 基础镜像
FROM openjdk:17-jdk-alpine
FROM openjdk:17
# 维护者信息
MAINTAINER youlai <youlaitech@163.com>
# 设置国内镜像源(中国科技大学镜像源),修改容器时区(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

View File

@@ -3,11 +3,11 @@
<img alt="logo" width="100" height="100" src="https://foruda.gitee.com/images/1733417239320800627/3c5290fe_716974.png">
<h2>youlai-boot</h2>
<img alt="有来技术" src="https://img.shields.io/badge/Java -17-brightgreen.svg"/>
<img alt="有来技术" src="https://img.shields.io/badge/SpringBoot-3.3.6-green.svg"/>
<img alt="有来技术" src="https://img.shields.io/badge/SpringBoot-3.5.0-green.svg"/>
<a href="https://gitee.com/youlaiorg/youlai-boot" target="_blank">
<img alt="有来技术" src="https://gitee.com/youlaiorg/youlai-boot/badge/star.svg"/>
</a>
<a href="https://github.com/haoxianrui" target="_blank">
<a href="https://github.com/haoxianrui/youlai-boot" target="_blank">
<img alt="有来技术" src="https://img.shields.io/github/stars/haoxianrui/youlai-boot.svg?style=social&label=Stars"/>
</a>
<br/>
@@ -20,12 +20,12 @@
![](https://raw.gitmirror.com/youlaitech/image/main/docs/rainbow.png)
<div align="center">
<a target="_blank" href="https://vue.youlai.tech/">🖥️ 在线预览</a> | <a target="_blank" href="https://youlai.blog.csdn.net/article/details/145178880">📑 阅读文档</a> | <a target="_blank" href="https://www.youlai.tech/">🌐 官网</a>
<a target="_blank" href="https://vue.youlai.tech/">🖥️ 在线预览</a> | <a target="_blank" href="https://youlai.blog.csdn.net/article/details/145178880">📑 阅读文档</a> | <a target="_blank" href="https://www.youlai.tech/youlai-boot">🌐 官网</a>
</div>
## 📢 项目简介
基于 JDK 17、Spring Boot 3、Spring Security 6、JWT、Redis、Mybatis-Plus、Knife4j、Vue 3、Element-Plus 构建的前后端分离单体权限管理系统。
基于 JDK 17、Spring Boot 3、Spring Security 6、JWT、Redis、Mybatis-Plus、Vue 3、Element-Plus 构建的前后端分离单体权限管理系统。 [Mybatis-Flex 版本](https://gitee.com/youlaiorg/youlai-boot-flex)
- **🚀 开发框架**: 使用 Spring Boot 3 和 Vue 3以及 Element-Plus 等主流技术栈,实时更新。
@@ -33,26 +33,26 @@
- **🔑 权限管理**: 基于 RBAC 模型,实现细粒度的权限控制,涵盖接口方法和按钮级别。
- **🛠️ 功能模块**: 包括用户管理、角色管理、菜单管理、部门管理、字典管理等多个功能。
- **🛠️ 功能模块**: 包括用户管理、角色管理、菜单管理、部门管理、字典管理等功能。
## 🌈 项目源码
| 项目类型 | GitCode | Gitee | Github |
|--------|----------------------------------|-----------------------------------------------------------------------|------------------------------------------------------------------------|
| 后端 | [youlai-boot](https://gitcode.com/youlai/youlai-boot) | [youlai-boot](https://gitee.com/youlaiorg/youlai-boot) | [youlai-boot](https://gitee.com/haoxianrui/youlai-boot) |
| 前端 | [vue3-element-admin](https://gitcode.com/youlai/vue3-element-admin) | [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) | [vue3-element-admin](https://github.com/youlaitech/vue3-element-admin) |
| 移动端 | [vue-uniapp-template](https://gitcode.com/youlai/vue-uniapp-template) | [vue-uniapp-template](https://gitee.com/youlaiorg/vue-uniapp-template) | [vue-uniapp-template](https://gitcode.com/youlaitech/vue-uniapp-template) |
| 项目类型 | 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) |
## 📚 文档地址
| 文档类型 | 文档名称 | 地址 |
|----------|-------------|--------------------------------------------------------------------------------------|
| 在线演示 | 项目在线演示 | 🌐 [https://vue.youlai.tech](https://vue.youlai.tech) |
| 接口文档 | Apifox 在线文档 | 📄 [https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5) |
| 官方文档 | 项目文档 | 📚 [ https://www.youlai.tech/youlai-boot](https://www.youlai.tech/youlai-boot/) |
| 系统文档 | 全功能手册 | 📚 [https://youlai.blog.csdn.net/article/details/145178880](https://youlai.blog.csdn.net/article/details/145178880) |
| 系统文档 | 从0到1搭建指南 | 📚 [https://youlai.blog.csdn.net/article/details/145177011](https://youlai.blog.csdn.net/article/details/145177011) |
## 📚 项目文档
| 文档名称 | 访问地址 |
|---------------|-------------------------------------------------------------------------------------------|
| 在线接口文档 | [https://www.apifox.cn/apidoc](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5) |
| 项目介绍与使用指南 | [https://www.youlai.tech/youlai-boot/](https://www.youlai.tech/youlai-boot/) |
| 功能详解与操作手册 | [https://youlai.blog.csdn.net/article/details/145178880](https://youlai.blog.csdn.net/article/details/145178880) |
| 新手入门指南(项目0到1) | [https://youlai.blog.csdn.net/article/details/145177011](https://youlai.blog.csdn.net/article/details/145177011) |
## 📁 项目目录
@@ -70,6 +70,7 @@ youlai-boot
├── sql # SQL脚本
│ ├── mysql # MySQL 脚本
├── src # 源码目录
│ ├── auth # 登录认证
│ ├── common # 公共模块
│ │ ├── annotation # 注解定义
│ │ ├── base # 基础类
@@ -86,12 +87,14 @@ youlai-boot
│ │ ├── filter # 过滤器(请求日志、限流)
│ │ ├── handler # 处理器(数据权限、数据填充)
│ │ └── security # Spring Security 安全模块
│ ├── modules # 业务模块
│ ├── module # 业务模块
│ │ ├── member # 会员模块【业务模块演示】
│ │ ├── order # 订单模块【业务模块演示】
│ │ ├── product # 商品模块【业务模块演示】
│ ├── module # 插件扩展
│ │ ├── knife4j # Knife4j 扩展
│ │ ├── mybatis # Mybatis 扩展
│ ├── shared # 共享模块
│ │ ├── auth # 认证模块
│ │ ├── file # 文件模块
│ │ ├── codegen # 代码生成模块
│ │ ├── mail # 邮件模块
@@ -121,7 +124,7 @@ youlai-boot
## 🚀 项目启动
详细参考官方文档: [项目启动](https://www.youlai.tech/youlai-boot/1.%E9%A1%B9%E7%9B%AE%E5%90%AF%E5%8A%A8/)
📚 完整流程参考: [项目启动](https://www.youlai.tech/youlai-boot/1.%E9%A1%B9%E7%9B%AE%E5%90%AF%E5%8A%A8/)
1. **克隆项目**
@@ -135,7 +138,7 @@ youlai-boot
3. **修改配置**
[application-dev.yml](src/main/resources/application-dev.yml) 修改MySQLRedis连接配置;
默认连接`有来`线上 MySQL/Redis仅读权限本地开发时请修改 [application-dev.yml](src/main/resources/application-dev.yml) 中的 MySQLRedis 连接信息。
4. **启动项目**
@@ -146,25 +149,22 @@ youlai-boot
## 🚀 项目部署
参考官方文档: [项目部署](https://www.youlai.tech/youlai-boot/5.%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B2/)
参考官方文档: [项目部署指南](https://www.youlai.tech/youlai-boot/5.%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B2/)
## ✅ 项目统计
![Alt](https://repobeats.axiom.co/api/embed/544c5c0b5b3611a6c4d5ef0faa243a9066b89659.svg "Repobeats analytics image")
![](https://repobeats.axiom.co/api/embed/544c5c0b5b3611a6c4d5ef0faa243a9066b89659.svg "Repobeats analytics image")
Thanks to all the contributors!
[![contributors](https://contrib.rocks/image?repo=haoxianrui/youlai-boot)](https://github.com/haoxianrui/youlai-boot/graphs/contributors)
[![](https://contrib.rocks/image?repo=haoxianrui/youlai-boot)](https://github.com/haoxianrui/youlai-boot/graphs/contributors)
## 💖 加交流群
> **关注「有来技术」公众号,点击菜单“交流群”获取加群二维码。**
>
> 如果二维码过期,请加微信(haoxianrui)备注「前端」、「后端」或「全栈」拉你进群。
>
> 交流群仅限技术交流,为过滤广告营销暂设此门槛,感谢理解与配合
关注「有来技术」公众号,点击菜单 **交流群** 获取加群二维码(此举防止广告进群,感谢理解和支持)。
![有来技术公众号二维码](https://foruda.gitee.com/images/1737108820762592766/3390ed0d_716974.png)
② 直接添加微信 **`haoxianrui`** 备注「前端/后端/全栈」。
![有来技术公众号](https://foruda.gitee.com/images/1737108820762592766/3390ed0d_716974.png)

View File

@@ -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
- youlai-boot

64
pom.xml
View File

@@ -6,13 +6,13 @@
<groupId>com.youlai</groupId>
<artifactId>youlai-boot</artifactId>
<version>2.21.1</version>
<version>3.2.0</version>
<description>基于 Java 17 + SpringBoot 3 + Spring Security 构建的权限管理系统。</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.6</version> <!-- lookup parent from repository -->
<version>3.5.5</version> <!-- lookup parent from repository -->
<relativePath/>
</parent>
@@ -33,7 +33,7 @@
<xxl-job.version>2.4.2</xxl-job.version>
<easyexcel.version>3.2.1</easyexcel.version>
<fastexcel.version>1.1.0</fastexcel.version>
<!-- 对象存储 -->
<minio.version>8.5.10</minio.version>
@@ -113,6 +113,21 @@
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
@@ -132,12 +147,25 @@
<version>${mybatis-plus.version}</version>
</dependency>
<!-- knife4j 接口文档 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
<exclusions>
<exclusion>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.8.9</version>
</dependency>
<!-- MapStruct 对象映射 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
@@ -150,21 +178,18 @@
<version>${mapstruct.version}</version>
</dependency>
<!-- xxl-job 定时任务 -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>${xxl-job.version}</version>
</dependency>
<!-- Excel 工具EasyExcel-PLus -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${easyexcel.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<groupId>cn.idev.excel</groupId>
<artifactId>fastexcel</artifactId>
<version>${fastexcel.version}</version>
</dependency>
<!-- MinIO 对象存储 -->
@@ -174,6 +199,7 @@
<version>${minio.version}</version>
</dependency>
<!-- 阿里云 OSS 对象存储 -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
@@ -187,34 +213,27 @@
<version>${redisson.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- mybatis-plus 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus-generator.version}</version>
</dependency>
<!-- velocity 模板引擎(代码生成) -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity.version}</version>
</dependency>
<!-- IP 转省市区 -->
<dependency>
<groupId>org.lionsoul</groupId>
<artifactId>ip2region</artifactId>
<version>${ip2region.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
@@ -233,6 +252,7 @@
<version>${weixin-java.version}</version>
</dependency>
<!-- 本地缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
@@ -251,4 +271,4 @@
</plugins>
</build>
</project>
</project>

View File

@@ -1,122 +0,0 @@
# 注意:如果不需要定时任务功能,请勿执行此脚本
# XXL-JOB v2.4.0
# Copyright (c) 2015-present, xuxueli.
CREATE database if NOT EXISTS `xxl_job` default character set utf8mb4 collate utf8mb4_unicode_ci;
use `xxl_job`;
SET NAMES utf8mb4;
CREATE TABLE `xxl_job_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_desc` varchar(255) NOT NULL,
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`author` varchar(64) DEFAULT NULL COMMENT '作者',
`alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件',
`schedule_type` varchar(50) NOT NULL DEFAULT 'NONE' COMMENT '调度类型',
`schedule_conf` varchar(128) DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型',
`misfire_strategy` varchar(50) NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略',
`executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
`executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略',
`executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注',
`glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间',
`child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID多个逗号分隔',
`trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态0-停止1-运行',
`trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间',
`trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_id` int(11) NOT NULL COMMENT '任务主键ID',
`executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` varchar(512) DEFAULT NULL COMMENT '执行器任务参数',
`executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`trigger_time` datetime DEFAULT NULL COMMENT '调度-时间',
`trigger_code` int(11) NOT NULL COMMENT '调度-结果',
`trigger_msg` text COMMENT '调度-日志',
`handle_time` datetime DEFAULT NULL COMMENT '执行-时间',
`handle_code` int(11) NOT NULL COMMENT '执行-状态',
`handle_msg` text COMMENT '执行-日志',
`alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态0-默认、1-无需告警、2-告警成功、3-告警失败',
PRIMARY KEY (`id`),
KEY `I_trigger_time` (`trigger_time`),
KEY `I_handle_code` (`handle_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_log_report` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`trigger_day` datetime DEFAULT NULL COMMENT '调度-时间',
`running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量',
`suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量',
`fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量',
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_logglue` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_id` int(11) NOT NULL COMMENT '任务主键ID',
`glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注',
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_registry` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`registry_group` varchar(50) NOT NULL,
`registry_key` varchar(255) NOT NULL,
`registry_value` varchar(255) NOT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_group` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`app_name` varchar(64) NOT NULL COMMENT '执行器AppName',
`title` varchar(12) NOT NULL COMMENT '执行器名称',
`address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型0=自动注册、1=手动录入',
`address_list` text COMMENT '执行器地址列表,多地址逗号分隔',
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '账号',
`password` varchar(50) NOT NULL COMMENT '密码',
`role` tinyint(4) NOT NULL COMMENT '角色0-普通用户、1-管理员',
`permission` varchar(255) DEFAULT NULL COMMENT '权限执行器ID列表多个逗号分割',
PRIMARY KEY (`id`),
UNIQUE KEY `i_username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `xxl_job_lock` (
`lock_name` varchar(50) NOT NULL COMMENT '锁名称',
PRIMARY KEY (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `xxl_job_group`(`id`, `app_name`, `title`, `address_type`, `address_list`, `update_time`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL, '2018-11-03 22:21:31' );
INSERT INTO `xxl_job_info`(`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`) VALUES (1, 1, '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'CRON', '0 0 0 * * ? *', 'DO_NOTHING', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '');
INSERT INTO `xxl_job_user`(`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);
INSERT INTO `xxl_job_lock` ( `lock_name`) VALUES ( 'schedule_lock');
commit;

View File

@@ -72,10 +72,10 @@ INSERT INTO `sys_dict` VALUES (3, 'notice_level', '通知级别', 1, NULL, now()
-- ----------------------------
-- Table structure for sys_dict_data
-- Table structure for sys_dict_item
-- ----------------------------
DROP TABLE IF EXISTS `sys_dict_data`;
CREATE TABLE `sys_dict_data` (
DROP TABLE IF EXISTS `sys_dict_item`;
CREATE TABLE `sys_dict_item` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`dict_code` varchar(50) COMMENT '关联字典编码与sys_dict表中的dict_code对应',
`value` varchar(50) COMMENT '字典项值',
@@ -89,23 +89,23 @@ CREATE TABLE `sys_dict_data` (
`update_time` datetime COMMENT '更新时间',
`update_by` bigint COMMENT '修改人ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字典数据';
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字典';
-- ----------------------------
-- Records of sys_dict_data
-- Records of sys_dict_item
-- ----------------------------
INSERT INTO `sys_dict_data` VALUES (1, 'gender', '1', '', 'primary', 1, 1, NULL, now(), 1,now(),1);
INSERT INTO `sys_dict_data` VALUES (2, 'gender', '2', '', 'danger', 1, 2, NULL, now(), 1,now(),1);
INSERT INTO `sys_dict_data` VALUES (3, 'gender', '0', '保密', 'info', 1, 3, NULL, now(), 1,now(),1);
INSERT INTO `sys_dict_data` VALUES (4, 'notice_type', '1', '系统升级', 'success', 1, 1, '', now(), 1,now(),1);
INSERT INTO `sys_dict_data` VALUES (5, 'notice_type', '2', '系统维护', 'primary', 1, 2, '', now(), 1,now(),1);
INSERT INTO `sys_dict_data` VALUES (6, 'notice_type', '3', '安全警告', 'danger', 1, 3, '', now(), 1,now(),1);
INSERT INTO `sys_dict_data` VALUES (7, 'notice_type', '4', '假期通知', 'success', 1, 4, '', now(), 1,now(),1);
INSERT INTO `sys_dict_data` VALUES (8, 'notice_type', '5', '公司新闻', 'primary', 1, 5, '', now(), 1,now(),1);
INSERT INTO `sys_dict_data` VALUES (9, 'notice_type', '99', '其他', 'info', 1, 99, '', now(), 1,now(),1);
INSERT INTO `sys_dict_data` VALUES (10, 'notice_level', 'L', '', 'info', 1, 1, '', now(), 1,now(),1);
INSERT INTO `sys_dict_data` VALUES (11, 'notice_level', 'M', '', 'warning', 1, 2, '', now(), 1,now(),1);
INSERT INTO `sys_dict_data` VALUES (12, 'notice_level', 'H', '', 'danger', 1, 3, '', now(), 1,now(),1);
INSERT INTO `sys_dict_item` VALUES (1, 'gender', '1', '', 'primary', 1, 1, NULL, now(), 1,now(),1);
INSERT INTO `sys_dict_item` VALUES (2, 'gender', '2', '', 'danger', 1, 2, NULL, now(), 1,now(),1);
INSERT INTO `sys_dict_item` VALUES (3, 'gender', '0', '保密', 'info', 1, 3, NULL, now(), 1,now(),1);
INSERT INTO `sys_dict_item` VALUES (4, 'notice_type', '1', '系统升级', 'success', 1, 1, '', now(), 1,now(),1);
INSERT INTO `sys_dict_item` VALUES (5, 'notice_type', '2', '系统维护', 'primary', 1, 2, '', now(), 1,now(),1);
INSERT INTO `sys_dict_item` VALUES (6, 'notice_type', '3', '安全警告', 'danger', 1, 3, '', now(), 1,now(),1);
INSERT INTO `sys_dict_item` VALUES (7, 'notice_type', '4', '假期通知', 'success', 1, 4, '', now(), 1,now(),1);
INSERT INTO `sys_dict_item` VALUES (8, 'notice_type', '5', '公司新闻', 'primary', 1, 5, '', now(), 1,now(),1);
INSERT INTO `sys_dict_item` VALUES (9, 'notice_type', '99', '其他', 'info', 1, 99, '', now(), 1,now(),1);
INSERT INTO `sys_dict_item` VALUES (10, 'notice_level', 'L', '', 'info', 1, 1, '', now(), 1,now(),1);
INSERT INTO `sys_dict_item` VALUES (11, 'notice_level', 'M', '', 'warning', 1, 2, '', now(), 1,now(),1);
INSERT INTO `sys_dict_item` VALUES (12, 'notice_level', 'H', '', 'danger', 1, 3, '', now(), 1,now(),1);
-- ----------------------------
-- Table structure for sys_menu
@@ -143,12 +143,12 @@ 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);
INSERT INTO `sys_menu` VALUES (30, 26, '0,26', '平台文档(外链)', 3, NULL, 'https://juejin.cn/post/7228990409909108793', '', NULL, NULL, NULL, 1, 2, 'link', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (30, 26, '0,26', '平台文档(外链)', 3, NULL, 'https://juejin.cn/post/7228990409909108793', '', NULL, NULL, NULL, 1, 2, 'document', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (31, 2, '0,1,2', '用户新增', 4, NULL, '', NULL, 'sys:user:add', NULL, NULL, 1, 1, '', '', now(), now(), 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);
@@ -170,10 +170,9 @@ INSERT INTO `sys_menu` VALUES (78, 5, '0,1,5', '部门删除', 4, NULL, '', NULL
INSERT INTO `sys_menu` VALUES (79, 6, '0,1,6', '字典新增', 4, NULL, '', NULL, 'sys:dict:add', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (81, 6, '0,1,6', '字典编辑', 4, NULL, '', NULL, 'sys:dict:edit', NULL, NULL, 1, 2, '', NULL, now(), now(), 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:password:reset', NULL, NULL, 1, 4, '', 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);
@@ -201,15 +200,25 @@ INSERT INTO `sys_menu` VALUES (129, 126, '0,1,126', '通知编辑', 4, NULL, '',
INSERT INTO `sys_menu` VALUES (130, 126, '0,1,126', '通知删除', 4, NULL, '', NULL, 'sys:notice:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (133, 126, '0,1,126', '通知发布', 4, NULL, '', NULL, 'sys:notice:publish', 0, 1, 1, 5, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (134, 126, '0,1,126', '通知撤回', 4, NULL, '', NULL, 'sys:notice:revoke', 0, 1, 1, 6, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (135, 1, '0,1', '字典数据', 1, 'DictData', 'dict-data', 'system/dict/data', NULL, 0, 1, 0, 6, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (136, 135, '0,1,135', '字典数据新增', 4, NULL, '', NULL, 'sys:dict-data:add', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (137, 135, '0,1,135', '字典数据编辑', 4, NULL, '', NULL, 'sys:dict-data:edit', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (138, 135, '0,1,135', '字典数据删除', 4, NULL, '', NULL, 'sys:dict-data:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (135, 1, '0,1', '字典', 1, 'DictItem', 'dict-item', 'system/dict/dict-item', NULL, 0, 1, 0, 6, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (136, 135, '0,1,135', '字典新增', 4, NULL, '', NULL, 'sys:dict-item:add', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (137, 135, '0,1,135', '字典编辑', 4, NULL, '', NULL, 'sys:dict-item:edit', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (138, 135, '0,1,135', '字典删除', 4, NULL, '', NULL, 'sys:dict-item:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (139, 3, '0,1,3', '角色查询', 4, NULL, '', NULL, 'sys:role:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (140, 4, '0,1,4', '菜单查询', 4, NULL, '', NULL, 'sys:menu:query', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
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-data: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', '', 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, NULL, 'drag', 'demo/drag', NULL, NULL, NULL, 1, 5, '', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (147, 36, '0,36', '滚动文本', 1, NULL, 'text-scroll', 'demo/text-scroll', NULL, NULL, NULL, 1, 6, '', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (148, 89, '0,89', '字典实时同步', 1, NULL, 'dict-sync', 'demo/dict-sync', NULL, NULL, NULL, 1, 3, '', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (149, 89, '0,89', 'VxeTable', 1, NULL, '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, 'AutoOpreationColumn', 'opreation-column', 'demo/auto-opreation-column', NULL, NULL, 1, 1, 1, '', '', now(), now(), NULL);
INSERT INTO `sys_menu` VALUES (151, 89, '0,89', 'PDF预览', 1, NULL, 'pdf-preview', 'demo/pdf-preview', NULL, NULL, 1, 1, 7, 'el-icon-Reading', '', now(), now(), NULL);
-- ----------------------------
-- Table structure for sys_role
@@ -221,7 +230,7 @@ CREATE TABLE `sys_role` (
`code` varchar(32) NOT NULL COMMENT '角色编码',
`sort` int NULL COMMENT '显示顺序',
`status` tinyint(1) DEFAULT 1 COMMENT '角色状态(1-正常 0-停用)',
`data_scope` tinyint NULL COMMENT '数据权限(0-所有数据 1-部门及子部门数据 2-本部门数据3-本人数据)',
`data_scope` tinyint NULL COMMENT '数据权限(1-所有数据 2-部门及子部门数据 3-本部门数据 4-本人数据)',
`create_by` bigint NULL COMMENT '创建人 ID',
`create_time` datetime NULL COMMENT '创建时间',
`update_by` bigint NULL COMMENT '更新人ID',
@@ -235,9 +244,9 @@ CREATE TABLE `sys_role` (
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, '超级管理员', 'ROOT', 1, 1, 0, NULL, now(), NULL, now(), 0);
INSERT INTO `sys_role` VALUES (2, '系统管理员', 'ADMIN', 2, 1, 0, NULL, now(), NULL, NULL, 0);
INSERT INTO `sys_role` VALUES (3, '访问游客', 'GUEST', 3, 1, 2, NULL, now(), NULL, now(), 0);
INSERT INTO `sys_role` VALUES (1, '超级管理员', 'ROOT', 1, 1, 1, NULL, now(), NULL, now(), 0);
INSERT INTO `sys_role` VALUES (2, '系统管理员', 'ADMIN', 2, 1, 1, NULL, now(), NULL, NULL, 0);
INSERT INTO `sys_role` VALUES (3, '访问游客', 'GUEST', 3, 1, 3, NULL, now(), NULL, now(), 0);
INSERT INTO `sys_role` VALUES (4, '系统管理员1', 'ADMIN1', 4, 1, 1, NULL, now(), NULL, NULL, 0);
INSERT INTO `sys_role` VALUES (5, '系统管理员2', 'ADMIN2', 5, 1, 1, NULL, now(), NULL, NULL, 0);
INSERT INTO `sys_role` VALUES (6, '系统管理员3', 'ADMIN3', 6, 1, 1, NULL, now(), NULL, NULL, 0);
@@ -343,6 +352,14 @@ INSERT INTO `sys_role_menu` VALUES (2, 140);
INSERT INTO `sys_role_menu` VALUES (2, 141);
INSERT INTO `sys_role_menu` VALUES (2, 142);
INSERT INTO `sys_role_menu` VALUES (2, 143);
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
@@ -366,7 +383,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 = '用户信息表';
-- ----------------------------
@@ -434,9 +451,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='代码生成基础配置表';
@@ -551,4 +570,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;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -12,7 +12,6 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
*/
@SpringBootApplication
@ConfigurationPropertiesScan // 开启配置属性绑定
// @EnableScheduling // 开启定时任务
public class YouLaiBootApplication {
public static void main(String[] args) {

View File

@@ -1,18 +1,22 @@
package com.youlai.boot.shared.auth.controller;
package com.youlai.boot.auth.controller;
import com.youlai.boot.auth.model.CaptchaInfo;
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.auth.service.AuthService;
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
import com.youlai.boot.core.security.model.AuthenticationToken;
import com.youlai.boot.common.annotation.Log;
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,7 +32,7 @@ public class AuthController {
private final AuthService authService;
@Operation(summary = "获取登录验证码")
@Operation(summary = "获取验证码")
@GetMapping("/captcha")
public Result<CaptchaInfo> getCaptcha() {
CaptchaInfo captcha = authService.getCaptcha();
@@ -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<AuthenticationToken> 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<Void> 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<Void> 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<AuthenticationToken> 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<AuthenticationToken> loginByWxMiniAppCode(@RequestBody @Valid WxMiniAppCodeLoginDTO loginDTO) {
AuthenticationToken token = authService.loginByWxMiniAppCode(loginDTO);
return Result.success(token);
}
@Operation(summary = "微信小程序登录(手机号)")
@PostMapping("/wx/miniapp/phone-login")
public Result<AuthenticationToken> 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);
}
}

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.shared.auth.enums;
package com.youlai.boot.auth.enums;
/**
* EasyCaptcha 验证码类型枚举

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.shared.auth.model;
package com.youlai.boot.auth.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

View File

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

View File

@@ -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 有来技术团队
* @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;
}

View File

@@ -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.auth.model.CaptchaInfo;
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
import com.youlai.boot.core.security.model.AuthenticationToken;
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
/**
* 认证服务接口
@@ -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);
/**
* 发送短信验证码
*

View File

@@ -1,23 +1,24 @@
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.enums.CaptchaTypeEnum;
import com.youlai.boot.auth.model.CaptchaInfo;
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
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.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.extension.wx.WxMiniAppCodeAuthenticationToken;
import com.youlai.boot.core.security.extension.wx.WxMiniAppPhoneAuthenticationToken;
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.core.security.util.SecurityUtils;
import com.youlai.boot.shared.sms.enums.SmsTypeEnum;
import com.youlai.boot.shared.sms.service.SmsService;
import lombok.RequiredArgsConstructor;
@@ -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;
}
/**
@@ -217,15 +218,53 @@ public class AuthServiceImpl implements AuthService {
*/
@Override
public AuthenticationToken refreshToken(String refreshToken) {
// 验证刷新令牌
boolean isValidate = tokenManager.validateToken(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;
}
}

View File

@@ -10,6 +10,11 @@ package com.youlai.boot.common.constant;
*/
public interface JwtClaimConstants {
/**
* 令牌类型
*/
String TOKEN_TYPE = "tokenType";
/**
* 用户ID
*/

View File

@@ -6,7 +6,7 @@ import lombok.Getter;
/**
* 数据权限枚举
*
* @author haoxr
* @author Ray.Hao
* @since 2.3.0
*/
@Getter
@@ -15,10 +15,10 @@ public enum DataScopeEnum implements IBaseEnum<Integer> {
/**
* value 越小,数据权限范围越大
*/
ALL(0, "所有数据"),
DEPT_AND_SUB(1, "部门及子部门数据"),
DEPT(2, "本部门数据"),
SELF(3, "本人数据");
ALL(1, "所有数据"),
DEPT_AND_SUB(2, "部门及子部门数据"),
DEPT(3, "本部门数据"),
SELF(4, "本人数据");
private final Integer value;

View File

@@ -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());
}

View File

@@ -9,6 +9,10 @@ import java.io.Serializable;
* 响应码枚举
* <p>
* 参考阿里巴巴开发手册响应码规范
* 00000 正常
* A**** 用户端错误
* B**** 系统执行出错
* C**** 调用第三方服务出错
*
* @author Ray.Hao
* @since 2020/6/23
@@ -57,6 +61,7 @@ public enum ResultCode implements IResultCode, Serializable {
USER_PASSWORD_ERROR("A0210", "用户名或密码错误"),
USER_INPUT_PASSWORD_ERROR_LIMIT_EXCEEDED("A0211", "用户输入密码错误次数超限"),
USER_NOT_EXIST("A0212", "用户不存在"),
USER_IDENTITY_VERIFICATION_FAILED("A0220", "用户身份校验失败"),
USER_FINGERPRINT_RECOGNITION_FAILED("A0221", "用户指纹识别失败"),
@@ -245,6 +250,8 @@ public enum ResultCode implements IResultCode, Serializable {
PRIMARY_KEY_CONFLICT("C0341", "主键冲突"),
DATABASE_ACCESS_DENIED("C0351", "演示环境已禁用数据库写入功能请本地部署修改数据库链接或开启Mock模式进行体验"),
/** 二级宏观错误码 */
THIRD_PARTY_DISASTER_RECOVERY_SYSTEM_TRIGGERED("C0400", "第三方容灾系统被触发"),
THIRD_PARTY_SYSTEM_RATE_LIMITING("C0401", "第三方系统限流"),

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.common.util;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.event.AnalysisEventListener;
import cn.idev.excel.EasyExcel;
import cn.idev.excel.event.AnalysisEventListener;
import java.io.InputStream;

View File

@@ -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.pulgin.mybatis.MyDataPermissionHandler;
import com.youlai.boot.pulgin.mybatis.MyMetaObjectHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

View File

@@ -8,7 +8,8 @@ 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.extension.wx.WxMiniAppCodeAuthenticationProvider;
import com.youlai.boot.core.security.extension.wx.WxMiniAppPhoneAuthenticationProvider;
import com.youlai.boot.core.security.filter.CaptchaValidationFilter;
import com.youlai.boot.core.security.filter.TokenAuthenticationFilter;
import com.youlai.boot.core.security.token.TokenManager;
@@ -65,12 +66,12 @@ public class SecurityConfig {
return http
.authorizeHttpRequests(requestMatcherRegistry -> {
// 忽略认证的 URI 地址
// 配置无需登录即可访问的公开接口
String[] ignoreUrls = securityProperties.getIgnoreUrls();
if (ArrayUtil.isNotEmpty(ignoreUrls)) {
requestMatcherRegistry.requestMatchers(ignoreUrls).permitAll();
}
// 其他请求都需要认证
// 其他所有请求需登录后访问
requestMatcherRegistry.anyRequest().authenticated();
}
)
@@ -125,13 +126,20 @@ public class SecurityConfig {
}
/**
* 微信认证 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 +155,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
);
}

View File

@@ -1,107 +1,169 @@
package com.youlai.boot.config;
import cn.hutool.core.util.StrUtil;
import cn.hutool.jwt.JWTPayload;
import cn.hutool.jwt.JWTUtil;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.system.event.UserConnectionEvent;
import com.youlai.boot.core.security.model.SysUserDetails;
import com.youlai.boot.core.security.token.TokenManager;
import com.youlai.boot.system.service.WebSocketService;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.ApplicationEventPublisher;
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;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* WebSocket 自动配置
* WebSocket配置
*
* @author haoxr
* @since 2.4.0
* @author Ray.Hao
* @since 3.0.0
*/
// 启用WebSocket消息代理功能和配置STOMP协议实现实时双向通信和消息传递
@EnableWebSocketMessageBroker
@Configuration
@Slf4j
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final ApplicationEventPublisher eventPublisher;
private final TokenManager tokenManager;
private final WebSocketService webSocketService;
public WebSocketConfig(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
/**
* 注册一个端点,客户端通过这个端点进行连接
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
// 注册 /ws 的端点
.addEndpoint("/ws")
// 允许跨域
.setAllowedOriginPatterns("*");
}
public WebSocketConfig(TokenManager tokenManager, @Lazy WebSocketService webSocketService) {
this.tokenManager = tokenManager;
this.webSocketService = webSocketService;
}
/**
* 注册一个端点,客户端通过这个端点进行连接
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
// 注册 /ws 的端点
.addEndpoint("/ws")
// 允许跨域
.setAllowedOriginPatterns("*");
}
/**
* 配置消息代理
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 客户端发送消息的请求前缀
registry.setApplicationDestinationPrefixes("/app");
/**
* 配置消息代理
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 客户端发送消息的请求前缀
registry.setApplicationDestinationPrefixes("/app");
// 客户端订阅消息的请求前缀topic一般用于广播推送queue用于点对点推送
registry.enableSimpleBroker("/topic", "/queue");
// 客户端订阅消息的请求前缀topic一般用于广播推送queue用于点对点推送
registry.enableSimpleBroker("/topic", "/queue");
// 服务端通知客户端的前缀可以不设置默认为user
registry.setUserDestinationPrefix("/user");
}
// 服务端通知客户端的前缀可以不设置默认为user
registry.setUserDestinationPrefix("/user");
}
/**
* 配置客户端入站通道拦截器
* <p>
* 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户
*
* @param registration 通道注册器
*/
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor != null) {
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {
bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length());
String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT);
if (StrUtil.isNotBlank(username)) {
accessor.setUser(() -> username);
eventPublisher.publishEvent(new UserConnectionEvent(this, username, true));
}
}
} else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {
if (accessor.getUser() != null) {
String username = accessor.getUser().getName();
eventPublisher.publishEvent(new UserConnectionEvent(this, username, false));
}
}
}
return ChannelInterceptor.super.preSend(message, channel);
/**
* 配置客户端入站通道拦截器
* <p>
* 核心功能:
* 1. 连接建立时解析令牌并绑定用户身份
* 2. 连接关闭时触发下线通知
* 3. 异常Token的防御性处理
*/
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor == null) {
return ChannelInterceptor.super.preSend(message, channel);
}
try {
// 处理客户端连接请求
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
/*
* 安全校验流程:
* 1. 从HEADER中获取Authorization值
* 2. 校验Bearer Token格式合法性
* 3. 解析并验证JWT有效性
* 4. 绑定用户身份到当前会话
*/
String authorization = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
// 防御性校验确保Authorization头存在且格式正确
if (StrUtil.isBlank(authorization) || !authorization.startsWith("Bearer ")) {
log.warn("非法连接请求缺少有效的Authorization头");
throw new AuthenticationCredentialsNotFoundException("Missing authorization header");
}
});
}
// 提取并处理JWT令牌移除Bearer前缀
String token = authorization.substring(7);
Authentication authentication = tokenManager.parseToken(token);
// 令牌解析失败处理
if (authentication == null) {
log.error("令牌解析失败:{}", token);
throw new BadCredentialsException("Invalid token");
}
// 获取用户详细信息
SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
if (userDetails == null || StrUtil.isBlank(userDetails.getUsername())) {
log.error("无效的用户凭证:{}", token);
throw new BadCredentialsException("Invalid user credentials");
}
String username = userDetails.getUsername();
log.info("WebSocket连接建立用户[{}]", username);
// 绑定用户身份到当前会话(重要:用于@SendToUser等注解
accessor.setUser(authentication);
// 记录用户上线状态
webSocketService.userConnected(username, accessor.getSessionId());
}
// 处理客户端断开请求
else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {
/*
* 注意:只有成功建立过认证的连接才会触发下线事件
* 防止未认证成功的连接产生脏数据
*/
Authentication authentication = (Authentication) accessor.getUser();
if (authentication != null && authentication.isAuthenticated()) {
String username = ((SysUserDetails) authentication.getPrincipal()).getUsername();
log.info("WebSocket连接关闭用户[{}]", username);
// 记录用户下线状态
webSocketService.userDisconnected(username);
}
}
} catch (AuthenticationException ex) {
// 认证失败时强制关闭连接
log.error("连接认证失败:{}", ex.getMessage());
throw ex;
} catch (Exception ex) {
// 捕获其他未知异常
log.error("WebSocket连接处理异常", ex);
throw new MessagingException("Connection processing failed");
}
return ChannelInterceptor.super.preSend(message, channel);
}
});
}
}

View File

@@ -7,7 +7,6 @@ import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.util.StringUtils;
import com.aliyun.oss.HttpMethod;
import com.youlai.boot.common.enums.LogModuleEnum;
import com.youlai.boot.common.util.IPUtils;
@@ -61,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;
@@ -72,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;
}
@@ -85,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();
@@ -109,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)) {
@@ -209,7 +211,7 @@ public class LogAspect {
* @return UserAgent
*/
public UserAgent resolveUserAgent(String userAgentString) {
if (StringUtils.isBlank(userAgentString)) {
if (StrUtil.isBlank(userAgentString)) {
return null;
}
// 给userAgentStringMD5加密一次防止过长

View File

@@ -21,7 +21,7 @@ import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* IP限流过滤器
* IP 限流过滤器
*
* @author Theo
* @since 2024/08/10 14:38

View File

@@ -5,7 +5,7 @@ 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.system.model.dto.UserAuthInfo;
import com.youlai.boot.core.security.model.UserAuthCredentials;
import com.youlai.boot.system.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
@@ -49,14 +49,14 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
String inputVerifyCode = (String) authentication.getCredentials();
// 根据手机号获取用户信息
UserAuthInfo userAuthInfo = userService.getUserAuthInfoByMobile(mobile);
UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByMobile(mobile);
if (userAuthInfo == null) {
if (userAuthCredentials == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 检查用户状态是否有效
if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) {
throw new DisabledException("用户已被禁用");
}
@@ -72,7 +72,7 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
}
// 构建认证后的用户详情信息
SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
SysUserDetails userDetails = new SysUserDetails(userAuthCredentials);
// 创建已认证的 SmsAuthenticationToken
return SmsAuthenticationToken.authenticated(

View File

@@ -1,11 +1,11 @@
package com.youlai.boot.core.security.extension.wechat;
package com.youlai.boot.core.security.extension.wx;
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.system.model.dto.UserAuthInfo;
import com.youlai.boot.core.security.model.UserAuthCredentials;
import com.youlai.boot.system.service.UserService;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
@@ -18,20 +18,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;
}
@@ -63,30 +62,29 @@ public class WechatAuthenticationProvider implements AuthenticationProvider {
}
// 根据微信 OpenID 查询用户信息
UserAuthInfo userAuthInfo = userService.getUserAuthInfoByOpenId(openId);
UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByOpenId(openId);
if (userAuthInfo == null) {
// TODO: 用户不存在则注册这里需要获取用户手机号并与现有用户绑定
if (userAuthCredentials == null) {
// 用户不存在则注册
userService.registerOrBindWechatUser(openId);
// 再次查询用户信息确保用户注册成功
userAuthInfo = userService.getUserAuthInfoByOpenId(openId);
if (userAuthInfo == null) {
userAuthCredentials = userService.getAuthCredentialsByOpenId(openId);
if (userAuthCredentials == null) {
throw new UsernameNotFoundException("用户注册失败,请稍后重试");
}
}
// 检查用户状态是否有效
if (ObjectUtil.notEqual(userAuthInfo.getStatus(), 1)) {
if (ObjectUtil.notEqual(userAuthCredentials.getStatus(), 1)) {
throw new DisabledException("用户已被禁用");
}
// 这里因为已经根据 code 从微信小程序获取到 openid 不需要再经过系统认证所以直接生成
// 构建认证后的用户详情信息
SysUserDetails userDetails = new SysUserDetails(userAuthInfo);
SysUserDetails userDetails = new SysUserDetails(userAuthCredentials);
// 创建已认证的 WeChatAuthenticationToken
return WechatAuthenticationToken.authenticated(
// 创建已认证的Token
return WxMiniAppCodeAuthenticationToken.authenticated(
userDetails,
userDetails.getAuthorities()
);
@@ -94,6 +92,6 @@ public class WechatAuthenticationProvider implements AuthenticationProvider {
@Override
public boolean supports(Class<?> authentication) {
return WechatAuthenticationToken.class.isAssignableFrom(authentication);
return WxMiniAppCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
}

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.security.extension.wechat;
package com.youlai.boot.core.security.extension.wx;
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<? extends GrantedAuthority> authorities) {
public WxMiniAppCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> 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<? extends GrantedAuthority> authorities) {
return new WechatAuthenticationToken(principal, authorities);
public static WxMiniAppCodeAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> authorities) {
return new WxMiniAppCodeAuthenticationToken(principal, authorities);
}
@Override
@@ -66,4 +66,4 @@ public class WechatAuthenticationToken extends AbstractAuthenticationToken {
public Object getPrincipal() {
return this.principal;
}
}
}

View File

@@ -0,0 +1,114 @@
package com.youlai.boot.core.security.extension.wx;
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.core.security.model.SysUserDetails;
import com.youlai.boot.core.security.model.UserAuthCredentials;
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);
}
}

View File

@@ -0,0 +1,89 @@
package com.youlai.boot.core.security.extension.wx;
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<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
/**
* 认证通过
*
* @param principal 用户信息
* @param authorities 授权信息
* @return 认证通过的Token
*/
public static WxMiniAppPhoneAuthenticationToken authenticated(Object principal, Collection<? extends GrantedAuthority> 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;
}
}

View File

@@ -56,7 +56,7 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
return;
}
// 将令牌解析为Spring Security认证对象
// 将令牌解析为 Spring Security 上下文认证对象
Authentication authentication = tokenManager.parseToken(rawToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

View File

@@ -3,7 +3,6 @@ package com.youlai.boot.core.security.model;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.system.model.dto.UserAuthInfo;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
@@ -65,9 +64,9 @@ public class SysUserDetails implements UserDetails {
/**
* 构造函数:根据用户认证信息初始化用户详情对象
*
* @param user 用户认证信息对象 {@link UserAuthInfo}
* @param user 用户认证信息对象 {@link UserAuthCredentials}
*/
public SysUserDetails(UserAuthInfo user) {
public SysUserDetails(UserAuthCredentials user) {
this.userId = user.getUserId();
this.username = user.getUsername();
this.password = user.getPassword();

View File

@@ -1,17 +1,16 @@
package com.youlai.boot.system.model.dto;
package com.youlai.boot.core.security.model;
import lombok.Data;
import java.util.Set;
/**
* 用户认证信息
* 用户认证凭证信息
*
* @author Ray.Hao
* @since 2022/10/22
*/
@Data
public class UserAuthInfo {
public class UserAuthCredentials {
/**
* 用户ID

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.core.security.service;
import com.youlai.boot.core.security.model.SysUserDetails;
import com.youlai.boot.system.model.dto.UserAuthInfo;
import com.youlai.boot.core.security.model.UserAuthCredentials;
import com.youlai.boot.system.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -33,11 +33,11 @@ public class SysUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
UserAuthInfo userAuthInfo = userService.getUserAuthInfo(username);
if (userAuthInfo == null) {
UserAuthCredentials userAuthCredentials = userService.getAuthCredentialsByUsername(username);
if (userAuthCredentials == null) {
throw new UsernameNotFoundException(username);
}
return new SysUserDetails(userAuthInfo);
return new SysUserDetails(userAuthCredentials);
} catch (Exception e) {
// 记录异常日志
log.error("认证异常:{}", e.getMessage());

View File

@@ -3,6 +3,7 @@ package com.youlai.boot.core.security.token;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTPayload;
@@ -13,8 +14,9 @@ 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.config.property.SecurityProperties;
import com.youlai.boot.core.security.model.SysUserDetails;
import com.youlai.boot.core.security.model.AuthenticationToken;
import org.apache.commons.lang3.StringUtils;
import com.youlai.boot.core.security.model.SysUserDetails;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -64,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)
@@ -108,21 +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);
return validateToken(token,false);
}
if (isValid) {
// 检查 Token 是否已被加入黑名单(注销、修改密码等场景)
JSONObject payloads = jwt.getPayloads();
String jti = payloads.getStr(JWTPayload.JWT_ID);
/**
* 校验刷新令牌
*
* @param refreshToken JWT Token
* @return 验证结果
*/
@Override
public boolean validateRefreshToken(String refreshToken) {
return validateToken(refreshToken,true);
}
// 判断是否在黑名单中如果在则返回false 标识Token无效
if (Boolean.TRUE.equals(redisTemplate.hasKey(RedisConstants.Auth.BLACKLIST_TOKEN + jti))) {
return false;
/**
* 校验令牌
*
* @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 isValid;
return false;
}
/**
@@ -132,17 +167,18 @@ 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 = RedisConstants.Auth.BLACKLIST_TOKEN + payloads.getStr(JWTPayload.JWT_ID);
String blacklistTokenKey = StrUtil.format(RedisConstants.Auth.BLACKLIST_TOKEN, payloads.getStr(JWTPayload.JWT_ID));
if (expirationAt != null) {
int currentTimeSeconds = Convert.toInt(System.currentTimeMillis() / 1000);
@@ -168,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().getRefreshTokenTimeToLive();
int accessTokenExpiration = securityProperties.getSession().getAccessTokenTimeToLive();
String newAccessToken = generateToken(authentication, accessTokenExpiration);
return AuthenticationToken.builder()
.accessToken(newAccessToken)
.refreshToken(refreshToken)
@@ -190,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<String, Object> payload = new HashMap<>();
payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID
payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID
@@ -210,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) {
@@ -221,4 +269,5 @@ public class JwtTokenManager implements TokenManager {
return JWTUtil.createToken(payload, secretKey);
}
}

View File

@@ -25,7 +25,7 @@ import java.util.stream.Collectors;
/**
* Redis Token 管理器
* <p>
* 用于生成、解析、校验、刷新 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
@@ -115,6 +115,17 @@ public class RedisTokenManager implements TokenManager {
return redisTemplate.hasKey(formatTokenKey(token));
}
/**
* 校验 RefreshToken 是否有效
*
* @param refreshToken 访问令牌
* @return 是否有效
*/
@Override
public boolean validateRefreshToken(String refreshToken) {
return redisTemplate.hasKey(formatRefreshTokenKey(refreshToken));
}
/**
* 刷新令牌
*
@@ -178,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) {
// 访问令牌 -> 用户信息
@@ -199,7 +210,7 @@ public class RedisTokenManager implements TokenManager {
/**
* 处理单设备登录控制
*
* @param userId 用户ID
* @param userId 用户ID
* @param accessToken 新生成的访问令牌
*/
private void handleSingleDeviceLogin(Long userId, String accessToken) {
@@ -220,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());
@@ -231,7 +242,7 @@ public class RedisTokenManager implements TokenManager {
/**
* 构建用户详情对象
*
* @param onlineUser 在线用户信息
* @param onlineUser 在线用户信息
* @param authorities 权限集合
* @return SysUserDetails 用户详情
*/
@@ -255,6 +266,16 @@ public class RedisTokenManager implements TokenManager {
return StrUtil.format(RedisConstants.Auth.ACCESS_TOKEN_USER, token);
}
/**
* 格式化刷新令牌的 Redis 键
*
* @param refreshToken 访问令牌
* @return 格式化后的 Redis 键
*/
private String formatRefreshTokenKey(String refreshToken) {
return StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken);
}
/**
* 将值存储到 Redis
*

View File

@@ -38,6 +38,14 @@ public interface TokenManager {
*/
boolean validateToken(String token);
/**
* 校验 刷新 Token 是否有效
*
* @param refreshToken JWT Token
* @return 是否有效
*/
boolean validateRefreshToken(String refreshToken);
/**
* 刷新 Token
*
@@ -49,7 +57,7 @@ public interface TokenManager {
/**
* 令 Token 失效
*
* @param token JWT Token
* @param token Token
*/
default void invalidateToken(String token) {
// 默认实现可以是空的,或者抛出不支持的操作异常

View File

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

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.modules.member.controller;
package com.youlai.boot.module.member.controller;
/**
* 会员控制层-业务模块演示

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.modules.member.mapper;
package com.youlai.boot.module.member.mapper;
/**
* 会员数据访问层-业务模块演示

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.modules.member.model;
package com.youlai.boot.module.member.model;
/**
* 会员实体-业务模块演示

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.modules.member.service;
package com.youlai.boot.module.member.service;
/**
* 会员管理服务类-业务模块演示

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.modules.order.controller;
package com.youlai.boot.module.order.controller;
/**
* 订单控制层-业务模块演示

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.modules.order.mapper;
package com.youlai.boot.module.order.mapper;
/**
* 订单数据访问层-业务模块演示

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.modules.order.model;
package com.youlai.boot.module.order.model;
/**
* 订单实体-业务模块演示

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.modules.order.service;
package com.youlai.boot.module.order.service;
/**
* 订单管理服务类-业务模块演示

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.modules.product.controller;
package com.youlai.boot.module.product.controller;
/**
* 商品控制层-业务模块演示

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.modules.product.mapper;
package com.youlai.boot.module.product.mapper;
/**
* 商品数据访问层-业务模块演示

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.modules.product.model;
package com.youlai.boot.module.product.model;
/**
* 商品实体-业务模块演示

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.modules.product.service;
package com.youlai.boot.module.product.service;
/**
* 会员管理服务类-业务模块演示

View File

@@ -0,0 +1,150 @@
package com.youlai.boot.pulgin.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 <a href="xiaoymin@foxmail.com">xiaoymin@foxmail.com</a>
* 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<String, Object> 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<String> 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<Class<?>> 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<Tag.name,ApiSupport.order>
Map<String, Integer> 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<Class<?>> scanPackageByAnnotation(
String packageName, final Class<? extends Annotation> annotationClass) {
ClassPathScanningCandidateComponentProvider scanner =
new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(annotationClass));
Set<Class<?>> classes = new HashSet<>();
for (BeanDefinition beanDefinition : scanner.findCandidateComponents(packageName)) {
try {
Class<?> clazz = Class.forName(beanDefinition.getBeanClassName());
classes.add(clazz);
} catch (ClassNotFoundException ignore) {
}
}
return classes;
}
}

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.handler;
package com.youlai.boot.pulgin.mybatis;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.StringPool;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.handler;
package com.youlai.boot.pulgin.mybatis;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;

View File

@@ -82,17 +82,19 @@ public class CodegenController {
@Operation(summary = "获取预览生成代码")
@GetMapping("/{tableName}/preview")
@Log(value = "预览生成代码", module = LogModuleEnum.OTHER)
public Result<List<CodegenPreviewVO>> getTablePreviewData(@PathVariable String tableName) {
List<CodegenPreviewVO> list = codegenService.getCodegenPreviewData(tableName);
public Result<List<CodegenPreviewVO>> getTablePreviewData(@PathVariable String tableName,
@RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType) {
List<CodegenPreviewVO> 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));

View File

@@ -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<GenFieldConfig> fieldConfigs);

View File

@@ -51,4 +51,14 @@ public class GenConfig extends BaseEntity {
* 作者
*/
private String author;
/**
* 页面类型 classic|curd
*/
private String pageType;
/**
* 要移除的表前缀,如: sys_
*/
private String removeTablePrefix;
}

View File

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

View File

@@ -29,12 +29,12 @@ public interface CodegenService {
* @param tableName 表名
* @return
*/
List<CodegenPreviewVO> getCodegenPreviewData(String tableName);
List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType);
/**
* 下载代码
* @param tableNames 表名
* @return
*/
byte[] downloadCode(String[] tableNames);
byte[] downloadCode(String[] tableNames, String pageType);
}

View File

@@ -2,6 +2,7 @@ package com.youlai.boot.shared.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;
@@ -72,7 +73,7 @@ public class CodegenServiceImpl implements CodegenService {
* @return 预览数据
*/
@Override
public List<CodegenPreviewVO> getCodegenPreviewData(String tableName) {
public List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType) {
List<CodegenPreviewVO> 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<GenFieldConfig> fieldConfigs) {
private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenConfig genConfig, List<GenFieldConfig> fieldConfigs, String pageType) {
Map<String, Object> bindMap = new HashMap<>();
@@ -225,7 +229,7 @@ public class CodegenServiceImpl implements CodegenService {
bindMap.put("tableName", genConfig.getTableName());
bindMap.put("author", genConfig.getAuthor());
bindMap.put("lowerFirstEntityName", StrUtil.lowerFirst(entityName)); // UserTest → userTest
bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-websocket
bindMap.put("kebabCaseEntityName", StrUtil.toSymbolCase(entityName, '-')); // UserTest → user-test
bindMap.put("businessName", genConfig.getBusinessName());
bindMap.put("fieldConfigs", fieldConfigs);
@@ -252,7 +256,15 @@ 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 ("VIEW".equals(FileNameUtil.mainName(path))) {
// 无法通过文件名区分时,依据子包名与扩展名判断
}
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 +276,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 +300,8 @@ public class CodegenServiceImpl implements CodegenService {
* @param tableName 表名
* @param zip 压缩文件输出流
*/
private void generateAndZipCode(String tableName, ZipOutputStream zip) {
List<CodegenPreviewVO> codePreviewList = getCodegenPreviewData(tableName);
private void generateAndZipCode(String tableName, ZipOutputStream zip, String ui) {
List<CodegenPreviewVO> codePreviewList = getCodegenPreviewData(tableName, ui);
for (CodegenPreviewVO codePreview : codePreviewList) {
String fileName = codePreview.getFileName();

View File

@@ -83,8 +83,13 @@ public class GenConfigServiceImpl extends ServiceImpl<GenConfigMapper, GenConfig
if (StrUtil.isNotBlank(tableComment)) {
genConfig.setBusinessName(tableComment.replace("", "").trim());
}
// 根据表名生成实体类名 例如sys_user -> 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()); // 默认模块名

View File

@@ -16,10 +16,10 @@ import org.springframework.web.multipart.MultipartFile;
/**
* 文件控制层
*
* @author Ray
* @author Ray.Hao
* @since 2022/10/16
*/
@Tag(name = "08.文件接口")
@Tag(name = "07.文件接口")
@RestController
@RequestMapping("/api/v1/files")
@RequiredArgsConstructor

View File

@@ -5,7 +5,7 @@ import org.springframework.web.bind.annotation.*;
/**
* 邮件控制层
*
* @author Ray
* @author Ray.Hao
* @since 2.10.0
*/
@RestController

View File

@@ -17,7 +17,7 @@ import java.security.Principal;
* <p>
* 包含点对点/广播发送消息
*
* @author Ray
* @author Ray.Hao
* @since 2.3.0
*/
@RestController

View File

@@ -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());
}
}

View File

@@ -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<String> 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<String> getAllOnlineUsers() {
return Collections.unmodifiableSet(onlineUsers);
}
/**
* 获取在线的接收者
* 从所有接收者中过滤出在线的接收者
*
* @param receivers 接收者
* @return 在线的接收者集合
*/
public Set<String> getOnlineReceivers(Set<String> receivers) {
return receivers.stream().filter(onlineUsers::contains).collect(Collectors.toSet());
}
/**
* 获取在线用户数量
*
* @return 在线用户数量
*/
public int getOnlineUserCount() {
return onlineUsers.size();
}
}

View File

@@ -28,7 +28,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
@Slf4j
@RestController
@RequiredArgsConstructor
@Tag(name = "10.系统配置")
@Tag(name = "08.系统配置")
@RequestMapping("/api/v1/config")
public class ConfigController {

View File

@@ -22,7 +22,7 @@ import java.util.List;
/**
* 部门控制器
*
* @author haoxr
* @author Ray.Hao
* @since 2020/11/6
*/
@Tag(name = "05.部门接口")

View File

@@ -1,16 +1,22 @@
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.common.enums.LogModuleEnum;
import com.youlai.boot.system.model.form.DictItemForm;
import com.youlai.boot.system.model.query.DictItemPageQuery;
import com.youlai.boot.system.model.query.DictPageQuery;
import com.youlai.boot.system.model.vo.DictItemOptionVO;
import com.youlai.boot.system.model.vo.DictItemPageVO;
import com.youlai.boot.system.model.vo.DictPageVO;
import com.youlai.boot.common.annotation.RepeatSubmit;
import com.youlai.boot.system.model.form.DictForm;
import com.youlai.boot.common.annotation.Log;
import com.youlai.boot.system.model.vo.DictVO;
import com.youlai.boot.system.service.DictItemService;
import com.youlai.boot.system.service.DictService;
import com.youlai.boot.system.service.WebSocketService;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
@@ -25,17 +31,23 @@ import java.util.List;
/**
* 字典控制层
*
* @author Ray
* @author Ray.Hao
* @since 2.9.0
*/
@Tag(name = "06.字典接口")
@RestController
@RequestMapping("/api/v1/dict")
@SuppressWarnings("SpellCheckingInspection")
@RequestMapping("/api/v1/dicts")
@RequiredArgsConstructor
public class DictController {
private final DictService dictService;
private final DictItemService dictItemService;
private final WebSocketService webSocketService;
//---------------------------------------------------
// 字典相关接口
//---------------------------------------------------
@Operation(summary = "字典分页列表")
@GetMapping("/page")
@Log( value = "字典分页列表",module = LogModuleEnum.DICT)
@@ -46,14 +58,15 @@ public class DictController {
return PageResult.success(result);
}
@Operation(summary = "所有字典列表")
@GetMapping("/list")
public Result<List<DictVO>> getAllDictWithData() {
List<DictVO> list = dictService.getAllDictWithData();
@Operation(summary = "字典列表")
@GetMapping
public Result<List<Option<String>>> getDictList() {
List<Option<String>> list = dictService.getDictList();
return Result.success(list);
}
@Operation(summary = "字典表单")
@Operation(summary = "获取字典表单数据")
@GetMapping("/{id}/form")
public Result<DictForm> getDictForm(
@Parameter(description = "字典ID") @PathVariable Long id
@@ -68,6 +81,10 @@ public class DictController {
@RepeatSubmit
public Result<?> saveDict(@Valid @RequestBody DictForm formData) {
boolean result = dictService.saveDict(formData);
// 发送字典更新通知
if (result) {
webSocketService.broadcastDictChange(formData.getDictCode());
}
return Result.judge(result);
}
@@ -76,9 +93,13 @@ public class DictController {
@PreAuthorize("@ss.hasPerm('sys:dict:edit')")
public Result<?> updateDict(
@PathVariable Long id,
@RequestBody DictForm DictForm
@RequestBody DictForm dictForm
) {
boolean status = dictService.updateDict(id, DictForm);
boolean status = dictService.updateDict(id, dictForm);
// 发送字典更新通知
if (status && dictForm.getDictCode() != null) {
webSocketService.broadcastDictChange(dictForm.getDictCode());
}
return Result.judge(status);
}
@@ -88,7 +109,105 @@ public class DictController {
public Result<?> deleteDictionaries(
@Parameter(description = "字典ID多个以英文逗号(,)拼接") @PathVariable String ids
) {
// 获取字典编码列表,用于发送删除通知
List<String> dictCodes = dictService.getDictCodesByIds(Arrays.stream(ids.split(",")).toList());
dictService.deleteDictByIds(Arrays.stream(ids.split(",")).toList());
// 发送字典删除通知
for (String dictCode : dictCodes) {
webSocketService.broadcastDictChange(dictCode);
}
return Result.success();
}
//---------------------------------------------------
// 字典项相关接口
//---------------------------------------------------
@Operation(summary = "字典项分页列表")
@GetMapping("/{dictCode}/items/page")
public PageResult<DictItemPageVO> getDictItemPage(
@PathVariable String dictCode,
DictItemPageQuery queryParams
) {
queryParams.setDictCode(dictCode);
Page<DictItemPageVO> result = dictItemService.getDictItemPage(queryParams);
return PageResult.success(result);
}
@Operation(summary = "字典项列表")
@GetMapping("/{dictCode}/items")
public Result<List<DictItemOptionVO>> getDictItems(
@Parameter(description = "字典编码") @PathVariable String dictCode
) {
List<DictItemOptionVO> list = dictItemService.getDictItems(dictCode);
return Result.success(list);
}
@Operation(summary = "新增字典项")
@PostMapping("/{dictCode}/items")
@PreAuthorize("@ss.hasPerm('sys:dict-item:add')")
@RepeatSubmit
public Result<Void> saveDictItem(
@PathVariable String dictCode,
@Valid @RequestBody DictItemForm formData
) {
formData.setDictCode(dictCode);
boolean result = dictItemService.saveDictItem(formData);
// 发送字典更新通知
if (result) {
webSocketService.broadcastDictChange(dictCode);
}
return Result.judge(result);
}
@Operation(summary = "字典项表单数据")
@GetMapping("/{dictCode}/items/{itemId}/form")
public Result<DictItemForm> getDictItemForm(
@PathVariable String dictCode,
@Parameter(description = "字典项ID") @PathVariable Long itemId
) {
DictItemForm formData = dictItemService.getDictItemForm(itemId);
return Result.success(formData);
}
@Operation(summary = "修改字典项")
@PutMapping("/{dictCode}/items/{itemId}")
@PreAuthorize("@ss.hasPerm('sys:dict-item:edit')")
@RepeatSubmit
public Result<?> updateDictItem(
@PathVariable String dictCode,
@PathVariable Long itemId,
@RequestBody DictItemForm formData
) {
formData.setId(itemId);
formData.setDictCode(dictCode);
boolean status = dictItemService.updateDictItem(formData);
// 发送字典更新通知
if (status) {
webSocketService.broadcastDictChange(dictCode);
}
return Result.judge(status);
}
@Operation(summary = "删除字典项")
@DeleteMapping("/{dictCode}/items/{itemIds}")
@PreAuthorize("@ss.hasPerm('sys:dict-item:delete')")
public Result<Void> deleteDictItems(
@PathVariable String dictCode,
@Parameter(description = "字典ID多个以英文逗号(,)拼接") @PathVariable String itemIds
) {
dictItemService.deleteDictItemByIds(itemIds);
// 发送字典更新通知
webSocketService.broadcastDictChange(dictCode);
return Result.success();
}

View File

@@ -1,96 +0,0 @@
package com.youlai.boot.system.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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.PageResult;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.system.model.form.DictDataForm;
import com.youlai.boot.system.model.query.DictDataPageQuery;
import com.youlai.boot.system.model.vo.DictDataPageVO;
import com.youlai.boot.system.service.DictDataService;
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 org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 字典数据控制层
*
* @author Ray
* @since 2.9.0
*/
@Tag(name = "07.字典数据接口")
@RestController
@RequestMapping("/api/v1/dict-data")
@RequiredArgsConstructor
public class DictDataController {
private final DictDataService dictDataService;
@Operation(summary = "字典数据分页列表")
@GetMapping("/page")
@Log( value = "字典数据分页列表",module = LogModuleEnum.DICT)
public PageResult<DictDataPageVO> getDictDataPage(
DictDataPageQuery queryParams
) {
Page<DictDataPageVO> result = dictDataService.getDictDataPage(queryParams);
return PageResult.success(result);
}
@Operation(summary = "获取字典数据表单")
@GetMapping("/{id}/form")
public Result<DictDataForm> getDictDataForm(
@Parameter(description = "字典数据ID") @PathVariable Long id
) {
DictDataForm formData = dictDataService.getDictDataForm(id);
return Result.success(formData);
}
@Operation(summary = "新增字典数据")
@PostMapping
@PreAuthorize("@ss.hasPerm('sys:dict-data:add')")
@RepeatSubmit
public Result<Void> saveDictData(@Valid @RequestBody DictDataForm formData) {
boolean result = dictDataService.saveDictData(formData);
return Result.judge(result);
}
@Operation(summary = "修改字典数据")
@PutMapping("/{id}")
@PreAuthorize("@ss.hasPerm('sys:dict-data:edit')")
public Result<?> updateDictData(
@PathVariable Long id,
@RequestBody DictDataForm formData
) {
boolean status = dictDataService.updateDictData(formData);
return Result.judge(status);
}
@Operation(summary = "删除字典数据")
@DeleteMapping("/{ids}")
@PreAuthorize("@ss.hasPerm('sys:dict-data:delete')")
public Result<Void> deleteDictionaries(
@Parameter(description = "字典ID多个以英文逗号(,)拼接") @PathVariable String ids
) {
dictDataService.deleteDictDataByIds(ids);
return Result.success();
}
@Operation(summary = "字典数据列表")
@GetMapping("/{dictCode}/options")
public Result<List<Option<String>>> getDictDataList(
@Parameter(description = "字典编码") @PathVariable String dictCode
) {
List<Option<String>> options = dictDataService.getDictDataList(dictCode);
return Result.success(options);
}
}

View File

@@ -15,8 +15,6 @@ import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
/**
* 日志控制层
@@ -24,7 +22,7 @@ import java.util.List;
* @author Ray.Hao
* @since 2.10.0
*/
@Tag(name = "13.日志接口")
@Tag(name = "10.日志接口")
@RestController
@RequestMapping("/api/v1/logs")
@RequiredArgsConstructor

View File

@@ -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.common.result.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,12 +19,11 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Set;
/**
* 菜单控制层
*
* @author Ray
* @author Ray.Hao
* @since 2020/11/06
*/
@Tag(name = "04.菜单接口")
@@ -64,6 +62,7 @@ public class MenuController {
@Operation(summary = "菜单表单数据")
@GetMapping("/{id}/form")
@PreAuthorize("@ss.hasPerm('sys:menu:edit')")
public Result<MenuForm> 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

View File

@@ -19,14 +19,13 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 通知公告前端控制层
*
* @author youlaitech
* @since 2024-08-27 10:31
*/
@Tag(name = "12.通知公告接口")
@Tag(name = "09.通知公告")
@RestController
@RequestMapping("/api/v1/notices")
@RequiredArgsConstructor

View File

@@ -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<RoleForm> 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<Void> assignMenusToRole(
@PathVariable Long roleId,

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.system.controller;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import cn.idev.excel.EasyExcel;
import cn.idev.excel.ExcelWriter;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.youlai.boot.common.annotation.Log;
@@ -19,7 +19,7 @@ import com.youlai.boot.system.model.dto.UserImportDTO;
import com.youlai.boot.system.model.entity.User;
import com.youlai.boot.system.model.form.*;
import com.youlai.boot.system.model.query.UserPageQuery;
import com.youlai.boot.system.model.vo.UserInfoVO;
import com.youlai.boot.system.model.dto.CurrentUserDTO;
import com.youlai.boot.system.model.vo.UserPageVO;
import com.youlai.boot.system.model.vo.UserProfileVO;
import com.youlai.boot.system.service.UserService;
@@ -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<UserForm> 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<Void> updateUserStatus(
@Parameter(description = "用户ID") @PathVariable Long userId,
@@ -128,15 +130,15 @@ public class UserController {
@Operation(summary = "获取当前登录用户信息")
@GetMapping("/me")
@Log(value = "获取当前登录用户信息", module = LogModuleEnum.USER)
public Result<UserInfoVO> getCurrentUserInfo() {
UserInfoVO userInfoVO = userService.getCurrentUserInfo();
return Result.success(userInfoVO);
public Result<CurrentUserDTO> getCurrentUser() {
CurrentUserDTO currentUserDTO = userService.getCurrentUserInfo();
return Result.success(currentUserDTO);
}
@Operation(summary = "用户导入模板下载")
@GetMapping("/template")
@Log(value = "用户导入模板下载", module = LogModuleEnum.USER)
public void downloadTemplate(HttpServletResponse response) throws IOException {
public void downloadTemplate(HttpServletResponse response) {
String fileName = "用户导入模板.xlsx";
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
@@ -144,14 +146,17 @@ public class UserController {
String fileClassPath = "templates" + File.separator + "excel" + File.separator + fileName;
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(fileClassPath);
ServletOutputStream outputStream = response.getOutputStream();
ExcelWriter excelWriter = EasyExcel.write(outputStream).withTemplate(inputStream).build();
excelWriter.finish();
try (ServletOutputStream outputStream = response.getOutputStream();
ExcelWriter excelWriter = EasyExcel.write(outputStream).withTemplate(inputStream).build()) {
excelWriter.finish();
} catch (IOException e) {
throw new RuntimeException("用户导入模板下载失败", e);
}
}
@Operation(summary = "导入用户")
@PostMapping("/import")
@PreAuthorize("@ss.hasPerm('sys:user:import')")
@Log(value = "导入用户", module = LogModuleEnum.USER)
public Result<ExcelResult> importUsers(MultipartFile file) throws IOException {
UserImportListener listener = new UserImportListener();
@@ -161,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";
@@ -191,7 +197,7 @@ public class UserController {
@Operation(summary = "重置用户密码")
@PutMapping(value = "/{userId}/password/reset")
@PreAuthorize("@ss.hasPerm('sys:user:password:reset')")
@PreAuthorize("@ss.hasPerm('sys:user:reset-password')")
public Result<?> resetPassword(
@Parameter(description = "用户ID") @PathVariable Long userId,
@RequestParam String password
@@ -246,7 +252,7 @@ public class UserController {
return Result.judge(result);
}
@Operation(summary = "用户下拉选项")
@Operation(summary = "获取用户下拉选项")
@GetMapping("/options")
public Result<List<Option<String>>> listUserOptions() {
List<Option<String>> list = userService.listUserOptions();

View File

@@ -1,30 +0,0 @@
package com.youlai.boot.system.converter;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.system.model.entity.DictData;
import com.youlai.boot.system.model.form.DictDataForm;
import com.youlai.boot.system.model.vo.DictPageVO;
import com.youlai.boot.common.model.Option;
import com.youlai.boot.system.model.form.DictForm;
import org.mapstruct.Mapper;
import java.util.List;
/**
* 字典项 对象转换器
*
* @author Ray
* @since 2022/6/8
*/
@Mapper(componentModel = "spring")
public interface DictDataConverter {
Page<DictPageVO> toPageVo(Page<DictData> page);
DictDataForm toForm(DictData entity);
DictData toEntity(DictDataForm formFata);
Option<Long> toOption(DictData dictData);
List<Option<Long>> toOption(List<DictData> dictData);
}

View File

@@ -0,0 +1,29 @@
package com.youlai.boot.system.converter;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.system.model.entity.DictItem;
import com.youlai.boot.system.model.form.DictItemForm;
import com.youlai.boot.system.model.vo.DictPageVO;
import com.youlai.boot.common.model.Option;
import org.mapstruct.Mapper;
import java.util.List;
/**
* 字典项对象转换器
*
* @author Ray.Hao
* @since 2022/6/8
*/
@Mapper(componentModel = "spring")
public interface DictItemConverter {
Page<DictPageVO> toPageVo(Page<DictItem> page);
DictItemForm toForm(DictItem entity);
DictItem toEntity(DictItemForm formFata);
Option<Long> toOption(DictItem dictItem);
List<Option<Long>> toOption(List<DictItem> dictData);
}

View File

@@ -3,7 +3,7 @@ package com.youlai.boot.system.converter;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.common.model.Option;
import com.youlai.boot.system.model.entity.User;
import com.youlai.boot.system.model.vo.UserInfoVO;
import com.youlai.boot.system.model.dto.CurrentUserDTO;
import com.youlai.boot.system.model.vo.UserPageVO;
import com.youlai.boot.system.model.vo.UserProfileVO;
import com.youlai.boot.system.model.bo.UserBO;
@@ -38,12 +38,12 @@ public interface UserConverter {
@Mappings({
@Mapping(target = "userId", source = "id")
})
UserInfoVO toUserInfoVo(User entity);
CurrentUserDTO toCurrentUserDto(User entity);
User toEntity(UserImportDTO vo);
UserProfileVO toProfileVO(UserBO bo);
UserProfileVO toProfileVo(UserBO bo);
User toEntity(UserProfileForm formData);

View File

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

View File

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

View File

@@ -6,8 +6,8 @@ import cn.hutool.core.lang.Validator;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import cn.idev.excel.context.AnalysisContext;
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;
@@ -50,7 +50,7 @@ public class UserImportListener extends AnalysisEventListener<UserImportDTO> {
private final List<Role> roleList;
private final List<Dept> deptList;
private final List<DictData> genderList;
private final List<DictItem> genderList;
/**
* 当前行
@@ -71,8 +71,8 @@ public class UserImportListener extends AnalysisEventListener<UserImportDTO> {
.select(Role::getId, Role::getCode));
this.deptList = SpringUtil.getBean(DeptService.class)
.list(new LambdaQueryWrapper<Dept>().select(Dept::getId, Dept::getCode));
this.genderList = SpringUtil.getBean(DictDataService.class)
.list(new LambdaQueryWrapper<DictData>().eq(DictData::getDictCode, DictCodeEnum.GENDER.getValue()));
this.genderList = SpringUtil.getBean(DictItemService.class)
.list(new LambdaQueryWrapper<DictItem>().eq(DictItem::getDictCode, DictCodeEnum.GENDER.getValue()));
this.excelResult = new ExcelResult();
}
@@ -202,7 +202,7 @@ public class UserImportListener extends AnalysisEventListener<UserImportDTO> {
return this.genderList.stream()
.filter(r -> r.getLabel().equals(genderLabel))
.findFirst()
.map(DictData::getValue)
.map(DictItem::getValue)
.map(Convert::toInt)
.orElse(null);
}

View File

@@ -1,28 +0,0 @@
package com.youlai.boot.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.common.model.Option;
import com.youlai.boot.system.model.entity.DictData;
import com.youlai.boot.system.model.query.DictDataPageQuery;
import com.youlai.boot.system.model.vo.DictDataPageVO;
import org.apache.ibatis.annotations.Mapper;
/**
* 字典数据映射层
*
* @author Ray Hao
* @since 2.9.0
*/
@Mapper
public interface DictDataMapper extends BaseMapper<DictData> {
/**
* 字典数据分页列表
*/
Page<DictDataPageVO> getDictDataPage(Page<DictDataPageVO> page, DictDataPageQuery queryParams);
}

View File

@@ -0,0 +1,27 @@
package com.youlai.boot.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.system.model.entity.DictItem;
import com.youlai.boot.system.model.query.DictItemPageQuery;
import com.youlai.boot.system.model.vo.DictItemPageVO;
import org.apache.ibatis.annotations.Mapper;
/**
* 字典项映射层
*
* @author Ray Hao
* @since 2.9.0
*/
@Mapper
public interface DictItemMapper extends BaseMapper<DictItem> {
/**
* 字典项分页列表
*/
Page<DictItemPageVO> getDictItemPage(Page<DictItemPageVO> page, DictItemPageQuery queryParams);
}

View File

@@ -2,15 +2,11 @@ package com.youlai.boot.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.common.model.Option;
import com.youlai.boot.system.model.entity.Dict;
import com.youlai.boot.system.model.query.DictPageQuery;
import com.youlai.boot.system.model.vo.DictPageVO;
import com.youlai.boot.system.model.vo.DictVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* 字典 访问层
*
@@ -29,13 +25,6 @@ public interface DictMapper extends BaseMapper<Dict> {
*/
Page<DictPageVO> getDictPage(Page<DictPageVO> page, DictPageQuery queryParams);
/**
* 获取字典列表(包含字典数据)
*
* @return 字典列表
*/
List<DictVO> getAllDictWithData();
}

View File

@@ -6,14 +6,19 @@ import org.apache.ibatis.annotations.Mapper;
import java.util.Set;
/**
* 角色持久层接口
*
* @author Ray.Hao
* @since 2022/1/14
*/
@Mapper
public interface RoleMapper extends BaseMapper<Role> {
/**
* 获取最大范围的数据权限
*
* @param roles
* @param roles 角色编码集合
* @return
*/
Integer getMaximumDataScope(Set<String> roles);

View File

@@ -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.system.model.dto.UserAuthInfo;
import com.youlai.boot.core.security.model.UserAuthCredentials;
import com.youlai.boot.system.model.dto.UserExportDTO;
import org.apache.ibatis.annotations.Mapper;
@@ -46,7 +46,7 @@ public interface UserMapper extends BaseMapper<User> {
* @param username 用户名
* @return 认证信息
*/
UserAuthInfo getUserAuthInfo(String username);
UserAuthCredentials getAuthCredentialsByUsername(String username);
/**
* 根据微信openid获取用户认证信息
@@ -54,7 +54,7 @@ public interface UserMapper extends BaseMapper<User> {
* @param openid 微信openid
* @return 认证信息
*/
UserAuthInfo getUserAuthInfoByOpenId(String openid);
UserAuthCredentials getAuthCredentialsByOpenId(String openid);
/**
* 根据手机号获取用户认证信息
@@ -62,7 +62,7 @@ public interface UserMapper extends BaseMapper<User> {
* @param mobile 手机号
* @return 认证信息
*/
UserAuthInfo getUserAuthInfoByMobile(String mobile);
UserAuthCredentials getAuthCredentialsByMobile(String mobile);
/**
* 获取导出用户列表

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.system.model.vo;
package com.youlai.boot.system.model.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@@ -6,14 +6,14 @@ import lombok.Data;
import java.util.Set;
/**
* 用户登录视图对象
* 当前登录用户对象
*
* @author haoxr
* @since 2022/1/14
*/
@Schema(description ="当前登录用户视图对象")
@Schema(description ="当前登录用户对象")
@Data
public class UserInfoVO {
public class CurrentUserDTO {
@Schema(description="用户ID")
private Long userId;

View File

@@ -1,8 +1,8 @@
package com.youlai.boot.system.model.dto;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.format.DateTimeFormat;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import lombok.Data;
import java.time.LocalDateTime;

View File

@@ -1,12 +1,12 @@
package com.youlai.boot.system.model.dto;
import com.alibaba.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* 用户导入对象
*
* @author haoxr
* @author Ray.Hao
* @since 2022/4/10
*/
@Data

View File

@@ -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<String> sessionIds;
/**
* 最后活动时间
*/
private long lastActiveTime;
public UserSessionDTO(String username) {
this.username = username;
this.sessionIds = new HashSet<>();
this.lastActiveTime = System.currentTimeMillis();
}
}

View File

@@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.EqualsAndHashCode;
/**
* 系统配置 实体
* 系统配置对象
*
* @author Theo
* @since 2024-07-29 11:17:26

View File

@@ -1,15 +1,14 @@
package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.youlai.boot.common.base.BaseEntity;
import lombok.Getter;
import lombok.Setter;
/**
* 部门实体
* 部门实体对象
*
* @author Ray
* @author Ray.Hao
* @since 2024/06/23
*/
@TableName("sys_dept")

View File

@@ -8,7 +8,7 @@ import lombok.EqualsAndHashCode;
/**
* 字典实体
*
* @author haoxr
* @author Ray.Hao
* @since 2022/12/17
*/
@EqualsAndHashCode(callSuper = false)

View File

@@ -1,23 +1,20 @@
package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.youlai.boot.common.base.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 字典数据实体对象
* 字典实体对象
*
* @author haoxr
* @author Ray.Hao
* @since 2022/12/17
*/
@EqualsAndHashCode(callSuper = false)
@TableName("sys_dict_data")
@TableName("sys_dict_item")
@Data
public class DictData extends BaseEntity {
public class DictItem extends BaseEntity {
/**
* 字典编码

View File

@@ -10,7 +10,7 @@ import java.time.LocalDateTime;
/**
* 系统日志 实体类
*
* @author Ray
* @author Ray.Hao
* @since 2.10.0
*/
@Data

View File

@@ -2,7 +2,6 @@ package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.youlai.boot.system.enums.MenuTypeEnum;
import lombok.Getter;
import lombok.Setter;
@@ -11,7 +10,7 @@ import java.time.LocalDateTime;
/**
* 菜单实体
*
* @author Ray
* @author Ray.Hao
* @since 2023/3/6
*/
@TableName("sys_menu")
@@ -37,7 +36,7 @@ public class Menu {
/**
* 菜单类型(1-菜单2-目录3-外链4-按钮权限)
*/
private MenuTypeEnum type;
private Integer type;
/**
* 路由名称Vue Router 中定义的路由名称)

View File

@@ -1,6 +1,5 @@
package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.youlai.boot.common.base.BaseEntity;
import lombok.Getter;
@@ -8,10 +7,11 @@ import lombok.Setter;
import java.io.Serial;
import java.time.LocalDateTime;
/**
* 通知公告实体对象
*
* @author youlaitech
* @author Kylin
* @since 2024-08-27 10:31
*/
@Getter

View File

@@ -8,7 +8,7 @@ import lombok.Setter;
/**
* 角色实体
*
* @author Ray
* @author Ray.Hao
* @since 2024/6/23
*/
@TableName("sys_role")

View File

@@ -1,12 +1,10 @@
package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 角色和菜单关联表
*/
@@ -25,6 +23,4 @@ public class RoleMenu {
*/
private Long menuId;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}

View File

@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
/**
* 用户通知公告实体对象
*
* @author youlaitech
* @author Kylin
* @since 2024-08-28 16:56
*/
@Getter

View File

@@ -1,6 +1,5 @@
package com.youlai.boot.system.model.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -10,7 +9,7 @@ import lombok.NoArgsConstructor;
/**
* 用户和角色关联表
*
* @author haoxr
* @author Rya.Hao
* @since 2022/12/17
*/
@TableName("sys_user_role")
@@ -27,7 +26,4 @@ public class UserRole {
* 角色ID
*/
private Long roleId;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}

View File

@@ -0,0 +1,27 @@
package com.youlai.boot.system.model.event;
import lombok.Data;
/**
* 字典更新事件
*
* @author Ray.Hao
* @since 3.0.0
*/
@Data
public class DictEvent {
/**
* 字典编码
*/
private String dictCode;
/**
* 时间戳
*/
private long timestamp;
public DictEvent(String dictCode) {
this.dictCode = dictCode;
this.timestamp = System.currentTimeMillis();
}
}

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