Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
690265b177 | ||
|
|
62ca74c66a | ||
|
|
3ecb28549f | ||
|
|
a2633b67c9 | ||
|
|
abf7f42773 | ||
|
|
4473e721f8 | ||
|
|
9152ab654d | ||
|
|
2accc6a489 | ||
|
|
1c14e278d5 | ||
|
|
2c5141bad6 | ||
|
|
cf09b39fa0 | ||
|
|
5646f46c58 | ||
|
|
9197065102 | ||
|
|
3e71dd7cae | ||
|
|
1a032365c1 | ||
|
|
42e5499119 | ||
|
|
567446d967 | ||
|
|
18d6a63b18 | ||
|
|
c01e54b9e3 | ||
|
|
a10e12f753 | ||
|
|
7ffad2d71c | ||
|
|
86a9b3e212 | ||
|
|
48ec38e076 | ||
|
|
02f835e59e | ||
|
|
2af4581f2d | ||
|
|
7ecf34cf43 | ||
|
|
395c0f8bfa | ||
|
|
af8b5c847d | ||
|
|
4c492784f3 | ||
|
|
d257cb6b7d | ||
|
|
a4c2048fef | ||
|
|
13ac1dac04 | ||
|
|
b7c4d861f8 | ||
|
|
f62dcb57e3 | ||
|
|
194f3e7ca8 | ||
|
|
be9faa2445 | ||
|
|
7d5b7f0a63 | ||
|
|
8934d2da99 | ||
|
|
1ff53a1c96 | ||
|
|
8f9d828205 | ||
|
|
3d3e7f8c92 | ||
|
|
ecc5f6f780 | ||
|
|
ea7eba05e1 | ||
|
|
fa86addc49 | ||
|
|
42e25a0e58 | ||
|
|
4dfa6ae1d8 | ||
|
|
5aff74d36f | ||
|
|
f06fe3ee01 | ||
|
|
819f4ce587 | ||
|
|
44a62c8d33 | ||
|
|
24bb6bc9a3 | ||
|
|
9bb4b8cb27 | ||
|
|
210c6dee00 | ||
|
|
f03e6fe98f | ||
|
|
4499cc03c4 | ||
|
|
a0ffdb26b8 | ||
|
|
cb6b2a4c13 | ||
|
|
3ba704171d | ||
|
|
94f7a54e5c | ||
|
|
42abc4fb6c | ||
|
|
86c31498ea | ||
|
|
e7dfdabe3c | ||
|
|
a27f7426d9 | ||
|
|
a0a11658de | ||
|
|
abb67a0bf9 | ||
|
|
ded1a527ea | ||
|
|
dc2016d3c7 | ||
|
|
c12f770c71 | ||
|
|
b57853477d | ||
|
|
4451c170c8 | ||
|
|
11603bd864 | ||
|
|
b26accaaa2 | ||
|
|
63b48bc225 | ||
|
|
482bd16f62 | ||
|
|
8cfa734238 | ||
|
|
04fba012b5 | ||
|
|
60cfa78a52 | ||
|
|
594eb2befb | ||
|
|
594a704df3 | ||
|
|
354b9f3c49 | ||
|
|
f924d32417 | ||
|
|
505dcf1a73 | ||
|
|
3244af424d | ||
|
|
7aef158dca | ||
|
|
490a03e85d | ||
|
|
03f383c6bb | ||
|
|
b05adf6549 | ||
|
|
f47c56bef2 | ||
|
|
311434b40b | ||
|
|
007d44e4ae | ||
|
|
60f94fdf7f |
16
Dockerfile
16
Dockerfile
@@ -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
|
||||
|
||||
62
README.md
62
README.md
@@ -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 @@
|
||||

|
||||
|
||||
<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) 修改MySQL、Redis连接配置;
|
||||
默认连接`有来`线上 MySQL/Redis(仅读权限),本地开发时请修改 [application-dev.yml](src/main/resources/application-dev.yml) 中的 MySQL 和 Redis 连接信息。
|
||||
|
||||
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/)
|
||||
|
||||
|
||||
## ✅ 项目统计
|
||||
|
||||

|
||||

|
||||
|
||||
Thanks to all the contributors!
|
||||
|
||||
[](https://github.com/haoxianrui/youlai-boot/graphs/contributors)
|
||||
[](https://github.com/haoxianrui/youlai-boot/graphs/contributors)
|
||||
|
||||
|
||||
## 💖 加交流群
|
||||
|
||||
> **关注「有来技术」公众号,点击菜单“交流群”获取加群二维码。**
|
||||
>
|
||||
> 如果二维码过期,请加微信(haoxianrui)备注「前端」、「后端」或「全栈」拉你进群。
|
||||
>
|
||||
> 交流群仅限技术交流,为过滤广告营销暂设此门槛,感谢理解与配合
|
||||
① 关注「有来技术」公众号,点击菜单 **交流群** 获取加群二维码(此举防止广告进群,感谢理解和支持)。
|
||||
|
||||

|
||||
② 直接添加微信 **`haoxianrui`** 备注「前端/后端/全栈」。
|
||||
|
||||

|
||||
@@ -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
64
pom.xml
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -12,7 +12,6 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@ConfigurationPropertiesScan // 开启配置属性绑定
|
||||
// @EnableScheduling // 开启定时任务
|
||||
public class YouLaiBootApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.shared.auth.enums;
|
||||
package com.youlai.boot.auth.enums;
|
||||
|
||||
/**
|
||||
* EasyCaptcha 验证码类型枚举
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,6 +10,11 @@ package com.youlai.boot.common.constant;
|
||||
*/
|
||||
public interface JwtClaimConstants {
|
||||
|
||||
/**
|
||||
* 令牌类型
|
||||
*/
|
||||
String TOKEN_TYPE = "tokenType";
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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", "第三方系统限流"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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加密一次防止过长
|
||||
|
||||
@@ -21,7 +21,7 @@ import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* IP限流过滤器
|
||||
* IP 限流过滤器
|
||||
*
|
||||
* @author Theo
|
||||
* @since 2024/08/10 14:38
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
|
||||
return;
|
||||
}
|
||||
|
||||
// 将令牌解析为Spring Security认证对象
|
||||
// 将令牌解析为 Spring Security 上下文认证对象
|
||||
Authentication authentication = tokenManager.parseToken(rawToken);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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) {
|
||||
// 默认实现可以是空的,或者抛出不支持的操作异常
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.modules.member.controller;
|
||||
package com.youlai.boot.module.member.controller;
|
||||
|
||||
/**
|
||||
* 会员控制层-业务模块演示
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.modules.member.mapper;
|
||||
package com.youlai.boot.module.member.mapper;
|
||||
|
||||
/**
|
||||
* 会员数据访问层-业务模块演示
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.modules.member.model;
|
||||
package com.youlai.boot.module.member.model;
|
||||
|
||||
/**
|
||||
* 会员实体-业务模块演示
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.modules.member.service;
|
||||
package com.youlai.boot.module.member.service;
|
||||
|
||||
/**
|
||||
* 会员管理服务类-业务模块演示
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.modules.order.controller;
|
||||
package com.youlai.boot.module.order.controller;
|
||||
|
||||
/**
|
||||
* 订单控制层-业务模块演示
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.modules.order.mapper;
|
||||
package com.youlai.boot.module.order.mapper;
|
||||
|
||||
/**
|
||||
* 订单数据访问层-业务模块演示
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.modules.order.model;
|
||||
package com.youlai.boot.module.order.model;
|
||||
|
||||
/**
|
||||
* 订单实体-业务模块演示
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.modules.order.service;
|
||||
package com.youlai.boot.module.order.service;
|
||||
|
||||
/**
|
||||
* 订单管理服务类-业务模块演示
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.modules.product.controller;
|
||||
package com.youlai.boot.module.product.controller;
|
||||
|
||||
/**
|
||||
* 商品控制层-业务模块演示
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.modules.product.mapper;
|
||||
package com.youlai.boot.module.product.mapper;
|
||||
|
||||
/**
|
||||
* 商品数据访问层-业务模块演示
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.modules.product.model;
|
||||
package com.youlai.boot.module.product.model;
|
||||
|
||||
/**
|
||||
* 商品实体-业务模块演示
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.modules.product.service;
|
||||
package com.youlai.boot.module.product.service;
|
||||
|
||||
/**
|
||||
* 会员管理服务类-业务模块演示
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -51,4 +51,14 @@ public class GenConfig extends BaseEntity {
|
||||
* 作者
|
||||
*/
|
||||
private String author;
|
||||
|
||||
/**
|
||||
* 页面类型 classic|curd
|
||||
*/
|
||||
private String pageType;
|
||||
|
||||
/**
|
||||
* 要移除的表前缀,如: sys_
|
||||
*/
|
||||
private String removeTablePrefix;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()); // 默认模块名
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
/**
|
||||
* 邮件控制层
|
||||
*
|
||||
* @author Ray
|
||||
* @author Ray.Hao
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@RestController
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.security.Principal;
|
||||
* <p>
|
||||
* 包含点对点/广播发送消息
|
||||
*
|
||||
* @author Ray
|
||||
* @author Ray.Hao
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@RestController
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import java.util.List;
|
||||
/**
|
||||
* 部门控制器
|
||||
*
|
||||
* @author haoxr
|
||||
* @author Ray.Hao
|
||||
* @since 2020/11/6
|
||||
*/
|
||||
@Tag(name = "05.部门接口")
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 获取导出用户列表
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 系统配置 实体
|
||||
* 系统配置对象
|
||||
*
|
||||
* @author Theo
|
||||
* @since 2024-07-29 11:17:26
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -8,7 +8,7 @@ import lombok.EqualsAndHashCode;
|
||||
/**
|
||||
* 字典实体
|
||||
*
|
||||
* @author haoxr
|
||||
* @author Ray.Hao
|
||||
* @since 2022/12/17
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
/**
|
||||
* 字典编码
|
||||
@@ -10,7 +10,7 @@ import java.time.LocalDateTime;
|
||||
/**
|
||||
* 系统日志 实体类
|
||||
*
|
||||
* @author Ray
|
||||
* @author Ray.Hao
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Data
|
||||
|
||||
@@ -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 中定义的路由名称)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@ import lombok.Setter;
|
||||
/**
|
||||
* 角色实体
|
||||
*
|
||||
* @author Ray
|
||||
* @author Ray.Hao
|
||||
* @since 2024/6/23
|
||||
*/
|
||||
@TableName("sys_role")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
/**
|
||||
* 用户通知公告实体对象
|
||||
*
|
||||
* @author youlaitech
|
||||
* @author Kylin
|
||||
* @since 2024-08-28 16:56
|
||||
*/
|
||||
@Getter
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user