Compare commits
60 Commits
v3.5.0
...
9927546b78
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9927546b78 | ||
|
|
b411baaddd | ||
|
|
1635f976c3 | ||
|
|
18e46cb5f2 | ||
|
|
cf1bfa773e | ||
|
|
6f94cf9c84 | ||
|
|
6e09aa273d | ||
|
|
c7c6799af4 | ||
|
|
10eb81ccd1 | ||
|
|
9cd3ff88f8 | ||
|
|
234b12f297 | ||
|
|
8f5c1fc8e4 | ||
|
|
c71becea68 | ||
|
|
8188c82c3d | ||
|
|
465e63c99d | ||
|
|
ba6203424a | ||
|
|
69f062d6b9 | ||
|
|
63c8cbc873 | ||
|
|
2931153422 | ||
|
|
5a86f69ecf | ||
|
|
6438c1ec2f | ||
|
|
f311b88f31 | ||
|
|
6b7cfee7b4 | ||
|
|
6119d730d8 | ||
|
|
5b378a26ca | ||
|
|
9a528f1191 | ||
|
|
ab7a878b93 | ||
|
|
a71a423313 | ||
|
|
e69baa6785 | ||
|
|
27a8f0e6a5 | ||
|
|
9d117ce884 | ||
|
|
51efe841c7 | ||
|
|
ef30d000bc | ||
|
|
24952afb67 | ||
|
|
3d407e3d91 | ||
|
|
e877ba125b | ||
|
|
5048bf460e | ||
|
|
739311381c | ||
|
|
f32996d9c2 | ||
|
|
b1b3cb1421 | ||
|
|
e794ffa03d | ||
|
|
3067a3672c | ||
|
|
d379e30d3f | ||
|
|
8df1252ff8 | ||
|
|
4402571ab1 | ||
|
|
faf6754bf4 | ||
|
|
3a35b24476 | ||
|
|
6f4e4eb6d6 | ||
|
|
e735f768b5 | ||
|
|
0fb278f6ff | ||
|
|
cdd482f41c | ||
|
|
e24efe6ed6 | ||
|
|
f9fb9b6eb9 | ||
|
|
37ae4c102a | ||
|
|
c30988b2da | ||
|
|
b6dee3eccf | ||
|
|
43394dd402 | ||
|
|
9432448d93 | ||
|
|
8a4994bf23 | ||
|
|
86f2b386a6 |
35
.editorconfig
Normal file
35
.editorconfig
Normal file
@@ -0,0 +1,35 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Unix-style newlines with a newline ending every file
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# Java files
|
||||
[*.java]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# XML files
|
||||
[*.xml]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# YAML files
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# Properties files
|
||||
[*.properties]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# Markdown files
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
14
.gitattributes
vendored
Normal file
14
.gitattributes
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto eol=lf
|
||||
|
||||
# Java files
|
||||
*.java text eol=lf
|
||||
*.xml text eol=lf
|
||||
|
||||
# Config files
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.properties text eol=lf
|
||||
|
||||
# Shell scripts
|
||||
*.sh text eol=lf
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,4 +17,3 @@ docker/*/data/
|
||||
docker/minio/config
|
||||
docker/xxljob/logs
|
||||
application-youlai.yml
|
||||
youlai_admin_template.sql
|
||||
60
CHANGELOG.md
60
CHANGELOG.md
@@ -1,60 +0,0 @@
|
||||
# 2.7.1 (2024/4/18)
|
||||
### 🐛 fix
|
||||
- 修复用户名或者密码错误时,返回的错误信息不正确问题
|
||||
### 🛠️ refactor
|
||||
- JWT 解析和验证代码优化重构
|
||||
- 优化代码结构和完善注释,提高代码可读性
|
||||
|
||||
# 2.7.0 (2024/4/13)
|
||||
### ✨ feat
|
||||
- 集成 Mybatis-Plus generator 代码生成器
|
||||
|
||||
# 2.6.0 (2024/3/6)
|
||||
|
||||
### ✨ feat
|
||||
- 黑名单方式实现 JWT 主动注销过期
|
||||
### 🛠️ refactor
|
||||
- 角色权限重构
|
||||
|
||||
|
||||
# 2.5.0 (2023/12/6)
|
||||
### ✨ feat
|
||||
- [集成 Spring Cache 和 Redis 缓存,路由缓存](https://blog.csdn.net/u013737132/article/details/134789862)
|
||||
### 🛠️ refactor
|
||||
- 权限判断逻辑调整,用户绑定权限调整为角色绑定权限
|
||||
### fix
|
||||
- [接口无请求权限,Spring Security 自定义异常无效问题修复](https://youlai.blog.csdn.net/article/details/134718249)
|
||||
|
||||
|
||||
# 2.4.1 (2023/11/7)
|
||||
### ✂️ refactor
|
||||
- 项目目录结构优化
|
||||
### ⬆️ chore
|
||||
- 升级 SpringBoot 版本 `3.1.4` → `3.1.5`
|
||||
|
||||
|
||||
# 2.2.1 (2023/5/25)
|
||||
|
||||
### 🐛 fix
|
||||
|
||||
- 修复多级路由的组件路径错误导致页面404问题
|
||||
|
||||
# 2.2.0 (2023/5/21)
|
||||
|
||||
### ✨ feat
|
||||
- 菜单、角色、字典、部门添加接口权限控制
|
||||
|
||||
### 🐛 fix
|
||||
|
||||
- 用户登录权限缓存键值不一致导致获取用户数据权限错误问题修复
|
||||
|
||||
### ✂️ refactor
|
||||
|
||||
- 递归获取菜单、部门属性列表代码重构优化
|
||||
|
||||
### ⬆️ chore
|
||||
- 升级 SpringBoot 版本 `3.0.6` → `3.1.0`
|
||||
|
||||
### 📝 docs
|
||||
- SQL 脚本更新,sys_menu 新增 `tree_path` 字段 (升级需更新SQL脚本)
|
||||
|
||||
@@ -20,4 +20,4 @@ CMD java \
|
||||
-jar /app.jar
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8989
|
||||
EXPOSE 8000
|
||||
|
||||
282
README.md
282
README.md
@@ -1,201 +1,175 @@
|
||||
<div align="center">
|
||||
|
||||
## 🎉 正在参加 Gitee 2025 最受欢迎开源软件评选
|
||||
|
||||
<a href="https://gitee.com/activity/2025opensource?ident=I6VXEH" target="_blank">
|
||||
<img src="https://img.shields.io/badge/VUE3--ELEMENT--ADMIN-点击投票支持-f97316?style=for-the-badge&logo=gitee&logoColor=white&labelColor=111827" alt="投票" height="50"/>
|
||||
</a>
|
||||
|
||||
<p>
|
||||
<strong>📢 投票最后1天!恳请助力一票 🙏 无需重复投,您的支持是我们前行的最大动力!</strong>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
<div align="center">
|
||||
<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.5.6-green.svg"/>
|
||||
<a href="https://gitcode.com/youlai/youlai-boot" target="_blank">
|
||||
<img alt="有来技术" src="https://gitcode.com/youlai/youlai-boot/star/badge.svg"/>
|
||||
</a>
|
||||
<p><b>Spring Boot 4 权限管理系统</b></p>
|
||||
<img alt="Java" src="https://img.shields.io/badge/Java-17-brightgreen.svg"/>
|
||||
<img alt="Spring Boot" src="https://img.shields.io/badge/SpringBoot-4.0.1-green.svg"/>
|
||||
<img alt="License" src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"/>
|
||||
<a href="https://gitee.com/youlaiorg/youlai-boot" target="_blank">
|
||||
<img alt="有来技术" src="https://gitee.com/youlaiorg/youlai-boot/badge/star.svg"/>
|
||||
<img alt="Gitee" src="https://gitee.com/youlaiorg/youlai-boot/badge/star.svg"/>
|
||||
</a>
|
||||
<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"/>
|
||||
<img alt="GitHub" src="https://img.shields.io/github/stars/haoxianrui/youlai-boot.svg?style=social&label=Stars"/>
|
||||
</a>
|
||||
<br/>
|
||||
<img alt="有来技术" src="https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg"/>
|
||||
<a href="https://gitee.com/youlaiorg" target="_blank">
|
||||
<img alt="有来技术" src="https://img.shields.io/badge/Author-有来开源组织-orange.svg"/>
|
||||
<a href="https://gitcode.com/youlai/youlai-boot" target="_blank">
|
||||
<img alt="GitCode" src="https://gitcode.com/youlai/youlai-boot/star/badge.svg"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||

|
||||
<p align="center">
|
||||
<a target="_blank" href="https://vue.youlai.tech/">🖥️ 在线预览</a>
|
||||
<span> | </span>
|
||||
<a target="_blank" href="https://www.youlai.tech/youlai-boot">📑 官方文档</a>
|
||||
<span> | </span>
|
||||
<a target="_blank" href="https://www.youlai.tech">🌐 官网</a>
|
||||
</p>
|
||||
|
||||
<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/youlai-boot">🌐 官网</a>
|
||||
</div>
|
||||
---
|
||||
|
||||
## 📢 项目简介
|
||||
|
||||
基于 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)
|
||||
基于 **JDK 17 + Spring Boot 4 + Spring Security** 构建的前后端分离权限管理系统,是 [**vue3-element-admin**](https://gitee.com/youlaiorg/vue3-element-admin) 的 Java 后端实现。
|
||||
|
||||
- **🚀 开发框架**: 使用 Spring Boot 3 和 Vue 3,以及 Element-Plus 等主流技术栈,
|
||||
实时更新。
|
||||
### 为什么选择 youlai-boot?
|
||||
|
||||
- **🔐 安全认证**: 基于 Spring Security 6 原生架构,集成 JWT 令牌自动续期(无状
|
||||
态)和 Redis 会话多端互斥管理(实时强制离线)双重认证机制,构建企业级身份安全
|
||||
中枢。
|
||||
| 特性 | 说明 |
|
||||
| ----------------- | ----------------------------------------------------- |
|
||||
| 🚀 **最新技术栈** | Spring Boot 4 + JDK 17,持续跟进最新版本 |
|
||||
| 🔐 **企业级认证** | Spring Security + JWT + Redis,支持令牌续期、多端互斥 |
|
||||
| 🔑 **细粒度权限** | RBAC 模型,接口级 + 按钮级权限控制 |
|
||||
| 🛠️ **开箱即用** | 用户、角色、菜单、部门、字典等核心模块 |
|
||||
| 📦 **代码生成** | 内置代码生成器,快速构建 CRUD 功能 |
|
||||
| 🌐 **完整生态** | Web 管理前端 + 移动端配套项目,多语言后端支持 |
|
||||
|
||||
- **🔑 权限管理**: 基于 RBAC 模型,实现细粒度的权限控制,涵盖接口方法和按钮级别
|
||||
。
|
||||
## 🌈 相关项目
|
||||
|
||||
- **🛠️ 功能模块**: 包括用户管理、角色管理、菜单管理、部门管理、字典管理等功能。
|
||||
| 项目 | 技术栈 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) | Vue 3 + Element Plus | 配套前端 |
|
||||
| [vue3-element-template](https://gitee.com/youlaiorg/vue3-element-template) | Vue 3 + Element Plus | 前端精简模板 |
|
||||
| [youlai-boot-tenant](https://gitee.com/youlaiorg/youlai-boot-tenant) | Spring Boot 4 | 多租户 SaaS 版 |
|
||||
| [youlai-boot-flex](https://gitee.com/youlaiorg/youlai-boot-flex) | Spring Boot 3 + MyBatis-Flex | MyBatis-Flex 版 |
|
||||
| [youlai-app](https://gitee.com/youlaiorg/youlai-app) | Vue 3 + uni-app | 移动端应用 |
|
||||
|
||||
## 🌈 项目源码
|
||||
---
|
||||
|
||||
| 项目类型 | 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://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) |
|
||||
- JDK 17+
|
||||
- MySQL 5.7+/8.0+
|
||||
- Redis 6.0+
|
||||
|
||||
## 📁 项目目录
|
||||
### 启动步骤
|
||||
|
||||
<details>
|
||||
<summary> 目录结构 </summary>
|
||||
|
||||
<br>
|
||||
|
||||
```
|
||||
youlai-boot
|
||||
├── docker # Docker 目录
|
||||
│ ├── docker-compose.yml # docker-compose 脚本
|
||||
├── sql # SQL脚本
|
||||
│ ├── mysql # MySQL 脚本
|
||||
├── src # 源码目录
|
||||
│ ├── auth # 认证模块(登录入口)
|
||||
│ ├── common # 公共模块
|
||||
│ │ ├── annotation # 注解定义
|
||||
│ │ ├── base # 基础类
|
||||
│ │ ├── constant # 常量
|
||||
│ │ ├── enums # 枚举类型
|
||||
│ │ ├── model # 数据模型
|
||||
│ │ └── util # 工具类
|
||||
│ ├── config # 自动装配配置
|
||||
│ │ └── property # 配置属性目录
|
||||
│ ├── core # 核心框架
|
||||
│ │ ├── aspect # 切面(日志、防重提交)
|
||||
│ │ ├── exception # 异常处理
|
||||
│ │ ├── filter # 过滤器(请求日志、限流)
|
||||
│ │ ├── validator # 验证器
|
||||
│ │ └── web # Web响应封装(Result、PageResult等)
|
||||
│ ├── platform # 平台服务(通用服务)
|
||||
│ │ ├── codegen # 代码生成模块
|
||||
│ │ ├── file # 文件服务
|
||||
│ │ ├── mail # 邮件服务
|
||||
│ │ ├── sms # 短信服务
|
||||
│ │ └── websocket # WebSocket服务
|
||||
│ ├── plugin # 插件扩展
|
||||
│ │ ├── knife4j # Knife4j 扩展
|
||||
│ │ └── mybatis # Mybatis 扩展
|
||||
│ ├── security # 安全框架(Spring Security)
|
||||
│ │ ├── exception # 安全异常
|
||||
│ │ ├── filter # 安全过滤器
|
||||
│ │ ├── handler # 安全处理器
|
||||
│ │ ├── model # 安全模型
|
||||
│ │ ├── provider # 认证提供者
|
||||
│ │ ├── service # 安全服务
|
||||
│ │ ├── token # Token管理
|
||||
│ │ └── util # 安全工具类
|
||||
│ ├── system # 系统模块
|
||||
│ │ ├── controller # 控制层
|
||||
│ │ ├── converter # MapStruct 转换器
|
||||
│ │ ├── enums # 枚举
|
||||
│ │ ├── handler # 处理器
|
||||
│ │ ├── listener # 监听器
|
||||
│ │ ├── mapper # 数据库访问层
|
||||
│ │ ├── model # 模型层
|
||||
│ │ │ ├── bo # 业务对象
|
||||
│ │ │ ├── dto # 数据传输对象
|
||||
│ │ │ ├── entity # 实体对象
|
||||
│ │ │ ├── event # 事件对象
|
||||
│ │ │ ├── form # 表单对象
|
||||
│ │ │ ├── query # 查询参数对象
|
||||
│ │ │ └── vo # 视图对象
|
||||
│ │ └── service # 业务逻辑层
|
||||
│ └── YouLaiBootApplication # 启动类
|
||||
└── end
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 🚀 项目启动
|
||||
|
||||
📚 完整流程参考:
|
||||
[项目启动](https://www.youlai.tech/youlai-boot/1.%E9%A1%B9%E7%9B%AE%E5%90%AF%E5%8A%A8/)
|
||||
|
||||
1. **克隆项目**
|
||||
**1. 克隆项目**
|
||||
|
||||
```bash
|
||||
git clone https://gitee.com/youlaiorg/youlai-boot.git
|
||||
```
|
||||
|
||||
2. **数据库初始化**
|
||||
**2. 初始化数据库**
|
||||
|
||||
执行 [youlai_admin.sql](sql/mysql/youlai_admin.sql) 脚本完成数据库创建、表结
|
||||
构和基础数据的初始化。
|
||||
执行 [youlai_admin.sql](sql/mysql/youlai_admin.sql) 创建数据库和基础数据。
|
||||
|
||||
3. **修改配置**
|
||||
**3. 修改配置**
|
||||
|
||||
默认连接`有来`线上 MySQL/Redis(仅读权限),本地开发时请修改
|
||||
[application-dev.yml](src/main/resources/application-dev.yml) 中的 MySQL 和
|
||||
Redis 连接信息。
|
||||
编辑 [application-dev.yml](src/main/resources/application-dev.yml),配置 MySQL 和 Redis:
|
||||
|
||||
4. **启动项目**
|
||||
```yaml
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/youlai_admin # 数据库连接地址
|
||||
username: root # 数据库用户名
|
||||
password: 123456 # 数据库密码
|
||||
data:
|
||||
redis:
|
||||
host: localhost # Redis 地址
|
||||
port: 6379 # Redis 端口
|
||||
```
|
||||
|
||||
执行
|
||||
[YoulaiBootApplication.java](src/main/java/com/youlai/boot/YoulaiBootApplication.java)
|
||||
的 main 方法完成后端项目启动;
|
||||
> 💡 默认连接线上环境(仅读权限),可直接启动体验。
|
||||
|
||||
访问接口文档地址
|
||||
[http://localhost:8989/doc.html](http://localhost:8989/doc.html) 验证项目启动
|
||||
是否成功。
|
||||
**4. 启动项目**
|
||||
|
||||
## 🚀 项目部署
|
||||
运行 [YoulaiBootApplication.java](src/main/java/com/youlai/boot/YouLaiBootApplication.java),访问 http://localhost:8000/doc.html 查看接口文档。
|
||||
|
||||
参考官方文档:
|
||||
[项目部署指南](https://www.youlai.tech/youlai-boot/5.%E9%A1%B9%E7%9B%AE%E9%83%A8%E7%BD%B2/)
|
||||
---
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
```
|
||||
youlai-boot
|
||||
├── docker/ # Docker 部署
|
||||
├── sql/ # 数据库脚本
|
||||
├── src/
|
||||
│ ├── auth/ # 认证授权业务
|
||||
│ ├── codegen/ # 代码生成器
|
||||
│ ├── common/ # 全局通用(常量、枚举、工具类、统一响应结果)
|
||||
│ ├── framework/ # 底层技术基座(缓存/验证码/安全/Web等)
|
||||
│ │ ├── apidoc/ # OpenAPI/Swagger 文档
|
||||
│ │ ├── cache/ # Redis/Caffeine 缓存
|
||||
│ │ ├── captcha/ # 验证码
|
||||
│ │ ├── integration/ # SMS/Mail/WxMa 集成
|
||||
│ │ ├── job/ # XxlJob 定时任务
|
||||
│ │ ├── mybatis/ # 数据库/MP配置/拦截器
|
||||
│ │ ├── security/ # 鉴权过滤器/Token机制
|
||||
│ │ └── web/ # 跨域/全局异常/限流/Jackson
|
||||
│ ├── message/ # 消息中心
|
||||
│ ├── file/ # 文件中心
|
||||
│ ├── system/ # 核心系统模块(用户/角色/菜单/部门)
|
||||
│ └── YouLaiBootApplication.java # 启动类
|
||||
└── pom.xml # Maven 配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐳 Docker 部署
|
||||
|
||||
```bash
|
||||
cd docker
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
详细文档:[部署指南](https://www.youlai.tech/docs/admin/backend/java/deploy.html)
|
||||
|
||||
---
|
||||
|
||||
## 📚 技术文档
|
||||
|
||||
| 文档 | 地址 |
|
||||
| -------- | ---------------------------------------------------------------------------------- |
|
||||
| 官方文档 | [youlai.tech](https://www.youlai.tech/youlai-boot/) |
|
||||
| 入门指南 | [CSDN 博客](https://youlai.blog.csdn.net/article/details/145177011) |
|
||||
| 接口文档 | [Apifox](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5) |
|
||||
|
||||
---
|
||||
|
||||
## 📄 开源协议
|
||||
|
||||
本项目基于 [Apache 2.0](LICENSE) 协议开源,可免费用于商业项目。
|
||||
|
||||
---
|
||||
|
||||
## ✅ 项目统计
|
||||
|
||||

|
||||

|
||||
|
||||
Thanks to all the contributors!
|
||||
---
|
||||
|
||||
## 🤝 贡献者
|
||||
|
||||
[](https://github.com/haoxianrui/youlai-boot/graphs/contributors)
|
||||
|
||||
## 💖 加交流群
|
||||
---
|
||||
|
||||
① 关注「有来技术」公众号,点击菜单 **交流群** 获取加群二维码(此举防止广告进群,
|
||||
感谢理解和支持)。
|
||||
## 💖 技术交流
|
||||
|
||||
② 直接添加微信 **`haoxianrui`** 备注「前端/后端/全栈」。
|
||||
关注「有来技术」公众号,点击菜单【交流群】获取微信群二维码(为防营销广告,实属无奈,望理解):
|
||||
|
||||

|
||||
<div align="center">
|
||||
<img src="https://foruda.gitee.com/images/1737108820762592766/3390ed0d_716974.png" width="280">
|
||||
</div>
|
||||
|
||||
> 二维码过期?添加微信 **`haoxianrui`**,备注「前端/后端/全栈」即可拉你入群。
|
||||
|
||||
53
pom.xml
53
pom.xml
@@ -6,13 +6,13 @@
|
||||
|
||||
<groupId>com.youlai</groupId>
|
||||
<artifactId>youlai-boot</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<description>基于 Java 17 + SpringBoot 3 + Spring Security 构建的权限管理系统。</description>
|
||||
<version>4.3.0</version>
|
||||
<description>基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。</description>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.6</version> <!-- lookup parent from repository -->
|
||||
<version>4.0.1</version> <!-- lookup parent from repository -->
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
|
||||
<mysql-connector-j.version>9.1.0</mysql-connector-j.version>
|
||||
<druid.version>1.2.24</druid.version>
|
||||
<mybatis-plus.version>3.5.5</mybatis-plus.version>
|
||||
<!-- Spring Boot 4.x 必须使用更新的版本 -->
|
||||
<mybatis-plus.version>3.5.15</mybatis-plus.version>
|
||||
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
|
||||
|
||||
<knife4j.version>4.5.0</knife4j.version>
|
||||
@@ -43,7 +44,7 @@
|
||||
<aliyun-sdk-oss.version>3.16.3</aliyun-sdk-oss.version>
|
||||
|
||||
<!-- redisson 分布式锁 -->
|
||||
<redisson.version>3.51.0</redisson.version>
|
||||
<redisson.version>4.1.0</redisson.version>
|
||||
|
||||
<!-- 自动代码生成 -->
|
||||
<mybatis-plus-generator.version>3.5.6</mybatis-plus-generator.version>
|
||||
@@ -56,14 +57,14 @@
|
||||
<aliyun.java.sdk.core.version>4.7.6</aliyun.java.sdk.core.version>
|
||||
<aliyun.java.sdk.dysmsapi.version>2.2.1</aliyun.java.sdk.dysmsapi.version>
|
||||
|
||||
<!-- 微信 jdk -->
|
||||
<weixin-java.version>4.7.7.B</weixin-java.version>
|
||||
<caffeine.version>2.9.3</caffeine.version>
|
||||
|
||||
<!-- 阿里 TransmittableThreadLocal (支持异步场景的ThreadLocal传递) -->
|
||||
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
|
||||
|
||||
<spring-ai-openai.version>1.1.2</spring-ai-openai.version>
|
||||
<weixin-java-miniapp.version>4.8.1.B</weixin-java-miniapp.version>
|
||||
|
||||
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -121,9 +122,10 @@
|
||||
<artifactId>spring-boot-starter-cache</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot 4.x 已改名 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
<artifactId>spring-boot-starter-aspectj</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
@@ -131,11 +133,6 @@
|
||||
<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>
|
||||
@@ -154,9 +151,15 @@
|
||||
<version>${druid.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot 4.x 必须使用boot4版本 -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<artifactId>mybatis-plus-spring-boot4-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-jsqlparser</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
|
||||
@@ -259,12 +262,6 @@
|
||||
<version>${aliyun.java.sdk.dysmsapi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-miniapp</artifactId>
|
||||
<version>${weixin-java.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 本地缓存 -->
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
@@ -272,6 +269,13 @@
|
||||
<version>${caffeine.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 微信小程序登录 -->
|
||||
<dependency>
|
||||
<groupId>com.github.binarywang</groupId>
|
||||
<artifactId>weixin-java-miniapp</artifactId>
|
||||
<version>${weixin-java-miniapp.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 动态多数据源 -->
|
||||
<!--<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
@@ -279,13 +283,6 @@
|
||||
<version>${dynamic-datasource.version}</version>
|
||||
</dependency>-->
|
||||
|
||||
<!-- Spring AI OpenAI(兼容通义千问、DeepSeek等) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
<version>${spring-ai-openai.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -129,17 +129,16 @@ CREATE TABLE `sys_menu` (
|
||||
`redirect` varchar(128) COMMENT '跳转路径',
|
||||
`create_time` datetime NULL COMMENT '创建时间',
|
||||
`update_time` datetime NULL COMMENT '更新时间',
|
||||
`params` varchar(255) NULL COMMENT '路由参数',
|
||||
`params` json NULL COMMENT '路由参数',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统菜单表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of sys_menu
|
||||
-- ----------------------------
|
||||
-- 顶级目录(1-9):系统/代码生成/AI助手/文档/接口文档/组件/演示/多级/路由
|
||||
-- 顶级目录(1-9):系统/代码生成/文档/接口文档/组件/演示/多级/路由
|
||||
INSERT INTO `sys_menu` VALUES (1, 0, '0', '系统管理', 'C', '', '/system', 'Layout', NULL, NULL, NULL, 1, 1, 'system', '/system/user', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2, 0, '0', '代码生成', 'C', '', '/codegen', 'Layout', NULL, NULL, NULL, 1, 2, 'code', '/codegen/index', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (3, 0, '0', 'AI助手', 'C', '', '/ai', 'Layout', NULL, NULL, NULL, 1, 3, 'ai', '/ai/command-record', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (4, 0, '0', '平台文档', 'C', '', '/doc', 'Layout', NULL, NULL, NULL, 1, 4, 'document', '', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (5, 0, '0', '接口文档', 'C', '', '/api', 'Layout', NULL, NULL, NULL, 1, 5, 'api', '', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (6, 0, '0', '组件封装', 'C', '', '/component', 'Layout', NULL, NULL, NULL, 1, 6, 'menu', '', now(), now(), NULL);
|
||||
@@ -182,13 +181,14 @@ INSERT INTO `sys_menu` VALUES (2502, 250, '0,1,250', '字典新增', 'B', NULL,
|
||||
INSERT INTO `sys_menu` VALUES (2503, 250, '0,1,250', '字典编辑', 'B', NULL, '', NULL, 'sys:dict:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2504, 250, '0,1,250', '字典删除', 'B', NULL, '', NULL, 'sys:dict:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
||||
|
||||
INSERT INTO `sys_menu` VALUES (251, 250, '0,1,250,251', '字典项', 'M', 'DictItem', 'dict-item', 'system/dict/dict-item', NULL, 0, 1, 0, 6, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2511, 251, '0,1,250,251', '字典项查询', 'B', NULL, '', NULL, 'sys:dict-item:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2512, 251, '0,1,250,251', '字典项新增', 'B', NULL, '', NULL, 'sys:dict-item:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2513, 251, '0,1,250,251', '字典项编辑', 'B', NULL, '', NULL, 'sys:dict-item:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2514, 251, '0,1,250,251', '字典项删除', 'B', NULL, '', NULL, 'sys:dict-item:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (251, 1, '0,1', '字典项', 'M', 'DictItem', 'dict-item', 'system/dict/dict-item', NULL, 0, 1, 0, 6, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2511, 251, '0,1,251', '字典项查询', 'B', NULL, '', NULL, 'sys:dict-item:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2512, 251, '0,1,251', '字典项新增', 'B', NULL, '', NULL, 'sys:dict-item:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2513, 251, '0,1,251', '字典项编辑', 'B', NULL, '', NULL, 'sys:dict-item:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2514, 251, '0,1,251', '字典项删除', 'B', NULL, '', NULL, 'sys:dict-item:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
||||
|
||||
INSERT INTO `sys_menu` VALUES (260, 1, '0,1', '系统日志', 'M', 'Log', 'log', 'system/log/index', NULL, 0, 1, 1, 7, 'document', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2601, 260, '0,1,260', '日志查询', 'B', NULL, '', NULL, 'sys:log:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
||||
|
||||
INSERT INTO `sys_menu` VALUES (270, 1, '0,1', '系统配置', 'M', 'Config', 'config', 'system/config/index', NULL, 0, 1, 1, 8, 'setting', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2701, 270, '0,1,270', '系统配置查询', 'B', NULL, '', NULL, 'sys:config:list', 0, 1, 1, 1, '', NULL, now(), now(), NULL);
|
||||
@@ -208,9 +208,6 @@ INSERT INTO `sys_menu` VALUES (2806, 280, '0,1,280', '通知撤回', 'B', NULL,
|
||||
-- 代码生成
|
||||
INSERT INTO `sys_menu` VALUES (310, 2, '0,2', '代码生成', 'M', 'Codegen', 'codegen', 'codegen/index', NULL, NULL, 1, 1, 1, 'code', NULL, now(), now(), NULL);
|
||||
|
||||
-- AI 助手
|
||||
INSERT INTO `sys_menu` VALUES (401, 3, '0,3', 'AI命令记录', 'M', 'ai', 'ai', 'ai/index', NULL, NULL, 1, 1, 1, 'document', NULL, now(), now(), NULL);
|
||||
|
||||
-- 平台文档(外链通过 route_path 识别)
|
||||
INSERT INTO `sys_menu` VALUES (501, 4, '0,4', '平台文档(外链)', 'M', NULL, 'https://juejin.cn/post/7228990409909108793', '', NULL, NULL, NULL, 1, 1, 'document', '', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (502, 4, '0,4', '后端文档', 'M', NULL, 'https://youlai.blog.csdn.net/article/details/145178880', '', NULL, NULL, NULL, 1, 2, 'document', '', now(), now(), NULL);
|
||||
@@ -232,11 +229,10 @@ INSERT INTO `sys_menu` VALUES (708, 6, '0,6', '滚动文本', 'M', 'TextScroll',
|
||||
INSERT INTO `sys_menu` VALUES (709, 6, '0,6', '自适应表格操作列', 'M', 'AutoOperationColumn', 'operation-column', 'demo/auto-operation-column', NULL, NULL, 1, 1, 1, '', '', now(), now(), NULL);
|
||||
|
||||
-- 功能演示
|
||||
INSERT INTO `sys_menu` VALUES (801, 7, '0,7', 'Websocket', 'M', 'WebSocket', '/function/websocket', 'demo/websocket', NULL, NULL, 1, 1, 1, '', '', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (802, 7, '0,7', 'Icons', 'M', 'IconDemo', 'icon-demo', 'demo/icons', NULL, NULL, 1, 1, 2, 'el-icon-Notification', '', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (803, 7, '0,7', '字典实时同步', 'M', 'DictSync', 'dict-sync', 'demo/dict-sync', NULL, NULL, NULL, 1, 3, '', '', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (804, 7, '0,7', 'VxeTable', 'M', 'VxeTable', 'vxe-table', 'demo/vxe-table/index', NULL, NULL, 1, 1, 4, 'el-icon-MagicStick', '', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (805, 7, '0,7', 'CURD单文件', 'M', 'CurdSingle', 'curd-single', 'demo/curd-single', NULL, NULL, 1, 1, 5, 'el-icon-Reading', '', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (801, 7, '0,7', 'Icons', 'M', 'IconDemo', 'icon-demo', 'demo/icons', NULL, NULL, 1, 1, 2, 'el-icon-Notification', '', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (802, 7, '0,7', '字典实时同步', 'M', 'DictSync', 'dict-sync', 'demo/dict-sync', NULL, NULL, NULL, 1, 3, '', '', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (803, 7, '0,7', 'VxeTable', 'M', 'VxeTable', 'vxe-table', 'demo/vxe-table/index', NULL, NULL, 1, 1, 4, 'el-icon-MagicStick', '', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (804, 7, '0,7', 'CURD单文件', 'M', 'CurdSingle', 'curd-single', 'demo/curd-single', NULL, NULL, 1, 1, 5, 'el-icon-Reading', '', now(), now(), NULL);
|
||||
|
||||
-- 多级菜单示例
|
||||
INSERT INTO `sys_menu` VALUES (910, 8, '0,8', '菜单一级', 'C', NULL, 'multi-level1', 'Layout', NULL, 1, NULL, 1, 1, '', '', now(), now(), NULL);
|
||||
@@ -258,7 +254,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 '数据权限(1-所有数据 2-部门及子部门数据 3-本部门数据 4-本人数据)',
|
||||
`data_scope` tinyint NULL COMMENT '数据权限(1-所有数据 2-部门及子部门数据 3-本部门数据 4-本人数据 5-自定义部门数据)',
|
||||
`create_by` bigint NULL COMMENT '创建人 ID',
|
||||
`create_time` datetime NULL COMMENT '创建时间',
|
||||
`update_by` bigint NULL COMMENT '更新人ID',
|
||||
@@ -275,15 +271,10 @@ CREATE TABLE `sys_role` (
|
||||
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);
|
||||
INSERT INTO `sys_role` VALUES (7, '系统管理员4', 'ADMIN4', 7, 1, 1, NULL, now(), NULL, NULL, 0);
|
||||
INSERT INTO `sys_role` VALUES (8, '系统管理员5', 'ADMIN5', 8, 1, 1, NULL, now(), NULL, NULL, 0);
|
||||
INSERT INTO `sys_role` VALUES (9, '系统管理员6', 'ADMIN6', 9, 1, 1, NULL, now(), NULL, NULL, 0);
|
||||
INSERT INTO `sys_role` VALUES (10, '系统管理员7', 'ADMIN7', 10, 1, 1, NULL, now(), NULL, NULL, 0);
|
||||
INSERT INTO `sys_role` VALUES (11, '系统管理员8', 'ADMIN8', 11, 1, 1, NULL, now(), NULL, NULL, 0);
|
||||
INSERT INTO `sys_role` VALUES (12, '系统管理员9', 'ADMIN9', 12, 1, 1, NULL, now(), NULL, NULL, 0);
|
||||
INSERT INTO `sys_role` VALUES (4, '部门主管', 'DEPT_MANAGER', 4, 1, 2, NULL, now(), NULL, now(), 0);
|
||||
INSERT INTO `sys_role` VALUES (5, '部门成员', 'DEPT_MEMBER', 5, 1, 3, NULL, now(), NULL, now(), 0);
|
||||
INSERT INTO `sys_role` VALUES (6, '普通员工', 'EMPLOYEE', 6, 1, 4, NULL, now(), NULL, now(), 0);
|
||||
INSERT INTO `sys_role` VALUES (7, '自定义权限用户', 'CUSTOM_USER', 7, 1, 5, NULL, now(), NULL, now(), 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_role_menu
|
||||
@@ -295,10 +286,26 @@ CREATE TABLE `sys_role_menu` (
|
||||
UNIQUE INDEX `uk_roleid_menuid`(`role_id` ASC, `menu_id` ASC) USING BTREE COMMENT '角色菜单唯一索引'
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '角色菜单关联表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_role_dept
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_role_dept`;
|
||||
CREATE TABLE `sys_role_dept` (
|
||||
`role_id` bigint NOT NULL COMMENT '角色ID',
|
||||
`dept_id` bigint NOT NULL COMMENT '部门ID',
|
||||
UNIQUE INDEX `uk_roleid_deptid`(`role_id` ASC, `dept_id` ASC) USING BTREE COMMENT '角色部门唯一索引'
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '角色部门关联表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of sys_role_dept
|
||||
-- ----------------------------
|
||||
INSERT IGNORE INTO `sys_role_dept` VALUES (7, 1);
|
||||
INSERT IGNORE INTO `sys_role_dept` VALUES (7, 2);
|
||||
|
||||
-- ============================================
|
||||
-- 系统管理员角色菜单权限(role_id=2)
|
||||
-- 顶级目录
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 1), (2, 2), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9);
|
||||
-- 系统管理
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 210), (2, 2101), (2, 2102), (2, 2103), (2, 2104), (2, 2105), (2, 2106), (2, 2107);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 220), (2, 2201), (2, 2202), (2, 2203), (2, 2204), (2, 2205);
|
||||
@@ -306,13 +313,27 @@ INSERT INTO `sys_role_menu` VALUES (2, 230), (2, 2301), (2, 2302), (2, 2303), (2
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 240), (2, 2401), (2, 2402), (2, 2403), (2, 2404);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 250), (2, 2501), (2, 2502), (2, 2503), (2, 2504);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 251), (2, 2511), (2, 2512), (2, 2513), (2, 2514);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 260);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 260), (2, 2601);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 270), (2, 2701), (2, 2702), (2, 2703), (2, 2704), (2, 2705);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 280), (2, 2801), (2, 2802), (2, 2803), (2, 2804), (2, 2805), (2, 2806);
|
||||
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (4, 1);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (4, 210), (4, 2101), (4, 2102), (4, 2103), (4, 2104), (4, 2105), (4, 2106), (4, 2107);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (4, 220), (4, 2201), (4, 2202), (4, 2203), (4, 2204), (4, 2205);
|
||||
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (5, 1);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (5, 210), (5, 2101), (5, 2102), (5, 2103), (5, 2104), (5, 2105), (5, 2106), (5, 2107);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (5, 220), (5, 2201), (5, 2202), (5, 2203), (5, 2204), (5, 2205);
|
||||
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (6, 1);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (6, 210), (6, 2101), (6, 2102), (6, 2103), (6, 2104), (6, 2105), (6, 2106), (6, 2107);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (6, 220), (6, 2201), (6, 2202), (6, 2203), (6, 2204), (6, 2205);
|
||||
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (7, 1);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (7, 210), (7, 2101), (7, 2102), (7, 2103), (7, 2104), (7, 2105), (7, 2106), (7, 2107);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (7, 220), (7, 2201), (7, 2202), (7, 2203), (7, 2204), (7, 2205);
|
||||
-- 代码生成
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 310);
|
||||
-- AI 助手
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 401);
|
||||
-- 平台文档
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 501), (2, 502), (2, 503), (2, 504);
|
||||
-- 接口文档
|
||||
@@ -320,7 +341,7 @@ INSERT INTO `sys_role_menu` VALUES (2, 601);
|
||||
-- 组件封装
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 701), (2, 702), (2, 703), (2, 704), (2, 705), (2, 706), (2, 707), (2, 708), (2, 709);
|
||||
-- 功能演示 / 多级菜单
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 801), (2, 802), (2, 803), (2, 804), (2, 805), (2, 910), (2, 911), (2, 912), (2, 913);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 801), (2, 802), (2, 803), (2, 804), (2, 910), (2, 911), (2, 912), (2, 913);
|
||||
-- 路由参数
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 1001), (2, 1002);
|
||||
|
||||
@@ -344,17 +365,19 @@ CREATE TABLE `sys_user` (
|
||||
`update_time` datetime COMMENT '更新时间',
|
||||
`update_by` bigint COMMENT '修改人ID',
|
||||
`is_deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)',
|
||||
`openid` char(28) COMMENT '微信 openid',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE KEY `uk_username` (`username`)
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统用户表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of sys_user
|
||||
-- ----------------------------
|
||||
INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345677', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0,NULL);
|
||||
INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345678', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0,NULL);
|
||||
INSERT INTO `sys_user` VALUES (3, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345679', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0,NULL);
|
||||
INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345677', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18888888888', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (3, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345679', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (4, 'dept_manager', '部门主管', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345680', 1, 'manager@youlaitech.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (5, 'dept_member', '部门成员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345681', 1, 'member@youlaitech.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (6, 'employee', '普通员工', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 2, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345682', 1, 'employee@youlaitech.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (7, 'custom_user', '自定义权限用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345683', 1, 'custom@youlaitech.com', now(), NULL, now(), NULL, 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_user_role
|
||||
@@ -369,9 +392,13 @@ CREATE TABLE `sys_user_role` (
|
||||
-- ----------------------------
|
||||
-- Records of sys_user_role
|
||||
-- ----------------------------
|
||||
INSERT INTO `sys_user_role` VALUES (1, 1);
|
||||
INSERT INTO `sys_user_role` VALUES (2, 2);
|
||||
INSERT INTO `sys_user_role` VALUES (3, 3);
|
||||
INSERT IGNORE INTO `sys_user_role` VALUES (1, 1);
|
||||
INSERT IGNORE INTO `sys_user_role` VALUES (2, 2);
|
||||
INSERT IGNORE INTO `sys_user_role` VALUES (3, 3);
|
||||
INSERT IGNORE INTO `sys_user_role` VALUES (4, 4);
|
||||
INSERT IGNORE INTO `sys_user_role` VALUES (5, 5);
|
||||
INSERT IGNORE INTO `sys_user_role` VALUES (6, 6);
|
||||
INSERT IGNORE INTO `sys_user_role` VALUES (7, 7);
|
||||
|
||||
|
||||
-- ----------------------------
|
||||
@@ -379,26 +406,30 @@ INSERT INTO `sys_user_role` VALUES (3, 3);
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_log`;
|
||||
CREATE TABLE `sys_log` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`module` varchar(50) NOT NULL COMMENT '日志模块',
|
||||
`request_method` varchar(64) NOT NULL COMMENT '请求方式',
|
||||
`request_params` text COMMENT '请求参数(批量请求参数可能会超过text)',
|
||||
`response_content` mediumtext COMMENT '返回参数',
|
||||
`content` varchar(255) NOT NULL COMMENT '日志内容',
|
||||
`request_uri` varchar(255) COMMENT '请求路径',
|
||||
`method` varchar(255) COMMENT '方法名',
|
||||
`ip` varchar(45) COMMENT 'IP地址',
|
||||
`province` varchar(100) COMMENT '省份',
|
||||
`city` varchar(100) COMMENT '城市',
|
||||
`execution_time` bigint COMMENT '执行时间(ms)',
|
||||
`browser` varchar(100) COMMENT '浏览器',
|
||||
`browser_version` varchar(100) COMMENT '浏览器版本',
|
||||
`os` varchar(100) COMMENT '终端系统',
|
||||
`create_by` bigint COMMENT '创建人ID',
|
||||
`create_time` datetime COMMENT '创建时间',
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`module` TINYINT NOT NULL COMMENT '模块,数字枚举,参考 LogModule 枚举',
|
||||
`action_type` TINYINT NOT NULL COMMENT '操作类型,数字枚举,参考 ActionType 枚举',
|
||||
`title` VARCHAR(100) NOT NULL COMMENT '前端显示标题',
|
||||
`content` TEXT COMMENT '自定义日志内容',
|
||||
`operator_id` BIGINT COMMENT '操作人ID',
|
||||
`operator_name` VARCHAR(50) COMMENT '操作人名称',
|
||||
`request_uri` VARCHAR(255) COMMENT '请求路径',
|
||||
`request_method` VARCHAR(10) COMMENT '请求方法',
|
||||
`ip` VARCHAR(45) COMMENT 'IP地址',
|
||||
`province` VARCHAR(100) COMMENT '省份',
|
||||
`city` VARCHAR(100) COMMENT '城市',
|
||||
`device` VARCHAR(100) COMMENT '设备',
|
||||
`os` VARCHAR(100) COMMENT '操作系统',
|
||||
`browser` VARCHAR(100) COMMENT '浏览器',
|
||||
`status` TINYINT DEFAULT 1 COMMENT '0失败 1成功',
|
||||
`error_msg` VARCHAR(255) COMMENT '错误信息',
|
||||
`execution_time` INT COMMENT '执行时间(ms)',
|
||||
`create_time` DATETIME COMMENT '操作时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `idx_create_time` (`create_time`)
|
||||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表';
|
||||
KEY `idx_module_action_time` (`module`, `action_type`, `create_time`),
|
||||
KEY `idx_operator_time` (`operator_id`, `create_time`),
|
||||
KEY `idx_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for gen_table
|
||||
@@ -513,7 +544,7 @@ CREATE TABLE `sys_user_notice` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
|
||||
`notice_id` bigint NOT NULL COMMENT '公共通知id',
|
||||
`user_id` bigint NOT NULL COMMENT '用户id',
|
||||
`is_read` bigint DEFAULT '0' COMMENT '读取状态(0: 未读, 1: 已读)',
|
||||
`is_read` tinyint DEFAULT '0' COMMENT '读取状态(0: 未读, 1: 已读)',
|
||||
`read_time` datetime COMMENT '阅读时间',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
`update_time` datetime COMMENT '更新时间',
|
||||
@@ -533,36 +564,23 @@ 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);
|
||||
|
||||
-- ----------------------------
|
||||
-- AI 命令记录表
|
||||
-- Table structure for sys_user_social
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `ai_assistant_record`;
|
||||
CREATE TABLE `ai_assistant_record` (
|
||||
DROP TABLE IF EXISTS `sys_user_social`;
|
||||
CREATE TABLE `sys_user_social` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`user_id` bigint DEFAULT NULL COMMENT '用户ID',
|
||||
`username` varchar(64) DEFAULT NULL COMMENT '用户名',
|
||||
`original_command` text COMMENT '原始命令',
|
||||
`ai_provider` varchar(32) DEFAULT NULL COMMENT 'AI 供应商(qwen/openai/deepseek/gemini等)',
|
||||
`ai_model` varchar(64) DEFAULT NULL COMMENT 'AI 模型名称(qwen-plus/qwen-max/gpt-4-turbo等)',
|
||||
`parse_status` tinyint DEFAULT '0' COMMENT '解析是否成功(0-失败, 1-成功)',
|
||||
`function_calls` text COMMENT '解析出的函数调用列表(JSON)',
|
||||
`explanation` varchar(500) DEFAULT NULL COMMENT 'AI的理解说明',
|
||||
`confidence` decimal(3,2) DEFAULT NULL COMMENT '置信度(0.00-1.00)',
|
||||
`parse_error_message` text COMMENT '解析错误信息',
|
||||
`input_tokens` int DEFAULT NULL COMMENT '输入Token数量',
|
||||
`output_tokens` int DEFAULT NULL COMMENT '输出Token数量',
|
||||
`parse_duration_ms` int DEFAULT NULL COMMENT '解析耗时(毫秒)',
|
||||
`function_name` varchar(255) DEFAULT NULL COMMENT '执行的函数名称',
|
||||
`function_arguments` text COMMENT '函数参数(JSON)',
|
||||
`execute_status` tinyint(1) DEFAULT NULL COMMENT '执行状态(0-待执行, 1-成功, -1-失败)',
|
||||
`execute_error_message` text COMMENT '执行错误信息',
|
||||
`ip_address` varchar(128) DEFAULT NULL COMMENT 'IP地址',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
|
||||
`user_id` bigint NOT NULL COMMENT '用户ID',
|
||||
`platform` varchar(20) NOT NULL COMMENT '平台类型(WECHAT_MINI/WECHAT_MP/ALIPAY/QQ/APPLE)',
|
||||
`openid` varchar(64) NOT NULL COMMENT '平台openid',
|
||||
`unionid` varchar(64) DEFAULT NULL COMMENT '微信unionid',
|
||||
`nickname` varchar(64) DEFAULT NULL COMMENT '第三方昵称',
|
||||
`avatar` varchar(255) DEFAULT NULL COMMENT '第三方头像URL',
|
||||
`session_key` varchar(128) DEFAULT NULL COMMENT '微信session_key',
|
||||
`verified` tinyint(1) DEFAULT 1 COMMENT '是否已验证(1-已验证 0-未验证)',
|
||||
`create_time` datetime DEFAULT NULL COMMENT '绑定时间',
|
||||
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_platform_openid` (`platform`, `openid`),
|
||||
KEY `idx_user_id` (`user_id`),
|
||||
KEY `idx_create_time` (`create_time`),
|
||||
KEY `idx_provider` (`ai_provider`),
|
||||
KEY `idx_model` (`ai_model`),
|
||||
KEY `idx_parse_status` (`parse_status`),
|
||||
KEY `idx_execute_status` (`execute_status`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='AI 助手行为记录表(解析、执行、审计)';
|
||||
KEY `idx_unionid` (`unionid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户第三方账号绑定表';
|
||||
|
||||
524
sql/mysql/youlai_admin_template.sql
Normal file
524
sql/mysql/youlai_admin_template.sql
Normal file
@@ -0,0 +1,524 @@
|
||||
# YouLai_Admin 数据库(MySQL 5.7 ~ MySQL 8.x)
|
||||
# Copyright (c) 2021-present, youlai.tech
|
||||
|
||||
|
||||
-- ----------------------------
|
||||
-- 1. 创建数据库
|
||||
-- ----------------------------
|
||||
CREATE DATABASE IF NOT EXISTS youlai_admin_template CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
|
||||
-- ----------------------------
|
||||
-- 2. 创建表 && 数据初始化
|
||||
-- ----------------------------
|
||||
USE youlai_admin_template;
|
||||
|
||||
SET NAMES utf8mb4; # 设置字符集
|
||||
SET FOREIGN_KEY_CHECKS = 0; # 关闭外键检查,加快导入速度
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_dept
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_dept`;
|
||||
CREATE TABLE `sys_dept` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`name` varchar(100) NOT NULL COMMENT '部门名称',
|
||||
`code` varchar(100) NOT NULL COMMENT '部门编号',
|
||||
`parent_id` bigint DEFAULT 0 COMMENT '父节点id',
|
||||
`tree_path` varchar(255) NOT NULL COMMENT '父节点id路径',
|
||||
`sort` smallint DEFAULT 0 COMMENT '显示顺序',
|
||||
`status` tinyint DEFAULT 1 COMMENT '状态(1-正常 0-禁用)',
|
||||
`create_by` bigint NULL COMMENT '创建人ID',
|
||||
`create_time` datetime NULL COMMENT '创建时间',
|
||||
`update_by` bigint NULL COMMENT '修改人ID',
|
||||
`update_time` datetime NULL COMMENT '更新时间',
|
||||
`is_deleted` tinyint DEFAULT 0 COMMENT '逻辑删除标识(1-已删除 0-未删除)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `uk_code`(`code` ASC) USING BTREE COMMENT '部门编号唯一索引'
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '部门管理表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of sys_dept
|
||||
-- ----------------------------
|
||||
INSERT INTO `sys_dept` VALUES (1, '有来技术', 'YOULAI', 0, '0', 1, 1, 1, NULL, 1, now(), 0);
|
||||
INSERT INTO `sys_dept` VALUES (2, '研发部门', 'RD001', 1, '0,1', 1, 1, 2, NULL, 2, now(), 0);
|
||||
INSERT INTO `sys_dept` VALUES (3, '测试部门', 'QA001', 1, '0,1', 1, 1, 2, NULL, 2, now(), 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_dict
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_dict`;
|
||||
CREATE TABLE `sys_dict` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键 ',
|
||||
`dict_code` varchar(50) COMMENT '类型编码',
|
||||
`name` varchar(50) COMMENT '类型名称',
|
||||
`status` tinyint(1) DEFAULT '0' COMMENT '状态(0:正常;1:禁用)',
|
||||
`remark` varchar(255) COMMENT '备注',
|
||||
`create_time` datetime COMMENT '创建时间',
|
||||
`create_by` bigint COMMENT '创建人ID',
|
||||
`update_time` datetime COMMENT '更新时间',
|
||||
`update_by` bigint COMMENT '修改人ID',
|
||||
`is_deleted` tinyint DEFAULT '0' COMMENT '是否删除(1-删除,0-未删除)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `idx_dict_code` (`dict_code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据字典类型表';
|
||||
-- ----------------------------
|
||||
-- Records of sys_dict
|
||||
-- ----------------------------
|
||||
INSERT INTO `sys_dict` VALUES (1, 'gender', '性别', 1, NULL, now() , 1,now(), 1,0);
|
||||
INSERT INTO `sys_dict` VALUES (2, 'notice_type', '通知类型', 1, NULL, now(), 1,now(), 1,0);
|
||||
INSERT INTO `sys_dict` VALUES (3, 'notice_level', '通知级别', 1, NULL, now(), 1,now(), 1,0);
|
||||
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_dict_item
|
||||
-- ----------------------------
|
||||
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 '字典项值',
|
||||
`label` varchar(100) COMMENT '字典项标签',
|
||||
`tag_type` varchar(50) COMMENT '标签类型,用于前端样式展示(如success、warning等)',
|
||||
`status` tinyint DEFAULT '0' COMMENT '状态(1-正常,0-禁用)',
|
||||
`sort` int DEFAULT '0' COMMENT '排序',
|
||||
`remark` varchar(255) COMMENT '备注',
|
||||
`create_time` datetime COMMENT '创建时间',
|
||||
`create_by` bigint COMMENT '创建人ID',
|
||||
`update_time` datetime COMMENT '更新时间',
|
||||
`update_by` bigint COMMENT '修改人ID',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据字典项表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of sys_dict_item
|
||||
-- ----------------------------
|
||||
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
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_menu`;
|
||||
CREATE TABLE `sys_menu` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||
`parent_id` bigint NOT NULL COMMENT '父菜单ID',
|
||||
`tree_path` varchar(255) COMMENT '父节点ID路径',
|
||||
`name` varchar(64) NOT NULL COMMENT '菜单名称',
|
||||
`type` char(1) NOT NULL COMMENT '菜单类型(C-目录 M-菜单 B-按钮)',
|
||||
`route_name` varchar(255) COMMENT '路由名称(Vue Router 中用于命名路由)',
|
||||
`route_path` varchar(128) COMMENT '路由路径(Vue Router 中定义的 URL 路径)',
|
||||
`component` varchar(128) COMMENT '组件路径(组件页面完整路径,相对于 src/views/,缺省后缀 .vue)',
|
||||
`perm` varchar(128) COMMENT '【按钮】权限标识',
|
||||
`always_show` tinyint DEFAULT 0 COMMENT '【目录】只有一个子路由是否始终显示(1-是 0-否)',
|
||||
`keep_alive` tinyint DEFAULT 0 COMMENT '【菜单】是否开启页面缓存(1-是 0-否)',
|
||||
`visible` tinyint(1) DEFAULT 1 COMMENT '显示状态(1-显示 0-隐藏)',
|
||||
`sort` int DEFAULT 0 COMMENT '排序',
|
||||
`icon` varchar(64) COMMENT '菜单图标',
|
||||
`redirect` varchar(128) COMMENT '跳转路径',
|
||||
`create_time` datetime NULL COMMENT '创建时间',
|
||||
`update_time` datetime NULL COMMENT '更新时间',
|
||||
`params` varchar(255) NULL COMMENT '路由参数',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统菜单表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of sys_menu
|
||||
-- ----------------------------
|
||||
-- 顶级目录:系统管理/代码生成/平台文档/接口文档
|
||||
INSERT INTO `sys_menu` VALUES (1, 0, '0', '系统管理', 'C', '', '/system', 'Layout', NULL, NULL, NULL, 1, 1, 'system', '/system/user', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2, 0, '0', '代码生成', 'C', '', '/codegen', 'Layout', NULL, NULL, NULL, 1, 2, 'code', '/codegen/index', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (4, 0, '0', '平台文档', 'C', '', '/doc', 'Layout', NULL, NULL, NULL, 1, 4, 'document', '', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (5, 0, '0', '接口文档', 'C', '', '/api', 'Layout', NULL, NULL, NULL, 1, 5, 'api', '', now(), now(), NULL);
|
||||
|
||||
-- 系统管理
|
||||
INSERT INTO `sys_menu` VALUES (210, 1, '0,1', '用户管理', 'M', 'User', 'user', 'system/user/index', NULL, NULL, 1, 1, 1, 'el-icon-User', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2101, 210, '0,1,210', '用户查询', 'B', NULL, '', NULL, 'sys:user:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2102, 210, '0,1,210', '用户新增', 'B', NULL, '', NULL, 'sys:user:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2103, 210, '0,1,210', '用户编辑', 'B', NULL, '', NULL, 'sys:user:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2104, 210, '0,1,210', '用户删除', 'B', NULL, '', NULL, 'sys:user:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2105, 210, '0,1,210', '重置密码', 'B', NULL, '', NULL, 'sys:user:reset-password', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2106, 210, '0,1,210', '用户导入', 'B', NULL, '', NULL, 'sys:user:import', NULL, NULL, 1, 6, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2107, 210, '0,1,210', '用户导出', 'B', NULL, '', NULL, 'sys:user:export', NULL, NULL, 1, 7, '', NULL, now(), now(), NULL);
|
||||
|
||||
INSERT INTO `sys_menu` VALUES (220, 1, '0,1', '角色管理', 'M', 'Role', 'role', 'system/role/index', NULL, NULL, 1, 1, 2, 'role', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2201, 220, '0,1,220', '角色查询', 'B', NULL, '', NULL, 'sys:role:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2202, 220, '0,1,220', '角色新增', 'B', NULL, '', NULL, 'sys:role:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2203, 220, '0,1,220', '角色编辑', 'B', NULL, '', NULL, 'sys:role:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2204, 220, '0,1,220', '角色删除', 'B', NULL, '', NULL, 'sys:role:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2205, 220, '0,1,220', '角色分配权限', 'B', NULL, '', NULL, 'sys:role:assign', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL);
|
||||
|
||||
INSERT INTO `sys_menu` VALUES (230, 1, '0,1', '菜单管理', 'M', 'SysMenu', 'menu', 'system/menu/index', NULL, NULL, 1, 1, 3, 'menu', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2301, 230, '0,1,230', '菜单查询', 'B', NULL, '', NULL, 'sys:menu:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2302, 230, '0,1,230', '菜单新增', 'B', NULL, '', NULL, 'sys:menu:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2303, 230, '0,1,230', '菜单编辑', 'B', NULL, '', NULL, 'sys:menu:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2304, 230, '0,1,230', '菜单删除', 'B', NULL, '', NULL, 'sys:menu:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
||||
|
||||
INSERT INTO `sys_menu` VALUES (240, 1, '0,1', '部门管理', 'M', 'Dept', 'dept', 'system/dept/index', NULL, NULL, 1, 1, 4, 'tree', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2401, 240, '0,1,240', '部门查询', 'B', NULL, '', NULL, 'sys:dept:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2402, 240, '0,1,240', '部门新增', 'B', NULL, '', NULL, 'sys:dept:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2403, 240, '0,1,240', '部门编辑', 'B', NULL, '', NULL, 'sys:dept:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2404, 240, '0,1,240', '部门删除', 'B', NULL, '', NULL, 'sys:dept:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
||||
|
||||
INSERT INTO `sys_menu` VALUES (250, 1, '0,1', '字典管理', 'M', 'Dict', 'dict', 'system/dict/index', NULL, NULL, 1, 1, 5, 'dict', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2501, 250, '0,1,250', '字典查询', 'B', NULL, '', NULL, 'sys:dict:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2502, 250, '0,1,250', '字典新增', 'B', NULL, '', NULL, 'sys:dict:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2503, 250, '0,1,250', '字典编辑', 'B', NULL, '', NULL, 'sys:dict:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2504, 250, '0,1,250', '字典删除', 'B', NULL, '', NULL, 'sys:dict:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
||||
|
||||
INSERT INTO `sys_menu` VALUES (251, 1, '0,1', '字典项', 'M', 'DictItem', 'dict-item', 'system/dict/dict-item', NULL, 0, 1, 0, 6, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2511, 251, '0,1,251', '字典项查询', 'B', NULL, '', NULL, 'sys:dict-item:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2512, 251, '0,1,251', '字典项新增', 'B', NULL, '', NULL, 'sys:dict-item:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2513, 251, '0,1,251', '字典项编辑', 'B', NULL, '', NULL, 'sys:dict-item:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2514, 251, '0,1,251', '字典项删除', 'B', NULL, '', NULL, 'sys:dict-item:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
||||
|
||||
INSERT INTO `sys_menu` VALUES (260, 1, '0,1', '系统日志', 'M', 'Log', 'log', 'system/log/index', NULL, 0, 1, 1, 7, 'document', NULL, now(), now(), NULL);
|
||||
|
||||
INSERT INTO `sys_menu` VALUES (270, 1, '0,1', '系统配置', 'M', 'Config', 'config', 'system/config/index', NULL, 0, 1, 1, 8, 'setting', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2701, 270, '0,1,270', '系统配置查询', 'B', NULL, '', NULL, 'sys:config:list', 0, 1, 1, 1, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2702, 270, '0,1,270', '系统配置新增', 'B', NULL, '', NULL, 'sys:config:create', 0, 1, 1, 2, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2703, 270, '0,1,270', '系统配置修改', 'B', NULL, '', NULL, 'sys:config:update', 0, 1, 1, 3, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2704, 270, '0,1,270', '系统配置删除', 'B', NULL, '', NULL, 'sys:config:delete', 0, 1, 1, 4, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2705, 270, '0,1,270', '系统配置刷新', 'B', NULL, '', NULL, 'sys:config:refresh', 0, 1, 1, 5, '', NULL, now(), now(), NULL);
|
||||
|
||||
INSERT INTO `sys_menu` VALUES (280, 1, '0,1', '通知公告', 'M', 'Notice', 'notice', 'system/notice/index', NULL, NULL, NULL, 1, 9, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2801, 280, '0,1,280', '通知查询', 'B', NULL, '', NULL, 'sys:notice:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2802, 280, '0,1,280', '通知新增', 'B', NULL, '', NULL, 'sys:notice:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2803, 280, '0,1,280', '通知编辑', 'B', NULL, '', NULL, 'sys:notice:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2804, 280, '0,1,280', '通知删除', 'B', NULL, '', NULL, 'sys:notice:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2805, 280, '0,1,280', '通知发布', 'B', NULL, '', NULL, 'sys:notice:publish', 0, 1, 1, 5, '', NULL, now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (2806, 280, '0,1,280', '通知撤回', 'B', NULL, '', NULL, 'sys:notice:revoke', 0, 1, 1, 6, '', NULL, now(), now(), NULL);
|
||||
|
||||
-- 代码生成
|
||||
INSERT INTO `sys_menu` VALUES (310, 2, '0,2', '代码生成', 'M', 'Codegen', 'codegen', 'codegen/index', NULL, NULL, 1, 1, 1, 'code', NULL, now(), now(), NULL);
|
||||
|
||||
-- 平台文档(外链通过 route_path 识别)
|
||||
INSERT INTO `sys_menu` VALUES (501, 4, '0,4', '平台文档(外链)', 'M', NULL, 'https://juejin.cn/post/7228990409909108793', '', NULL, NULL, NULL, 1, 1, 'document', '', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (502, 4, '0,4', '后端文档', 'M', NULL, 'https://youlai.blog.csdn.net/article/details/145178880', '', NULL, NULL, NULL, 1, 2, 'document', '', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (503, 4, '0,4', '移动端文档', 'M', NULL, 'https://youlai.blog.csdn.net/article/details/143222890', '', NULL, NULL, NULL, 1, 3, 'document', '', now(), now(), NULL);
|
||||
INSERT INTO `sys_menu` VALUES (504, 4, '0,4', '内部文档', 'M', NULL, 'internal-doc', 'demo/internal-doc', NULL, NULL, NULL, 1, 4, 'document', '', now(), now(), NULL);
|
||||
|
||||
-- 接口文档
|
||||
INSERT INTO `sys_menu` VALUES (601, 5, '0,5', 'Apifox', 'M', 'Apifox', 'apifox', 'demo/api/apifox', NULL, NULL, 1, 1, 1, 'api', '', now(), now(), NULL);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_role
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_role`;
|
||||
CREATE TABLE `sys_role` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(64) NOT NULL COMMENT '角色名称',
|
||||
`code` varchar(32) NOT NULL COMMENT '角色编码',
|
||||
`sort` int NULL COMMENT '显示顺序',
|
||||
`status` tinyint(1) DEFAULT 1 COMMENT '角色状态(1-正常 0-停用)',
|
||||
`data_scope` tinyint NULL COMMENT '数据权限(1-所有数据 2-部门及子部门数据 3-本部门数据 4-本人数据 5-自定义部门数据)',
|
||||
`create_by` bigint NULL COMMENT '创建人 ID',
|
||||
`create_time` datetime NULL COMMENT '创建时间',
|
||||
`update_by` bigint NULL COMMENT '更新人ID',
|
||||
`update_time` datetime NULL COMMENT '更新时间',
|
||||
`is_deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
UNIQUE INDEX `uk_name`(`name` ASC) USING BTREE COMMENT '角色名称唯一索引',
|
||||
UNIQUE INDEX `uk_code`(`code` ASC) USING BTREE COMMENT '角色编码唯一索引'
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统角色表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of sys_role
|
||||
-- ----------------------------
|
||||
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, '部门主管', 'DEPT_MANAGER', 4, 1, 2, NULL, now(), NULL, now(), 0);
|
||||
INSERT INTO `sys_role` VALUES (5, '部门成员', 'DEPT_MEMBER', 5, 1, 3, NULL, now(), NULL, now(), 0);
|
||||
INSERT INTO `sys_role` VALUES (6, '普通员工', 'EMPLOYEE', 6, 1, 4, NULL, now(), NULL, now(), 0);
|
||||
INSERT INTO `sys_role` VALUES (7, '自定义权限用户', 'CUSTOM_USER', 7, 1, 5, NULL, now(), NULL, now(), 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_role_menu
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_role_menu`;
|
||||
CREATE TABLE `sys_role_menu` (
|
||||
`role_id` bigint NOT NULL COMMENT '角色ID',
|
||||
`menu_id` bigint NOT NULL COMMENT '菜单ID',
|
||||
UNIQUE INDEX `uk_roleid_menuid`(`role_id` ASC, `menu_id` ASC) USING BTREE COMMENT '角色菜单唯一索引'
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '角色菜单关联表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_role_dept
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_role_dept`;
|
||||
CREATE TABLE `sys_role_dept` (
|
||||
`role_id` bigint NOT NULL COMMENT '角色ID',
|
||||
`dept_id` bigint NOT NULL COMMENT '部门ID',
|
||||
UNIQUE INDEX `uk_roleid_deptid`(`role_id` ASC, `dept_id` ASC) USING BTREE COMMENT '角色部门唯一索引'
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '角色部门关联表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of sys_role_dept
|
||||
-- ----------------------------
|
||||
INSERT IGNORE INTO `sys_role_dept` VALUES (7, 1);
|
||||
INSERT IGNORE INTO `sys_role_dept` VALUES (7, 2);
|
||||
|
||||
-- ============================================
|
||||
-- 系统管理员角色菜单权限(role_id=2)
|
||||
-- 顶级目录
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 1), (2, 2), (2, 4), (2, 5);
|
||||
-- 系统管理
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 210), (2, 2101), (2, 2102), (2, 2103), (2, 2104), (2, 2105), (2, 2106), (2, 2107);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 220), (2, 2201), (2, 2202), (2, 2203), (2, 2204), (2, 2205);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 230), (2, 2301), (2, 2302), (2, 2303), (2, 2304);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 240), (2, 2401), (2, 2402), (2, 2403), (2, 2404);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 250), (2, 2501), (2, 2502), (2, 2503), (2, 2504);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 251), (2, 2511), (2, 2512), (2, 2513), (2, 2514);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 260), (2, 2601);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 270), (2, 2701), (2, 2702), (2, 2703), (2, 2704), (2, 2705);
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 280), (2, 2801), (2, 2802), (2, 2803), (2, 2804), (2, 2805), (2, 2806);
|
||||
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (4, 1);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (4, 210), (4, 2101), (4, 2102), (4, 2103), (4, 2104), (4, 2105), (4, 2106), (4, 2107);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (4, 220), (4, 2201), (4, 2202), (4, 2203), (4, 2204), (4, 2205);
|
||||
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (5, 1);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (5, 210), (5, 2101), (5, 2102), (5, 2103), (5, 2104), (5, 2105), (5, 2106), (5, 2107);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (5, 220), (5, 2201), (5, 2202), (5, 2203), (5, 2204), (5, 2205);
|
||||
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (6, 1);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (6, 210), (6, 2101), (6, 2102), (6, 2103), (6, 2104), (6, 2105), (6, 2106), (6, 2107);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (6, 220), (6, 2201), (6, 2202), (6, 2203), (6, 2204), (6, 2205);
|
||||
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (7, 1);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (7, 210), (7, 2101), (7, 2102), (7, 2103), (7, 2104), (7, 2105), (7, 2106), (7, 2107);
|
||||
INSERT IGNORE INTO `sys_role_menu` VALUES (7, 220), (7, 2201), (7, 2202), (7, 2203), (7, 2204), (7, 2205);
|
||||
-- 代码生成
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 310);
|
||||
-- 平台文档
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 501), (2, 502), (2, 503), (2, 504);
|
||||
-- 接口文档
|
||||
INSERT INTO `sys_role_menu` VALUES (2, 601);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_user
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_user`;
|
||||
CREATE TABLE `sys_user` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`username` varchar(64) COMMENT '用户名',
|
||||
`nickname` varchar(64) COMMENT '昵称',
|
||||
`gender` tinyint(1) DEFAULT 1 COMMENT '性别((1-男 2-女 0-保密)',
|
||||
`password` varchar(100) COMMENT '密码',
|
||||
`dept_id` int COMMENT '部门ID',
|
||||
`avatar` varchar(255) COMMENT '用户头像',
|
||||
`mobile` varchar(20) COMMENT '联系方式',
|
||||
`status` tinyint(1) DEFAULT 1 COMMENT '状态(1-正常 0-禁用)',
|
||||
`email` varchar(128) COMMENT '用户邮箱',
|
||||
`create_time` datetime COMMENT '创建时间',
|
||||
`create_by` bigint COMMENT '创建人ID',
|
||||
`update_time` datetime COMMENT '更新时间',
|
||||
`update_by` bigint COMMENT '修改人ID',
|
||||
`is_deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统用户表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of sys_user
|
||||
-- ----------------------------
|
||||
INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345677', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18888888888', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (3, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345679', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (4, 'dept_manager', '部门主管', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345680', 1, 'manager@youlaitech.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (5, 'dept_member', '部门成员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345681', 1, 'member@youlaitech.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (6, 'employee', '普通员工', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 2, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345682', 1, 'employee@youlaitech.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (7, 'custom_user', '自定义权限用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345683', 1, 'custom@youlaitech.com', now(), NULL, now(), NULL, 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_user_role
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_user_role`;
|
||||
CREATE TABLE `sys_user_role` (
|
||||
`user_id` bigint NOT NULL COMMENT '用户ID',
|
||||
`role_id` bigint NOT NULL COMMENT '角色ID',
|
||||
PRIMARY KEY (`user_id`, `role_id`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '用户角色关联表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of sys_user_role
|
||||
-- ----------------------------
|
||||
INSERT INTO `sys_user_role` VALUES (1, 1);
|
||||
INSERT INTO `sys_user_role` VALUES (2, 2);
|
||||
INSERT INTO `sys_user_role` VALUES (3, 3);
|
||||
INSERT IGNORE INTO `sys_user_role` VALUES (4, 4);
|
||||
INSERT IGNORE INTO `sys_user_role` VALUES (5, 5);
|
||||
INSERT IGNORE INTO `sys_user_role` VALUES (6, 6);
|
||||
INSERT IGNORE INTO `sys_user_role` VALUES (7, 7);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_log
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_log`;
|
||||
CREATE TABLE `sys_log` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`module` TINYINT NOT NULL COMMENT '模块,数字枚举,参考 LogModule 枚举',
|
||||
`action_type` TINYINT NOT NULL COMMENT '操作类型,数字枚举,参考 ActionType 枚举',
|
||||
`title` VARCHAR(100) NOT NULL COMMENT '前端显示标题',
|
||||
`content` TEXT COMMENT '自定义日志内容',
|
||||
`operator_id` BIGINT COMMENT '操作人ID',
|
||||
`operator_name` VARCHAR(50) COMMENT '操作人名称',
|
||||
`request_uri` VARCHAR(255) COMMENT '请求路径',
|
||||
`request_method` VARCHAR(10) COMMENT '请求方法',
|
||||
`ip` VARCHAR(45) COMMENT 'IP地址',
|
||||
`province` VARCHAR(100) COMMENT '省份',
|
||||
`city` VARCHAR(100) COMMENT '城市',
|
||||
`device` VARCHAR(100) COMMENT '设备',
|
||||
`os` VARCHAR(100) COMMENT '操作系统',
|
||||
`browser` VARCHAR(100) COMMENT '浏览器',
|
||||
`status` TINYINT DEFAULT 1 COMMENT '0失败 1成功',
|
||||
`error_msg` VARCHAR(255) COMMENT '错误信息',
|
||||
`execution_time` INT COMMENT '执行时间(ms)',
|
||||
`create_time` DATETIME COMMENT '操作时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `idx_module_action_time` (`module`, `action_type`, `create_time`),
|
||||
KEY `idx_operator_time` (`operator_id`, `create_time`),
|
||||
KEY `idx_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for gen_table
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `gen_table`;
|
||||
CREATE TABLE `gen_table` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`table_name` varchar(100) NOT NULL COMMENT '表名',
|
||||
`module_name` varchar(100) COMMENT '模块名',
|
||||
`package_name` varchar(255) NOT NULL COMMENT '包名',
|
||||
`business_name` varchar(100) NOT NULL COMMENT '业务名',
|
||||
`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` tinyint(4) DEFAULT 0 COMMENT '是否删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tablename` (`table_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成配置表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for gen_table_column
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `gen_table_column`;
|
||||
CREATE TABLE `gen_table_column` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`table_id` bigint NOT NULL COMMENT '关联的表配置ID',
|
||||
`column_name` varchar(100) ,
|
||||
`column_type` varchar(50) ,
|
||||
`column_length` int ,
|
||||
`field_name` varchar(100) NOT NULL COMMENT '字段名称',
|
||||
`field_type` varchar(100) COMMENT '字段类型',
|
||||
`field_sort` int COMMENT '字段排序',
|
||||
`field_comment` varchar(255) COMMENT '字段描述',
|
||||
`max_length` int ,
|
||||
`is_required` tinyint(1) COMMENT '是否必填',
|
||||
`is_show_in_list` tinyint(1) DEFAULT '0' COMMENT '是否在列表显示',
|
||||
`is_show_in_form` tinyint(1) DEFAULT '0' COMMENT '是否在表单显示',
|
||||
`is_show_in_query` tinyint(1) DEFAULT '0' COMMENT '是否在查询条件显示',
|
||||
`query_type` tinyint COMMENT '查询方式',
|
||||
`form_type` tinyint COMMENT '表单类型',
|
||||
`dict_type` varchar(50) COMMENT '字典类型',
|
||||
`create_time` datetime COMMENT '创建时间',
|
||||
`update_time` datetime COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_table_id` (`table_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成字段配置表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 系统配置表
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_config`;
|
||||
CREATE TABLE `sys_config` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`config_name` varchar(50) NOT NULL COMMENT '配置名称',
|
||||
`config_key` varchar(50) NOT NULL COMMENT '配置key',
|
||||
`config_value` varchar(100) NOT NULL COMMENT '配置值',
|
||||
`remark` varchar(255) COMMENT '备注',
|
||||
`create_time` datetime COMMENT '创建时间',
|
||||
`create_by` bigint COMMENT '创建人ID',
|
||||
`update_time` datetime COMMENT '更新时间',
|
||||
`update_by` bigint COMMENT '更新人ID',
|
||||
`is_deleted` tinyint(4) DEFAULT '0' NOT NULL COMMENT '逻辑删除标识(0-未删除 1-已删除)',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB COMMENT='系统配置表';
|
||||
|
||||
INSERT INTO `sys_config` VALUES (1, '系统限流QPS', 'IP_QPS_THRESHOLD_LIMIT', '10', '单个IP请求的最大每秒查询数(QPS)阈值Key', now(), 1, NULL, NULL, 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- 通知公告表
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_notice`;
|
||||
CREATE TABLE `sys_notice` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`title` varchar(50) COMMENT '通知标题',
|
||||
`content` text COMMENT '通知内容',
|
||||
`type` tinyint NOT NULL COMMENT '通知类型(关联字典编码:notice_type)',
|
||||
`level` varchar(5) NOT NULL COMMENT '通知等级(字典code:notice_level)',
|
||||
`target_type` tinyint NOT NULL COMMENT '目标类型(1: 全体, 2: 指定)',
|
||||
`target_user_ids` varchar(255) COMMENT '目标人ID集合(多个使用英文逗号,分割)',
|
||||
`publisher_id` bigint COMMENT '发布人ID',
|
||||
`publish_status` tinyint DEFAULT '0' COMMENT '发布状态(0: 未发布, 1: 已发布, -1: 已撤回)',
|
||||
`publish_time` datetime COMMENT '发布时间',
|
||||
`revoke_time` datetime COMMENT '撤回时间',
|
||||
`create_by` bigint NOT NULL COMMENT '创建人ID',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
`update_by` bigint COMMENT '更新人ID',
|
||||
`update_time` datetime COMMENT '更新时间',
|
||||
`is_deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除(0: 未删除, 1: 已删除)',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统通知公告表';
|
||||
|
||||
INSERT INTO `sys_notice` VALUES (1, 'v3.0.0 版本发布 - 多租户功能上线', '<p>🎉 新版本发布,主要更新内容:</p><p>1. 新增多租户功能,支持租户隔离和数据管理</p><p>2. 优化系统性能,提升响应速度</p><p>3. 完善权限管理,增强安全性</p><p>4. 修复已知问题,提升系统稳定性</p>', 1, 'H', 1, NULL, 1, 1, '2024-12-15 10:00:00', NULL, 1, '2024-12-15 10:00:00', 1, '2024-12-15 10:00:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (2, '系统维护通知 - 2024年12月20日', '<p>⏰ 系统维护通知</p><p>系统将于 <strong>2024年12月20日(本周五)凌晨 2:00-4:00</strong> 进行例行维护升级。</p><p>维护期间系统将暂停服务,请提前做好数据备份工作。</p><p>给您带来的不便,敬请谅解!</p>', 2, 'H', 1, NULL, 1, 1, '2024-12-18 14:30:00', NULL, 1, '2024-12-18 14:30:00', 1, '2024-12-18 14:30:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (3, '安全提醒 - 防范钓鱼邮件', '<p>⚠️ 安全提醒</p><p>近期发现有不法分子通过钓鱼邮件进行网络攻击,请大家提高警惕:</p><p>1. 不要点击来源不明的邮件链接</p><p>2. 不要下载可疑附件</p><p>3. 遇到可疑邮件请及时联系IT部门</p><p>4. 定期修改密码,使用强密码策略</p>', 3, 'H', 1, NULL, 1, 1, '2024-12-10 09:00:00', NULL, 1, '2024-12-10 09:00:00', 1, '2024-12-10 09:00:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (4, '元旦假期安排通知', '<p>📅 元旦假期安排</p><p>根据国家法定节假日安排,公司元旦假期时间为:</p><p><strong>2024年12月30日(周一)至 2025年1月1日(周三)</strong>,共3天。</p><p>2024年12月29日(周日)正常上班。</p><p>祝大家元旦快乐,假期愉快!</p>', 4, 'M', 1, NULL, 1, 1, '2024-12-25 16:00:00', NULL, 1, '2024-12-25 16:00:00', 1, '2024-12-25 16:00:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (5, '新产品发布会邀请', '<p>🎊 新产品发布会邀请</p><p>公司将于 <strong>2025年1月15日下午14:00</strong> 在总部会议室举办新产品发布会。</p><p>届时将展示最新研发的产品和技术成果,欢迎全体员工参加。</p><p>请各部门提前安排好工作,准时参加。</p>', 5, 'M', 1, NULL, 1, 1, '2024-12-28 11:00:00', NULL, 1, '2024-12-28 11:00:00', 1, '2024-12-28 11:00:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (6, 'v2.16.1 版本更新', '<p>✨ 版本更新</p><p>v2.16.1 版本已发布,主要修复内容:</p><p>1. 修复 WebSocket 重复连接导致的后台线程阻塞问题</p><p>2. 优化通知公告功能,提升用户体验</p><p>3. 修复部分已知bug</p><p>建议尽快更新到最新版本。</p>', 1, 'M', 1, NULL, 1, 1, '2024-12-05 15:30:00', NULL, 1, '2024-12-05 15:30:00', 1, '2024-12-05 15:30:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (7, '年终总结会议通知', '<p>📋 年终总结会议通知</p><p>各部门年终总结会议将于 <strong>2024年12月30日上午9:00</strong> 召开。</p><p>请各部门负责人提前准备好年度工作总结和下年度工作计划。</p><p>会议地点:总部大会议室</p>', 5, 'M', 2, '1,2', 1, 1, '2024-12-22 10:00:00', NULL, 1, '2024-12-22 10:00:00', 1, '2024-12-22 10:00:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (8, '系统功能优化完成', '<p>✅ 系统功能优化</p><p>已完成以下功能优化:</p><p>1. 优化用户管理界面,提升操作体验</p><p>2. 增强数据导出功能,支持更多格式</p><p>3. 优化搜索功能,提升查询效率</p><p>4. 修复部分界面显示问题</p>', 1, 'L', 1, NULL, 1, 1, '2024-12-12 14:20:00', NULL, 1, '2024-12-12 14:20:00', 1, '2024-12-12 14:20:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (9, '员工培训计划', '<p>📚 员工培训计划</p><p>为提升员工专业技能,公司将于 <strong>2025年1月8日-10日</strong> 组织技术培训。</p><p>培训内容:</p><p>1. 新技术框架应用</p><p>2. 代码规范与最佳实践</p><p>3. 系统架构设计</p><p>请各部门合理安排工作,确保培训顺利进行。</p>', 5, 'M', 1, NULL, 1, 1, '2024-12-20 09:30:00', NULL, 1, '2024-12-20 09:30:00', 1, '2024-12-20 09:30:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (10, '数据备份提醒', '<p>💾 数据备份提醒</p><p>请各部门注意定期备份重要数据,建议每周至少备份一次。</p><p>备份方式:</p><p>1. 使用系统自带备份功能</p><p>2. 手动导出重要数据</p><p>3. 联系IT部门协助备份</p><p>数据安全,人人有责!</p>', 3, 'L', 1, NULL, 1, 1, '2024-12-08 08:00:00', NULL, 1, '2024-12-08 08:00:00', 1, '2024-12-08 08:00:00', 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- 用户通知公告表
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_user_notice`;
|
||||
CREATE TABLE `sys_user_notice` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
|
||||
`notice_id` bigint NOT NULL COMMENT '公共通知id',
|
||||
`user_id` bigint NOT NULL COMMENT '用户id',
|
||||
`is_read` bigint DEFAULT '0' COMMENT '读取状态(0: 未读, 1: 已读)',
|
||||
`read_time` datetime COMMENT '阅读时间',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
`update_time` datetime COMMENT '更新时间',
|
||||
`is_deleted` tinyint DEFAULT '0' COMMENT '逻辑删除(0: 未删除, 1: 已删除)',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户通知公告关联表';
|
||||
|
||||
INSERT INTO `sys_user_notice` VALUES (1, 1, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (2, 2, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (3, 3, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (4, 4, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (5, 5, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (6, 6, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (7, 7, 2, 1, NULL, now(), now(), 0);
|
||||
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);
|
||||
@@ -1,14 +1,13 @@
|
||||
package com.youlai.boot.auth.controller;
|
||||
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
|
||||
import com.youlai.boot.auth.model.vo.CaptchaVO;
|
||||
import com.youlai.boot.auth.model.dto.LoginRequest;
|
||||
import com.youlai.boot.auth.model.LoginReq;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.auth.service.AuthService;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -34,27 +33,25 @@ public class AuthController {
|
||||
|
||||
@Operation(summary = "获取验证码")
|
||||
@GetMapping("/captcha")
|
||||
public Result<CaptchaVO> getCaptcha() {
|
||||
CaptchaVO captcha = authService.getCaptcha();
|
||||
public Result<CaptchaInfo> getCaptcha() {
|
||||
CaptchaInfo captcha = authService.getCaptcha();
|
||||
return Result.success(captcha);
|
||||
}
|
||||
|
||||
@Operation(summary = "账号密码登录")
|
||||
@PostMapping("/login")
|
||||
@Log(value = "登录", module = LogModuleEnum.LOGIN)
|
||||
public Result<?> login(@RequestBody @Valid LoginRequest request) {
|
||||
String username = request.getUsername();
|
||||
String password = request.getPassword();
|
||||
AuthenticationToken authenticationToken = authService.login(username, password);
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
|
||||
public Result<AuthenticationToken> login(@RequestBody @Valid LoginReq request) {
|
||||
AuthenticationToken authenticationToken = authService.login(request.getUsername(), request.getPassword());
|
||||
return Result.success(authenticationToken);
|
||||
}
|
||||
|
||||
@Operation(summary = "短信验证码登录")
|
||||
@PostMapping("/login/sms")
|
||||
@Log(value = "短信验证码登录", module = LogModuleEnum.LOGIN)
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
|
||||
public Result<AuthenticationToken> loginBySms(
|
||||
@Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile,
|
||||
@Parameter(description = "验证码", example = "1234") @RequestParam String code
|
||||
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile,
|
||||
@Parameter(description = "验证码", example = "123456") @RequestParam String code
|
||||
) {
|
||||
AuthenticationToken loginResult = authService.loginBySms(mobile, code);
|
||||
return Result.success(loginResult);
|
||||
@@ -62,48 +59,24 @@ public class AuthController {
|
||||
|
||||
@Operation(summary = "发送登录短信验证码")
|
||||
@PostMapping("/sms/code")
|
||||
public Result<Void> sendLoginVerifyCode(
|
||||
@Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile
|
||||
public Result<Void> sendSmsCode(
|
||||
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile
|
||||
) {
|
||||
authService.sendSmsLoginCode(mobile);
|
||||
authService.sendSmsCode(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() {
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGOUT)
|
||||
public Result<Void> logout() {
|
||||
authService.logout();
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "刷新令牌")
|
||||
@PostMapping("/refresh-token")
|
||||
public Result<?> refreshToken(
|
||||
public Result<AuthenticationToken> refreshToken(
|
||||
@Parameter(description = "刷新令牌", example = "xxx.xxx.xxx") @RequestParam String refreshToken
|
||||
) {
|
||||
AuthenticationToken authenticationToken = authService.refreshToken(refreshToken);
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.youlai.boot.auth.controller;
|
||||
|
||||
import com.youlai.boot.auth.model.WxMaBindMobileReq;
|
||||
import com.youlai.boot.auth.model.WxMaPhoneLoginReq;
|
||||
import com.youlai.boot.auth.model.WxMaLoginResp;
|
||||
import com.youlai.boot.auth.service.WxMaAuthService;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
/**
|
||||
* 微信小程序认证控制层
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.4.0
|
||||
*/
|
||||
@Tag(name = "13.微信小程序认证")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/wxma/auth")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class WxMaAuthController {
|
||||
|
||||
private final WxMaAuthService wxMaAuthService;
|
||||
|
||||
/**
|
||||
* 静默登录
|
||||
* <p>
|
||||
* 适用场景:个人小程序、无需手机号的登录场景
|
||||
* <ul>
|
||||
* <li>已绑定手机号的用户:直接返回 token,登录成功</li>
|
||||
* <li>未绑定手机号的用户:返回 openid,需调用绑定手机号接口</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Operation(summary = "静默登录", description = "通过微信 code 登录,已绑定用户直接返回 token,未绑定用户返回 openid 需绑定手机号")
|
||||
@PostMapping("/silent-login")
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
|
||||
public Result<WxMaLoginResp> silentLogin(
|
||||
@Parameter(description = "微信登录凭证(wx.login 获取)", required = true, example = "0xxx")
|
||||
@RequestParam String code
|
||||
) {
|
||||
WxMaLoginResp result = wxMaAuthService.silentLogin(code);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号快捷登录
|
||||
* <p>
|
||||
* 适用场景:企业认证小程序(已开通手机号快捷登录权限)
|
||||
* <p>
|
||||
* 一步完成登录,无需绑定流程,自动创建新用户
|
||||
*/
|
||||
@Operation(summary = "手机号快捷登录", description = "同时使用微信 code 和手机号授权 code 登录,适用于企业认证小程序")
|
||||
@PostMapping("/phone-login")
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
|
||||
public Result<AuthenticationToken> phoneLogin(@Valid @RequestBody WxMaPhoneLoginReq req) {
|
||||
AuthenticationToken result = wxMaAuthService.phoneLogin(req.getLoginCode(), req.getPhoneCode());
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定手机号
|
||||
* <p>
|
||||
* 适用场景:静默登录后未绑定手机号的用户
|
||||
* <p>
|
||||
* 绑定成功后自动完成登录
|
||||
*/
|
||||
@Operation(summary = "绑定手机号", description = "为静默登录用户绑定手机号,绑定成功后自动登录")
|
||||
@PostMapping("/bind-mobile")
|
||||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
|
||||
public Result<AuthenticationToken> bindMobile(@Valid @RequestBody WxMaBindMobileReq req) {
|
||||
AuthenticationToken result = wxMaAuthService.bindMobile(req.getOpenid(), req.getMobile(), req.getSmsCode());
|
||||
return Result.success(result);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.auth.model.dto;
|
||||
package com.youlai.boot.auth.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@@ -13,7 +13,7 @@ import jakarta.validation.constraints.NotBlank;
|
||||
*/
|
||||
@Schema(description = "登录请求参数")
|
||||
@Data
|
||||
public class LoginRequest {
|
||||
public class LoginReq {
|
||||
|
||||
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin")
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@@ -26,7 +26,6 @@ public class LoginRequest {
|
||||
@Schema(description = "验证码缓存ID", example = "captcha_id_123")
|
||||
private String captchaId;
|
||||
|
||||
@Schema(description = "验证码", example = "1234")
|
||||
@Schema(description = "验证码", example = "123456")
|
||||
private String captchaCode;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.youlai.boot.auth.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 微信小程序绑定手机号请求
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Schema(description = "微信小程序绑定手机号请求")
|
||||
@Data
|
||||
public class WxMaBindMobileReq {
|
||||
|
||||
@NotBlank(message = "openid 不能为空")
|
||||
@Schema(description = "微信用户唯一标识(静默登录返回)", example = "oVBkZ0aYgDMDIywRdgPW8-joxXc4")
|
||||
private String openid;
|
||||
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Schema(description = "手机号码", example = "18888888888")
|
||||
private String mobile;
|
||||
|
||||
@NotBlank(message = "短信验证码不能为空")
|
||||
@Schema(description = "短信验证码", example = "123456")
|
||||
private String smsCode;
|
||||
}
|
||||
42
src/main/java/com/youlai/boot/auth/model/WxMaLoginResp.java
Normal file
42
src/main/java/com/youlai/boot/auth/model/WxMaLoginResp.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package com.youlai.boot.auth.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 微信小程序登录响应
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.4.0
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "微信小程序登录响应")
|
||||
public class WxMaLoginResp {
|
||||
|
||||
@Schema(description = "是否新用户")
|
||||
private Boolean isNewUser;
|
||||
|
||||
@Schema(description = "是否需要绑定手机号")
|
||||
private Boolean needBindMobile;
|
||||
|
||||
@Schema(description = "微信openid(绑定手机号时需要)")
|
||||
private String openid;
|
||||
|
||||
@Schema(description = "访问令牌")
|
||||
private String accessToken;
|
||||
|
||||
@Schema(description = "刷新令牌")
|
||||
private String refreshToken;
|
||||
|
||||
@Schema(description = "令牌类型")
|
||||
private String tokenType;
|
||||
|
||||
@Schema(description = "过期时间(秒)")
|
||||
private Integer expiresIn;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.youlai.boot.auth.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 微信小程序手机号快捷登录请求
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@Schema(description = "微信小程序手机号快捷登录请求")
|
||||
@Data
|
||||
public class WxMaPhoneLoginReq {
|
||||
|
||||
@NotBlank(message = "微信登录凭证不能为空")
|
||||
@Schema(description = "微信登录凭证(wx.login 获取)", example = "0xxx")
|
||||
private String loginCode;
|
||||
|
||||
@NotBlank(message = "手机号授权凭证不能为空")
|
||||
@Schema(description = "手机号授权凭证(getPhoneNumber 事件获取)", example = "0xxx")
|
||||
private String phoneCode;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.youlai.boot.auth.model.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
*微信小程序Code登录请求参数
|
||||
*/
|
||||
@Schema(description = "微信小程序Code登录请求参数")
|
||||
@Data
|
||||
public class WxMiniAppCodeLoginDTO {
|
||||
|
||||
@Schema(description = "微信小程序登录时获取的code", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "code不能为空")
|
||||
private String code;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.youlai.boot.auth.model.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 微信小程序手机号登录请求参数
|
||||
*/
|
||||
@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,9 +1,7 @@
|
||||
package com.youlai.boot.auth.service;
|
||||
|
||||
import com.youlai.boot.auth.model.vo.CaptchaVO;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
|
||||
import com.youlai.boot.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
|
||||
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
|
||||
/**
|
||||
* 认证服务接口
|
||||
@@ -14,71 +12,45 @@ import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
|
||||
public interface AuthService {
|
||||
|
||||
/**
|
||||
* 登录
|
||||
* 账号密码登录
|
||||
*
|
||||
* @param username 用户名
|
||||
* @param password 密码
|
||||
* @return 登录结果
|
||||
* @return 认证令牌
|
||||
*/
|
||||
AuthenticationToken login(String username, String password);
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
void logout();
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*
|
||||
* @return 验证码
|
||||
*/
|
||||
CaptchaVO getCaptcha();
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 登录结果
|
||||
*/
|
||||
AuthenticationToken refreshToken(String refreshToken);
|
||||
|
||||
/**
|
||||
* 微信小程序登录
|
||||
*
|
||||
* @param code 微信登录code
|
||||
* @return 登录结果
|
||||
*/
|
||||
AuthenticationToken loginByWechat(String code);
|
||||
|
||||
/**
|
||||
* 微信小程序Code登录
|
||||
*
|
||||
* @param loginDto 登录参数
|
||||
* @return 访问令牌
|
||||
*/
|
||||
AuthenticationToken loginByWxMiniAppCode(WxMiniAppCodeLoginDTO loginDto);
|
||||
|
||||
/**
|
||||
* 微信小程序手机号登录
|
||||
*
|
||||
* @param loginDto 登录参数
|
||||
* @return 访问令牌
|
||||
*/
|
||||
AuthenticationToken loginByWxMiniAppPhone(WxMiniAppPhoneLoginDTO loginDto);
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*
|
||||
* @param mobile 手机号
|
||||
*/
|
||||
void sendSmsLoginCode(String mobile);
|
||||
|
||||
/**
|
||||
* 短信验证码登录
|
||||
*
|
||||
* @param mobile 手机号
|
||||
* @param code 验证码
|
||||
* @return 登录结果
|
||||
* @return 认证令牌
|
||||
*/
|
||||
AuthenticationToken loginBySms(String mobile, String code);
|
||||
|
||||
/**
|
||||
* 发送短信验证码
|
||||
*
|
||||
* @param mobile 手机号
|
||||
*/
|
||||
void sendSmsCode(String mobile);
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
void logout();
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
CaptchaInfo getCaptcha();
|
||||
|
||||
/**
|
||||
* 刷新令牌
|
||||
*
|
||||
* @param refreshToken 刷新令牌
|
||||
* @return 认证令牌
|
||||
*/
|
||||
AuthenticationToken refreshToken(String refreshToken);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.youlai.boot.auth.service;
|
||||
|
||||
import com.youlai.boot.auth.model.WxMaLoginResp;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
|
||||
/**
|
||||
* 微信小程序认证服务接口
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.4.0
|
||||
*/
|
||||
public interface WxMaAuthService {
|
||||
|
||||
/**
|
||||
* 静默登录
|
||||
* <p>
|
||||
* 通过微信登录凭证(code)获取用户唯一标识(openid),
|
||||
* 如果用户已绑定手机号则直接登录成功,否则返回需绑定手机号的提示。
|
||||
* </p>
|
||||
*
|
||||
* @param code 微信登录凭证(wx.login 获取)
|
||||
* @return 登录结果(成功返回 token,需绑定返回 openid)
|
||||
*/
|
||||
WxMaLoginResp silentLogin(String code);
|
||||
|
||||
/**
|
||||
* 手机号快捷登录
|
||||
* <p>
|
||||
* 同时使用微信登录凭证和手机号授权凭证,
|
||||
* 一步完成用户注册/登录,无需额外绑定流程。
|
||||
* 适用于企业认证的小程序(已开通手机号快捷登录权限)。
|
||||
* </p>
|
||||
*
|
||||
* @param loginCode 微信登录凭证(wx.login 获取)
|
||||
* @param phoneCode 手机号授权凭证(getPhoneNumber 事件获取)
|
||||
* @return 认证令牌
|
||||
*/
|
||||
AuthenticationToken phoneLogin(String loginCode, String phoneCode);
|
||||
|
||||
/**
|
||||
* 绑定手机号
|
||||
* <p>
|
||||
* 为已静默登录但未绑定手机号的用户绑定手机号,
|
||||
* 绑定成功后自动完成登录。
|
||||
* </p>
|
||||
*
|
||||
* @param openid 微信用户唯一标识
|
||||
* @param mobile 手机号码
|
||||
* @param smsCode 短信验证码
|
||||
* @return 认证令牌
|
||||
*/
|
||||
AuthenticationToken bindMobile(String openid, String mobile, String smsCode);
|
||||
}
|
||||
@@ -1,26 +1,16 @@
|
||||
package com.youlai.boot.auth.service.impl;
|
||||
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.CaptchaUtil;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppCodeLoginDTO;
|
||||
import com.youlai.boot.auth.model.dto.WxMiniAppPhoneLoginDTO;
|
||||
import com.youlai.boot.auth.model.vo.CaptchaVO;
|
||||
import com.youlai.boot.auth.service.AuthService;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.common.enums.CaptchaTypeEnum;
|
||||
import com.youlai.boot.config.property.CaptchaProperties;
|
||||
import com.youlai.boot.platform.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.platform.sms.service.SmsService;
|
||||
import com.youlai.boot.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.security.model.SmsAuthenticationToken;
|
||||
import com.youlai.boot.security.model.WxMiniAppCodeAuthenticationToken;
|
||||
import com.youlai.boot.security.model.WxMiniAppPhoneAuthenticationToken;
|
||||
import com.youlai.boot.security.token.TokenManager;
|
||||
import com.youlai.boot.security.util.SecurityUtils;
|
||||
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
|
||||
import com.youlai.boot.framework.captcha.service.CaptchaService;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.SmsAuthenticationToken;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.framework.integration.sms.service.SmsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
@@ -30,7 +20,6 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -49,12 +38,9 @@ public class AuthServiceImpl implements AuthService {
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final TokenManager tokenManager;
|
||||
|
||||
private final Font captchaFont;
|
||||
private final CaptchaProperties captchaProperties;
|
||||
private final CodeGenerator codeGenerator;
|
||||
|
||||
private final SmsService smsService;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final CaptchaService captchaService;
|
||||
|
||||
/**
|
||||
* 用户名密码登录
|
||||
@@ -84,34 +70,13 @@ public class AuthServiceImpl implements AuthService {
|
||||
return authenticationTokenResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信一键授权登录
|
||||
*
|
||||
* @param code 微信登录code
|
||||
* @return 访问令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthenticationToken loginByWechat(String code) {
|
||||
// 1. 创建用户微信认证的令牌(未认证)
|
||||
WxMiniAppCodeAuthenticationToken authenticationToken = new WxMiniAppCodeAuthenticationToken(code);
|
||||
|
||||
// 2. 执行认证(认证中)
|
||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||
|
||||
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
||||
AuthenticationToken token = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送登录短信验证码
|
||||
*
|
||||
* @param mobile 手机号
|
||||
*/
|
||||
@Override
|
||||
public void sendSmsLoginCode(String mobile) {
|
||||
public void sendSmsCode(String mobile) {
|
||||
|
||||
// 随机生成4位验证码
|
||||
// String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
|
||||
@@ -148,7 +113,6 @@ public class AuthServiceImpl implements AuthService {
|
||||
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
||||
AuthenticationToken authenticationToken = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
return authenticationToken;
|
||||
}
|
||||
|
||||
@@ -167,50 +131,10 @@ public class AuthServiceImpl implements AuthService {
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*
|
||||
* @return 验证码
|
||||
*/
|
||||
@Override
|
||||
public CaptchaVO getCaptcha() {
|
||||
|
||||
String captchaType = captchaProperties.getType();
|
||||
int width = captchaProperties.getWidth();
|
||||
int height = captchaProperties.getHeight();
|
||||
int interfereCount = captchaProperties.getInterfereCount();
|
||||
int codeLength = captchaProperties.getCode().getLength();
|
||||
|
||||
AbstractCaptcha captcha;
|
||||
if (CaptchaTypeEnum.CIRCLE.name().equalsIgnoreCase(captchaType)) {
|
||||
captcha = CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount);
|
||||
} else if (CaptchaTypeEnum.GIF.name().equalsIgnoreCase(captchaType)) {
|
||||
captcha = CaptchaUtil.createGifCaptcha(width, height, codeLength);
|
||||
} else if (CaptchaTypeEnum.LINE.name().equalsIgnoreCase(captchaType)) {
|
||||
captcha = CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount);
|
||||
} else if (CaptchaTypeEnum.SHEAR.name().equalsIgnoreCase(captchaType)) {
|
||||
captcha = CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid captcha type: " + captchaType);
|
||||
}
|
||||
captcha.setGenerator(codeGenerator);
|
||||
captcha.setTextAlpha(captchaProperties.getTextAlpha());
|
||||
captcha.setFont(captchaFont);
|
||||
|
||||
String captchaCode = captcha.getCode();
|
||||
String imageBase64Data = captcha.getImageBase64Data();
|
||||
|
||||
// 验证码文本缓存至Redis,用于登录校验
|
||||
String captchaId = IdUtil.fastSimpleUUID();
|
||||
redisTemplate.opsForValue().set(
|
||||
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId),
|
||||
captchaCode,
|
||||
captchaProperties.getExpireSeconds(),
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
|
||||
return CaptchaVO.builder()
|
||||
.captchaId(captchaId)
|
||||
.captchaBase64(imageBase64Data)
|
||||
.build();
|
||||
public CaptchaInfo getCaptcha() {
|
||||
return captchaService.generate();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -224,50 +148,4 @@ public class AuthServiceImpl implements AuthService {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
package com.youlai.boot.auth.service.impl;
|
||||
|
||||
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.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.auth.model.WxMaLoginResp;
|
||||
import com.youlai.boot.auth.service.WxMaAuthService;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.framework.security.exception.NeedBindMobileException;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.SysUserDetails;
|
||||
import com.youlai.boot.framework.security.model.WxMaAuthenticationToken;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.youlai.boot.system.enums.SocialPlatformEnum;
|
||||
import com.youlai.boot.system.model.entity.SysUser;
|
||||
import com.youlai.boot.system.service.UserSocialService;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import com.youlai.boot.system.service.UserRoleService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 微信小程序认证服务实现
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.0.0
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class WxMaAuthServiceImpl implements WxMaAuthService {
|
||||
|
||||
private final WxMaService wxMaService;
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final TokenManager tokenManager;
|
||||
private final UserService userService;
|
||||
private final UserSocialService userSocialService;
|
||||
private final UserRoleService userRoleService;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
/**
|
||||
* 静默登录
|
||||
*/
|
||||
@Override
|
||||
public WxMaLoginResp silentLogin(String code) {
|
||||
WxMaAuthenticationToken token = new WxMaAuthenticationToken(code);
|
||||
|
||||
try {
|
||||
Authentication authentication = authenticationManager.authenticate(token);
|
||||
AuthenticationToken authToken = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
return WxMaLoginResp.builder()
|
||||
.isNewUser(false)
|
||||
.needBindMobile(false)
|
||||
.accessToken(authToken.getAccessToken())
|
||||
.refreshToken(authToken.getRefreshToken())
|
||||
.tokenType(authToken.getTokenType())
|
||||
.expiresIn(authToken.getExpiresIn())
|
||||
.build();
|
||||
} catch (NeedBindMobileException e) {
|
||||
return WxMaLoginResp.builder()
|
||||
.isNewUser(true)
|
||||
.needBindMobile(true)
|
||||
.openid(e.getOpenid())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号快捷登录
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AuthenticationToken phoneLogin(String loginCode, String phoneCode) {
|
||||
// 1. 解析微信登录凭证,获取会话信息
|
||||
WxMaJscode2SessionResult session = resolveSession(loginCode);
|
||||
String openid = session.getOpenid();
|
||||
|
||||
// 2. 解析手机号授权凭证,获取手机号
|
||||
String mobile = resolvePhoneNumber(phoneCode);
|
||||
|
||||
log.info("微信小程序手机号快捷登录:openid={}, mobile={}", openid, mobile);
|
||||
|
||||
// 3. 查询或创建用户
|
||||
SysUser user = findOrCreateUser(mobile);
|
||||
|
||||
// 4. 绑定微信 openid
|
||||
bindWechatOpenid(user, session);
|
||||
|
||||
// 5. 生成认证令牌
|
||||
return generateAuthToken(mobile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定手机号
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AuthenticationToken bindMobile(String openid, String mobile, String smsCode) {
|
||||
// 1. 验证短信验证码
|
||||
validateSmsCode(mobile, smsCode);
|
||||
|
||||
// 2. 查询或创建用户
|
||||
SysUser user = findOrCreateUser(mobile);
|
||||
|
||||
// 3. 绑定微信 openid
|
||||
userSocialService.bindOrUpdate(
|
||||
user.getId(),
|
||||
SocialPlatformEnum.WECHAT_MINI,
|
||||
openid,
|
||||
null, null, null, null
|
||||
);
|
||||
|
||||
log.info("微信小程序绑定手机号成功:mobile={}, openid={}", mobile, openid);
|
||||
|
||||
// 4. 生成认证令牌
|
||||
return generateAuthToken(mobile);
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/**
|
||||
* 解析微信登录凭证,获取会话信息
|
||||
*/
|
||||
private WxMaJscode2SessionResult resolveSession(String loginCode) {
|
||||
try {
|
||||
return wxMaService.jsCode2SessionInfo(loginCode);
|
||||
} catch (Exception e) {
|
||||
log.error("获取微信会话信息失败,loginCode={}", loginCode, e);
|
||||
throw new IllegalArgumentException("微信登录失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析手机号授权凭证,获取手机号
|
||||
*/
|
||||
private String resolvePhoneNumber(String phoneCode) {
|
||||
try {
|
||||
WxMaPhoneNumberInfo phoneInfo = wxMaService.getUserService().getPhoneNoInfo(phoneCode);
|
||||
return phoneInfo.getPhoneNumber();
|
||||
} catch (Exception e) {
|
||||
log.error("获取微信手机号失败,phoneCode={}", phoneCode, e);
|
||||
throw new IllegalArgumentException("获取手机号失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询或创建用户
|
||||
*/
|
||||
private SysUser findOrCreateUser(String mobile) {
|
||||
SysUser user = userService.lambdaQuery()
|
||||
.eq(SysUser::getMobile, mobile)
|
||||
.one();
|
||||
|
||||
if (user == null) {
|
||||
user = createNewUser(mobile);
|
||||
log.info("微信小程序登录:创建新用户,mobile={}, userId={}", mobile, user.getId());
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
* <p>
|
||||
* 新用户默认分配 GUEST(访问游客)角色
|
||||
* </p>
|
||||
*/
|
||||
private SysUser createNewUser(String mobile) {
|
||||
SysUser user = new SysUser();
|
||||
user.setMobile(mobile);
|
||||
user.setUsername("wx_" + IdUtil.fastSimpleUUID().substring(0, 8));
|
||||
user.setNickname("微信用户");
|
||||
user.setStatus(1);
|
||||
user.setIsDeleted(0);
|
||||
user.setCreateTime(LocalDateTime.now());
|
||||
user.setUpdateTime(LocalDateTime.now());
|
||||
userService.save(user);
|
||||
|
||||
// 分配 GUEST 角色(角色ID=3)
|
||||
userRoleService.saveUserRoles(user.getId(), Collections.singletonList(3L));
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定微信 openid
|
||||
*/
|
||||
private void bindWechatOpenid(SysUser user, WxMaJscode2SessionResult session) {
|
||||
try {
|
||||
userSocialService.bindOrUpdate(
|
||||
user.getId(),
|
||||
SocialPlatformEnum.WECHAT_MINI,
|
||||
session.getOpenid(),
|
||||
session.getUnionid(),
|
||||
user.getNickname(),
|
||||
user.getAvatar(),
|
||||
session.getSessionKey()
|
||||
);
|
||||
} catch (Exception e) {
|
||||
// 绑定失败不影响登录
|
||||
log.warn("绑定微信 openid 失败,userId={}, openid={}", user.getId(), session.getOpenid(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证短信验证码
|
||||
*/
|
||||
private void validateSmsCode(String mobile, String smsCode) {
|
||||
String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile);
|
||||
String cachedCode = (String) redisTemplate.opsForValue().get(cacheKey);
|
||||
|
||||
if (!StrUtil.equals(smsCode, cachedCode)) {
|
||||
throw new IllegalArgumentException("验证码错误");
|
||||
}
|
||||
|
||||
// 验证成功后删除验证码
|
||||
redisTemplate.delete(cacheKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成认证令牌
|
||||
*/
|
||||
private AuthenticationToken generateAuthToken(String mobile) {
|
||||
SysUserDetails userDetails = new SysUserDetails(userService.getAuthInfoByMobile(mobile));
|
||||
Authentication authentication = new UsernamePasswordAuthenticationToken(
|
||||
userDetails, null, userDetails.getAuthorities()
|
||||
);
|
||||
AuthenticationToken authToken = tokenManager.generateToken(authentication);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
return authToken;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package com.youlai.boot.config.property;
|
||||
package com.youlai.boot.codegen.config;
|
||||
|
||||
import cn.hutool.core.io.file.FileNameUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
@@ -16,6 +17,7 @@ import java.util.Map;
|
||||
* @since 2.11.0
|
||||
*/
|
||||
@Component
|
||||
@EnableConfigurationProperties(CodegenProperties.class)
|
||||
@ConfigurationProperties(prefix = "codegen")
|
||||
@Data
|
||||
public class CodegenProperties {
|
||||
@@ -1,17 +1,18 @@
|
||||
package com.youlai.boot.platform.codegen.controller;
|
||||
package com.youlai.boot.codegen.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.core.web.PageResult;
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.config.property.CodegenProperties;
|
||||
import com.youlai.boot.common.result.PageResult;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.codegen.config.CodegenProperties;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.platform.codegen.service.CodegenService;
|
||||
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.platform.codegen.model.query.TableQuery;
|
||||
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||
import com.youlai.boot.codegen.service.CodegenService;
|
||||
import com.youlai.boot.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.codegen.model.query.TableQuery;
|
||||
import com.youlai.boot.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.codegen.model.vo.TablePageVO;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.platform.codegen.service.GenTableService;
|
||||
import com.youlai.boot.codegen.service.GenTableService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -45,7 +46,6 @@ public class CodegenController {
|
||||
|
||||
@Operation(summary = "获取数据表分页列表")
|
||||
@GetMapping("/table")
|
||||
@Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER)
|
||||
public PageResult<TablePageVO> getTablePage(
|
||||
TableQuery queryParams
|
||||
) {
|
||||
@@ -64,7 +64,7 @@ public class CodegenController {
|
||||
|
||||
@Operation(summary = "保存代码生成配置")
|
||||
@PostMapping("/{tableName}/config")
|
||||
@Log(value = "生成代码", module = LogModuleEnum.OTHER)
|
||||
@Log(module = LogModuleEnum.CODEGEN, value = ActionTypeEnum.UPDATE)
|
||||
public Result<?> saveGenConfig(@RequestBody GenConfigForm formData) {
|
||||
genTableService.saveGenConfig(formData);
|
||||
return Result.success();
|
||||
@@ -81,20 +81,21 @@ public class CodegenController {
|
||||
|
||||
@Operation(summary = "获取预览生成代码")
|
||||
@GetMapping("/{tableName}/preview")
|
||||
@Log(value = "预览生成代码", module = LogModuleEnum.OTHER)
|
||||
public Result<List<CodegenPreviewVO>> getTablePreviewData(@PathVariable String tableName,
|
||||
@RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType) {
|
||||
List<CodegenPreviewVO> list = codegenService.getCodegenPreviewData(tableName, pageType);
|
||||
@RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType,
|
||||
@RequestParam(value = "type", required = false, defaultValue = "ts") String type) {
|
||||
List<CodegenPreviewVO> list = codegenService.getCodegenPreviewData(tableName, pageType, type);
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
@Operation(summary = "下载代码")
|
||||
@GetMapping("/{tableName}/download")
|
||||
@Log(value = "下载代码", module = LogModuleEnum.OTHER)
|
||||
@Log(module = LogModuleEnum.CODEGEN, value = ActionTypeEnum.DOWNLOAD)
|
||||
public void downloadZip(HttpServletResponse response, @PathVariable String tableName,
|
||||
@RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType) {
|
||||
@RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType,
|
||||
@RequestParam(value = "type", required = false, defaultValue = "ts") String type) {
|
||||
String[] tableNames = tableName.split(",");
|
||||
byte[] data = codegenService.downloadCode(tableNames, pageType);
|
||||
byte[] data = codegenService.downloadCode(tableNames, pageType, type);
|
||||
|
||||
response.reset();
|
||||
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8));
|
||||
@@ -104,8 +105,8 @@ public class CodegenController {
|
||||
outputStream.write(data);
|
||||
outputStream.flush();
|
||||
} catch (IOException e) {
|
||||
log.error("Error while writing the zip file to response", e);
|
||||
throw new RuntimeException("Failed to write the zip file to response", e);
|
||||
log.error("Error while writing the zip file1 to response", e);
|
||||
throw new RuntimeException("Failed to write the zip file1 to response", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.youlai.boot.platform.codegen.converter;
|
||||
package com.youlai.boot.codegen.converter;
|
||||
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTable;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTableColumn;
|
||||
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.codegen.model.entity.GenTable;
|
||||
import com.youlai.boot.codegen.model.entity.GenTableColumn;
|
||||
import com.youlai.boot.codegen.model.form.GenConfigForm;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.platform.codegen.enums;
|
||||
package com.youlai.boot.codegen.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.platform.codegen.enums;
|
||||
package com.youlai.boot.codegen.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.platform.codegen.enums;
|
||||
package com.youlai.boot.codegen.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.youlai.boot.platform.codegen.mapper;
|
||||
package com.youlai.boot.codegen.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.platform.codegen.model.bo.ColumnMetaData;
|
||||
import com.youlai.boot.platform.codegen.model.bo.TableMetaData;
|
||||
import com.youlai.boot.platform.codegen.model.query.TableQuery;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||
import com.youlai.boot.codegen.model.vo.ColumnMetaVO;
|
||||
import com.youlai.boot.codegen.model.vo.TableMetaVO;
|
||||
import com.youlai.boot.codegen.model.query.TableQuery;
|
||||
import com.youlai.boot.codegen.model.vo.TablePageVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
@@ -35,13 +35,7 @@ public interface DatabaseMapper extends BaseMapper {
|
||||
* @param tableName
|
||||
* @return
|
||||
*/
|
||||
List<ColumnMetaData> getTableColumns(String tableName);
|
||||
List<ColumnMetaVO> getTableColumns(String tableName);
|
||||
|
||||
/**
|
||||
* 获取表元数据
|
||||
*
|
||||
* @param tableName
|
||||
* @return
|
||||
*/
|
||||
TableMetaData getTableMetadata(String tableName);
|
||||
TableMetaVO getTableMetadata(String tableName);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.platform.codegen.mapper;
|
||||
package com.youlai.boot.codegen.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTableColumn;
|
||||
import com.youlai.boot.codegen.model.entity.GenTableColumn;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.platform.codegen.mapper;
|
||||
package com.youlai.boot.codegen.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTable;
|
||||
import com.youlai.boot.codegen.model.entity.GenTable;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.platform.codegen.model.entity;
|
||||
package com.youlai.boot.codegen.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.youlai.boot.platform.codegen.model.entity;
|
||||
package com.youlai.boot.codegen.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import com.youlai.boot.platform.codegen.enums.FormTypeEnum;
|
||||
import com.youlai.boot.platform.codegen.enums.QueryTypeEnum;
|
||||
import com.youlai.boot.codegen.enums.FormTypeEnum;
|
||||
import com.youlai.boot.codegen.enums.QueryTypeEnum;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.platform.codegen.model.form;
|
||||
package com.youlai.boot.codegen.model.form;
|
||||
|
||||
import com.youlai.boot.platform.codegen.enums.FormTypeEnum;
|
||||
import com.youlai.boot.platform.codegen.enums.QueryTypeEnum;
|
||||
import com.youlai.boot.codegen.enums.FormTypeEnum;
|
||||
import com.youlai.boot.codegen.enums.QueryTypeEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.platform.codegen.model.query;
|
||||
package com.youlai.boot.codegen.model.query;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.youlai.boot.common.base.BaseQuery;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.platform.codegen.model.query;
|
||||
package com.youlai.boot.codegen.model.query;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.youlai.boot.common.base.BaseQuery;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.platform.codegen.model.vo;
|
||||
package com.youlai.boot.codegen.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@@ -16,4 +16,10 @@ public class CodegenPreviewVO {
|
||||
@Schema(description = "生成文件内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "文件范围(frontend/backend)")
|
||||
private String scope;
|
||||
|
||||
@Schema(description = "文件语言(扩展名)")
|
||||
private String language;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.youlai.boot.codegen.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "数据表字段元数据")
|
||||
@Data
|
||||
public class ColumnMetaVO {
|
||||
|
||||
private String columnName;
|
||||
|
||||
private String dataType;
|
||||
|
||||
private String columnComment;
|
||||
|
||||
private Long characterMaximumLength;
|
||||
|
||||
private Integer isPrimaryKey;
|
||||
|
||||
private String isNullable;
|
||||
|
||||
private String characterSetName;
|
||||
|
||||
private String collationName;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.youlai.boot.codegen.model.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 数据表元数据
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Data
|
||||
public class TableMetaVO {
|
||||
|
||||
private String tableName;
|
||||
|
||||
private String tableComment;
|
||||
|
||||
private String tableCollation;
|
||||
|
||||
private String engine;
|
||||
|
||||
private String charset;
|
||||
|
||||
private String createTime;
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.platform.codegen.model.vo;
|
||||
package com.youlai.boot.codegen.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.youlai.boot.platform.codegen.service;
|
||||
package com.youlai.boot.codegen.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.platform.codegen.model.query.TableQuery;
|
||||
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||
import com.youlai.boot.codegen.model.query.TableQuery;
|
||||
import com.youlai.boot.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.codegen.model.vo.TablePageVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -29,12 +29,12 @@ public interface CodegenService {
|
||||
* @param tableName 表名
|
||||
* @return
|
||||
*/
|
||||
List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType);
|
||||
List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType, String type);
|
||||
|
||||
/**
|
||||
* 下载代码
|
||||
* @param tableNames 表名
|
||||
* @return
|
||||
*/
|
||||
byte[] downloadCode(String[] tableNames, String pageType);
|
||||
byte[] downloadCode(String[] tableNames, String pageType, String type);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.platform.codegen.service;
|
||||
package com.youlai.boot.codegen.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTableColumn;
|
||||
import com.youlai.boot.codegen.model.entity.GenTableColumn;
|
||||
|
||||
/**
|
||||
* 代码生成配置接口
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.youlai.boot.platform.codegen.service;
|
||||
package com.youlai.boot.codegen.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTable;
|
||||
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.codegen.model.entity.GenTable;
|
||||
import com.youlai.boot.codegen.model.form.GenConfigForm;
|
||||
|
||||
/**
|
||||
* 代码生成配置接口
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.platform.codegen.service.impl;
|
||||
package com.youlai.boot.codegen.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
@@ -11,18 +11,18 @@ import cn.hutool.extra.template.TemplateEngine;
|
||||
import cn.hutool.extra.template.TemplateUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.platform.codegen.enums.JavaTypeEnum;
|
||||
import com.youlai.boot.config.property.CodegenProperties;
|
||||
import com.youlai.boot.platform.codegen.service.GenTableService;
|
||||
import com.youlai.boot.platform.codegen.service.GenTableColumnService;
|
||||
import com.youlai.boot.platform.codegen.service.CodegenService;
|
||||
import com.youlai.boot.core.exception.BusinessException;
|
||||
import com.youlai.boot.platform.codegen.mapper.DatabaseMapper;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTable;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTableColumn;
|
||||
import com.youlai.boot.platform.codegen.model.query.TableQuery;
|
||||
import com.youlai.boot.platform.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.platform.codegen.model.vo.TablePageVO;
|
||||
import com.youlai.boot.codegen.enums.JavaTypeEnum;
|
||||
import com.youlai.boot.codegen.config.CodegenProperties;
|
||||
import com.youlai.boot.codegen.service.GenTableService;
|
||||
import com.youlai.boot.codegen.service.GenTableColumnService;
|
||||
import com.youlai.boot.codegen.service.CodegenService;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.codegen.mapper.DatabaseMapper;
|
||||
import com.youlai.boot.codegen.model.entity.GenTable;
|
||||
import com.youlai.boot.codegen.model.entity.GenTableColumn;
|
||||
import com.youlai.boot.codegen.model.query.TableQuery;
|
||||
import com.youlai.boot.codegen.model.vo.CodegenPreviewVO;
|
||||
import com.youlai.boot.codegen.model.vo.TablePageVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -70,6 +70,49 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
return databaseMapper.getTablePage(page, queryParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析前端模板路径
|
||||
*
|
||||
* @param templateName 模板标识
|
||||
* @param templateConfig 模板配置
|
||||
* @param frontendType 前端类型
|
||||
* @return 模板路径
|
||||
*/
|
||||
private String resolveFrontendTemplatePath(String templateName,
|
||||
CodegenProperties.TemplateConfig templateConfig,
|
||||
String frontendType) {
|
||||
if (!"js".equals(frontendType)) {
|
||||
return templateConfig.getTemplatePath();
|
||||
}
|
||||
if ("API".equals(templateName)) {
|
||||
return "codegen/frontend/js/api.js.vm";
|
||||
}
|
||||
if ("VIEW".equals(templateName)) {
|
||||
return "codegen/frontend/js/index.js.vue.vm";
|
||||
}
|
||||
return templateConfig.getTemplatePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析前端文件后缀
|
||||
*
|
||||
* @param templateName 模板标识
|
||||
* @param templateConfig 模板配置
|
||||
* @param frontendType 前端类型
|
||||
* @return 文件后缀
|
||||
*/
|
||||
private String resolveFrontendExtension(String templateName,
|
||||
CodegenProperties.TemplateConfig templateConfig,
|
||||
String frontendType) {
|
||||
if (!"js".equals(frontendType)) {
|
||||
return templateConfig.getExtension();
|
||||
}
|
||||
if ("API".equals(templateName) || "API_TYPES".equals(templateName)) {
|
||||
return ".js";
|
||||
}
|
||||
return templateConfig.getExtension();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预览生成代码
|
||||
*
|
||||
@@ -77,7 +120,7 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
* @return 预览数据
|
||||
*/
|
||||
@Override
|
||||
public List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType) {
|
||||
public List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType, String type) {
|
||||
|
||||
List<CodegenPreviewVO> list = new ArrayList<>();
|
||||
|
||||
@@ -99,22 +142,31 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
|
||||
// 遍历模板配置
|
||||
Map<String, CodegenProperties.TemplateConfig> templateConfigs = codegenProperties.getTemplateConfigs();
|
||||
String frontendType = StrUtil.blankToDefault(type, "ts").toLowerCase();
|
||||
for (Map.Entry<String, CodegenProperties.TemplateConfig> templateConfigEntry : templateConfigs.entrySet()) {
|
||||
CodegenPreviewVO previewVo = new CodegenPreviewVO();
|
||||
|
||||
CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue();
|
||||
|
||||
String templateName = templateConfigEntry.getKey();
|
||||
if ("js".equals(frontendType) && "API_TYPES".equals(templateName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String effectiveTemplatePath = resolveFrontendTemplatePath(templateName, templateConfig, frontendType);
|
||||
String extension = resolveFrontendExtension(templateName, templateConfig, frontendType);
|
||||
|
||||
/* 1. 生成文件名 UserController */
|
||||
// User Role Menu Dept
|
||||
String entityName = genTable.getEntityName();
|
||||
// Controller Service Mapper Entity
|
||||
String templateName = templateConfigEntry.getKey();
|
||||
// .java .ts .vue
|
||||
String extension = templateConfig.getExtension();
|
||||
|
||||
// 文件名 UserController.java
|
||||
String fileName = getFileName(entityName, templateName, extension);
|
||||
previewVo.setFileName(fileName);
|
||||
previewVo.setScope(resolveScope(templateName));
|
||||
previewVo.setLanguage(resolveLanguage(fileName));
|
||||
|
||||
/* 2. 生成文件路径 */
|
||||
// 包名:com.youlai.boot
|
||||
@@ -131,7 +183,13 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
// 将模板文件中的变量替换为具体的值 生成代码内容
|
||||
// 优先使用保存的 ui,没有则使用请求参数
|
||||
String finalType = StrUtil.blankToDefault(genTable.getPageType(), pageType);
|
||||
String content = getCodeContent(templateConfig, genTable, fieldConfigs, finalType);
|
||||
String content = getCodeContent(
|
||||
effectiveTemplatePath,
|
||||
templateConfig.getSubpackageName(),
|
||||
genTable,
|
||||
fieldConfigs,
|
||||
finalType
|
||||
);
|
||||
previewVo.setContent(content);
|
||||
|
||||
list.add(previewVo);
|
||||
@@ -139,6 +197,17 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
return list;
|
||||
}
|
||||
|
||||
private String resolveScope(String templateName) {
|
||||
return switch (templateName) {
|
||||
case "API", "API_TYPES", "VIEW" -> "frontend";
|
||||
default -> "backend";
|
||||
};
|
||||
}
|
||||
|
||||
private String resolveLanguage(String fileName) {
|
||||
return FileNameUtil.extName(fileName).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成文件名。
|
||||
*
|
||||
@@ -232,7 +301,11 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
* @param pageType 前端页面类型
|
||||
* @return 渲染后的代码内容
|
||||
*/
|
||||
private String getCodeContent(CodegenProperties.TemplateConfig templateConfig, GenTable genTable, List<GenTableColumn> fieldConfigs, String pageType) {
|
||||
private String getCodeContent(String templatePath,
|
||||
String subpackageName,
|
||||
GenTable genTable,
|
||||
List<GenTableColumn> fieldConfigs,
|
||||
String pageType) {
|
||||
|
||||
Map<String, Object> bindMap = new HashMap<>();
|
||||
|
||||
@@ -240,7 +313,7 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
|
||||
bindMap.put("packageName", genTable.getPackageName());
|
||||
bindMap.put("moduleName", genTable.getModuleName());
|
||||
bindMap.put("subpackageName", templateConfig.getSubpackageName());
|
||||
bindMap.put("subpackageName", subpackageName);
|
||||
bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm"));
|
||||
bindMap.put("entityName", entityName);
|
||||
bindMap.put("tableName", genTable.getTableName());
|
||||
@@ -282,10 +355,14 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
|
||||
TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH));
|
||||
// 根据 ui 选择不同的前端页面模板:默认 index.vue.vm;封装版使用 index.curd.vue.vm
|
||||
String path = templateConfig.getTemplatePath();
|
||||
if ("curd".equalsIgnoreCase(pageType) && path.endsWith("index.vue.vm")) {
|
||||
String path = templatePath;
|
||||
if ("curd".equalsIgnoreCase(pageType)) {
|
||||
if (path.endsWith("index.js.vue.vm")) {
|
||||
path = path.replace("index.js.vue.vm", "index.curd.js.vue.vm");
|
||||
} else if (path.endsWith("index.vue.vm")) {
|
||||
path = path.replace("index.vue.vm", "index.curd.vue.vm");
|
||||
}
|
||||
}
|
||||
Template template = templateEngine.getTemplate(path);
|
||||
|
||||
return template.render(bindMap);
|
||||
@@ -299,13 +376,13 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
* @return zip 压缩文件字节数组
|
||||
*/
|
||||
@Override
|
||||
public byte[] downloadCode(String[] tableNames, String ui) {
|
||||
public byte[] downloadCode(String[] tableNames, String ui, String type) {
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
ZipOutputStream zip = new ZipOutputStream(outputStream)) {
|
||||
|
||||
// 遍历每个表名,生成对应的代码并压缩到 zip 文件中
|
||||
for (String tableName : tableNames) {
|
||||
generateAndZipCode(tableName, zip, ui);
|
||||
generateAndZipCode(tableName, zip, ui, type);
|
||||
}
|
||||
// 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整
|
||||
zip.finish();
|
||||
@@ -313,7 +390,7 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("Error while generating zip for code download", e);
|
||||
throw new RuntimeException("Failed to generate code zip file", e);
|
||||
throw new RuntimeException("Failed to generate code zip file1", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,8 +401,8 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
* @param zip 压缩文件输出流
|
||||
* @param ui 页面类型
|
||||
*/
|
||||
private void generateAndZipCode(String tableName, ZipOutputStream zip, String ui) {
|
||||
List<CodegenPreviewVO> codePreviewList = getCodegenPreviewData(tableName, ui);
|
||||
private void generateAndZipCode(String tableName, ZipOutputStream zip, String ui, String type) {
|
||||
List<CodegenPreviewVO> codePreviewList = getCodegenPreviewData(tableName, ui, type);
|
||||
|
||||
for (CodegenPreviewVO codePreview : codePreviewList) {
|
||||
String fileName = codePreview.getFileName();
|
||||
@@ -344,7 +421,7 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
zip.closeEntry();
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("Error while adding file {} to zip", fileName, e);
|
||||
log.error("Error while adding file1 {} to zip", fileName, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.youlai.boot.platform.codegen.service.impl;
|
||||
package com.youlai.boot.codegen.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.platform.codegen.mapper.GenTableColumnMapper;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTableColumn;
|
||||
import com.youlai.boot.platform.codegen.service.GenTableColumnService;
|
||||
import com.youlai.boot.codegen.mapper.GenTableColumnMapper;
|
||||
import com.youlai.boot.codegen.model.entity.GenTableColumn;
|
||||
import com.youlai.boot.codegen.service.GenTableColumnService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.platform.codegen.service.impl;
|
||||
package com.youlai.boot.codegen.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
@@ -7,21 +7,21 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.YouLaiBootApplication;
|
||||
import com.youlai.boot.common.enums.EnvEnum;
|
||||
import com.youlai.boot.platform.codegen.enums.FormTypeEnum;
|
||||
import com.youlai.boot.platform.codegen.enums.JavaTypeEnum;
|
||||
import com.youlai.boot.platform.codegen.enums.QueryTypeEnum;
|
||||
import com.youlai.boot.core.exception.BusinessException;
|
||||
import com.youlai.boot.config.property.CodegenProperties;
|
||||
import com.youlai.boot.platform.codegen.converter.CodegenConverter;
|
||||
import com.youlai.boot.platform.codegen.mapper.DatabaseMapper;
|
||||
import com.youlai.boot.platform.codegen.mapper.GenTableMapper;
|
||||
import com.youlai.boot.platform.codegen.model.bo.ColumnMetaData;
|
||||
import com.youlai.boot.platform.codegen.model.bo.TableMetaData;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTable;
|
||||
import com.youlai.boot.platform.codegen.model.entity.GenTableColumn;
|
||||
import com.youlai.boot.platform.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.platform.codegen.service.GenTableService;
|
||||
import com.youlai.boot.platform.codegen.service.GenTableColumnService;
|
||||
import com.youlai.boot.codegen.enums.FormTypeEnum;
|
||||
import com.youlai.boot.codegen.enums.JavaTypeEnum;
|
||||
import com.youlai.boot.codegen.enums.QueryTypeEnum;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.codegen.config.CodegenProperties;
|
||||
import com.youlai.boot.codegen.converter.CodegenConverter;
|
||||
import com.youlai.boot.codegen.mapper.DatabaseMapper;
|
||||
import com.youlai.boot.codegen.mapper.GenTableMapper;
|
||||
import com.youlai.boot.codegen.model.vo.ColumnMetaVO;
|
||||
import com.youlai.boot.codegen.model.vo.TableMetaVO;
|
||||
import com.youlai.boot.codegen.model.entity.GenTable;
|
||||
import com.youlai.boot.codegen.model.entity.GenTableColumn;
|
||||
import com.youlai.boot.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.codegen.service.GenTableService;
|
||||
import com.youlai.boot.codegen.service.GenTableColumnService;
|
||||
import com.youlai.boot.system.service.MenuService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -72,7 +72,7 @@ public class GenTableServiceImpl extends ServiceImpl<GenTableMapper, GenTable> i
|
||||
|
||||
// 如果没有代码生成配置,则根据表的元数据生成默认配置
|
||||
if (genTable == null) {
|
||||
TableMetaData tableMetadata = databaseMapper.getTableMetadata(tableName);
|
||||
TableMetaVO tableMetadata = databaseMapper.getTableMetadata(tableName);
|
||||
Assert.isTrue(tableMetadata != null, "未找到表元数据");
|
||||
|
||||
genTable = new GenTable();
|
||||
@@ -100,7 +100,7 @@ public class GenTableServiceImpl extends ServiceImpl<GenTableMapper, GenTable> i
|
||||
List<GenTableColumn> genTableColumns = new ArrayList<>();
|
||||
|
||||
// 获取表的列
|
||||
List<ColumnMetaData> tableColumns = databaseMapper.getTableColumns(tableName);
|
||||
List<ColumnMetaVO> tableColumns = databaseMapper.getTableColumns(tableName);
|
||||
if (CollectionUtil.isNotEmpty(tableColumns)) {
|
||||
// 查询字段生成配置
|
||||
List<GenTableColumn> fieldConfigList = genTableColumnService.list(
|
||||
@@ -113,7 +113,7 @@ public class GenTableServiceImpl extends ServiceImpl<GenTableMapper, GenTable> i
|
||||
.filter(Objects::nonNull) // 过滤掉空值
|
||||
.max(Integer::compareTo)
|
||||
.orElse(0);
|
||||
for (ColumnMetaData tableColumn : tableColumns) {
|
||||
for (ColumnMetaVO tableColumn : tableColumns) {
|
||||
// 根据列名获取字段生成配置
|
||||
String columnName = tableColumn.getColumnName();
|
||||
GenTableColumn fieldConfig = fieldConfigList.stream()
|
||||
@@ -150,16 +150,16 @@ public class GenTableServiceImpl extends ServiceImpl<GenTableMapper, GenTable> i
|
||||
/**
|
||||
* 创建默认字段配置
|
||||
*
|
||||
* @param columnMetaData 表字段元数据
|
||||
* @param columnMetaVO 表字段元数据
|
||||
* @return
|
||||
*/
|
||||
private GenTableColumn createDefaultFieldConfig(ColumnMetaData columnMetaData) {
|
||||
private GenTableColumn createDefaultFieldConfig(ColumnMetaVO columnMetaVO) {
|
||||
GenTableColumn fieldConfig = new GenTableColumn();
|
||||
fieldConfig.setColumnName(columnMetaData.getColumnName());
|
||||
fieldConfig.setColumnType(columnMetaData.getDataType());
|
||||
fieldConfig.setFieldComment(columnMetaData.getColumnComment());
|
||||
fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaData.getColumnName()));
|
||||
fieldConfig.setIsRequired("YES".equals(columnMetaData.getIsNullable()) ? 0 : 1);
|
||||
fieldConfig.setColumnName(columnMetaVO.getColumnName());
|
||||
fieldConfig.setColumnType(columnMetaVO.getDataType());
|
||||
fieldConfig.setFieldComment(columnMetaVO.getColumnComment());
|
||||
fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaVO.getColumnName()));
|
||||
fieldConfig.setIsRequired("YES".equals(columnMetaVO.getIsNullable()) ? 0 : 1);
|
||||
|
||||
String columnType = StrUtil.blankToDefault(fieldConfig.getColumnType(), "").toLowerCase();
|
||||
if ("date".equals(columnType)) {
|
||||
@@ -171,7 +171,7 @@ public class GenTableServiceImpl extends ServiceImpl<GenTableMapper, GenTable> i
|
||||
}
|
||||
|
||||
fieldConfig.setQueryType(QueryTypeEnum.EQ);
|
||||
fieldConfig.setMaxLength(columnMetaData.getCharacterMaximumLength());
|
||||
fieldConfig.setMaxLength(columnMetaVO.getCharacterMaximumLength());
|
||||
return fieldConfig;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.youlai.boot.common.annotation;
|
||||
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
@@ -16,34 +17,31 @@ import java.lang.annotation.*;
|
||||
public @interface Log {
|
||||
|
||||
/**
|
||||
* 日志描述
|
||||
* 模块
|
||||
*
|
||||
* @return 日志描述
|
||||
* @return 模块
|
||||
*/
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* 日志模块
|
||||
*
|
||||
* @return 日志模块
|
||||
*/
|
||||
|
||||
LogModuleEnum module();
|
||||
|
||||
/**
|
||||
* 是否记录请求参数
|
||||
* 操作类型
|
||||
*
|
||||
* @return 是否记录请求参数
|
||||
* @return 操作类型
|
||||
*/
|
||||
boolean params() default true;
|
||||
ActionTypeEnum value();
|
||||
|
||||
/**
|
||||
* 是否记录响应结果
|
||||
* <br/>
|
||||
* 响应结果默认不记录,避免日志过大
|
||||
* @return 是否记录响应结果
|
||||
* 操作标题(可选,默认使用枚举描述)
|
||||
*
|
||||
* @return 标题
|
||||
*/
|
||||
boolean result() default false;
|
||||
String title() default "";
|
||||
|
||||
/**
|
||||
* 自定义日志内容(可选,用于记录操作细节)
|
||||
*
|
||||
* @return 日志内容
|
||||
*/
|
||||
String content() default "";
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.youlai.boot.common.annotation;
|
||||
|
||||
import com.youlai.boot.core.validator.FieldValidator;
|
||||
import com.youlai.boot.common.validator.FieldValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
|
||||
144
src/main/java/com/youlai/boot/common/aspect/LogAspect.java
Normal file
144
src/main/java/com/youlai/boot/common/aspect/LogAspect.java
Normal file
@@ -0,0 +1,144 @@
|
||||
package com.youlai.boot.common.aspect;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.useragent.UserAgent;
|
||||
import cn.hutool.http.useragent.UserAgentUtil;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.util.IPUtils;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import com.youlai.boot.system.model.entity.SysLog;
|
||||
import com.youlai.boot.system.service.LogService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 日志切面
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class LogAspect {
|
||||
|
||||
private final LogService logService;
|
||||
|
||||
/**
|
||||
* 日志注解切点
|
||||
*/
|
||||
@Pointcut("@annotation(logAnnotation)")
|
||||
public void logPointCut(Log logAnnotation) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 环绕通知:记录操作日志
|
||||
*/
|
||||
@Around(value = "logPointCut(logAnnotation)", argNames = "pjp,logAnnotation")
|
||||
public Object around(ProceedingJoinPoint pjp, Log logAnnotation) throws Throwable {
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
// 在方法执行前获取用户信息,避免 logout 等操作清除 SecurityContext 后无法获取
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
String username = SecurityUtils.getUsername();
|
||||
Object result = null;
|
||||
Exception exception = null;
|
||||
|
||||
try {
|
||||
result = pjp.proceed();
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
exception = e;
|
||||
throw e;
|
||||
} finally {
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
// fallback:登录等场景在 proceed() 前未认证,需在 proceed() 后获取
|
||||
if (userId == null) {
|
||||
userId = SecurityUtils.getUserId();
|
||||
username = SecurityUtils.getUsername();
|
||||
}
|
||||
try {
|
||||
saveLogAsync(logAnnotation, executionTime, exception, userId, username);
|
||||
} catch (Exception ex) {
|
||||
log.error("保存操作日志失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步保存日志
|
||||
*/
|
||||
@Async
|
||||
public void saveLogAsync(Log logAnnotation, long executionTime, Exception exception, Long userId, String username) {
|
||||
try {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
|
||||
// 解析 User-Agent
|
||||
String userAgentStr = request.getHeader("User-Agent");
|
||||
UserAgent userAgent = UserAgentUtil.parse(userAgentStr);
|
||||
|
||||
// 解析 IP 地区
|
||||
String ip = IPUtils.getIpAddr(request);
|
||||
String region = IPUtils.getRegion(ip);
|
||||
String province = null;
|
||||
String city = null;
|
||||
if (StrUtil.isNotBlank(region)) {
|
||||
String[] parts = region.split("\\|");
|
||||
if (parts.length >= 3) {
|
||||
province = StrUtil.blankToDefault(parts[2], null);
|
||||
city = StrUtil.blankToDefault(parts[3], null);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建日志实体
|
||||
LogModuleEnum module = logAnnotation.module();
|
||||
ActionTypeEnum actionType = logAnnotation.value();
|
||||
String title = StrUtil.blankToDefault(logAnnotation.title(),
|
||||
module.getLabel() + "-" + actionType.getLabel());
|
||||
String content = logAnnotation.content();
|
||||
|
||||
SysLog logEntity = new SysLog();
|
||||
logEntity.setModule(module);
|
||||
logEntity.setActionType(actionType);
|
||||
logEntity.setTitle(title);
|
||||
logEntity.setContent(content);
|
||||
logEntity.setOperatorId(userId);
|
||||
logEntity.setOperatorName(username);
|
||||
logEntity.setRequestUri(request.getRequestURI());
|
||||
logEntity.setRequestMethod(request.getMethod());
|
||||
logEntity.setIp(ip);
|
||||
logEntity.setProvince(province);
|
||||
logEntity.setCity(city);
|
||||
logEntity.setDevice(userAgent.getOs().getName());
|
||||
logEntity.setOs(userAgent.getOs().getName());
|
||||
logEntity.setBrowser(userAgent.getBrowser().getName());
|
||||
logEntity.setStatus(exception == null ? 1 : 0);
|
||||
logEntity.setErrorMsg(exception != null ? exception.getMessage() : null);
|
||||
logEntity.setExecutionTime((int) executionTime);
|
||||
logEntity.setCreateTime(LocalDateTime.now());
|
||||
|
||||
logService.save(logEntity);
|
||||
} catch (Exception e) {
|
||||
log.error("保存操作日志异常: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.youlai.boot.core.aspect;
|
||||
package com.youlai.boot.common.aspect;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.core.web.ResultCode;
|
||||
import com.youlai.boot.core.exception.BusinessException;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.annotation.RepeatSubmit;
|
||||
import com.youlai.boot.common.util.IPUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
@@ -26,9 +26,11 @@ public interface JwtClaimConstants {
|
||||
String DEPT_ID = "deptId";
|
||||
|
||||
/**
|
||||
* 数据权限
|
||||
* 数据权限列表
|
||||
* <p>
|
||||
* 存储用户所有角色的数据权限范围,用于实现多角色权限合并(并集策略)
|
||||
*/
|
||||
String DATA_SCOPE = "dataScope";
|
||||
String DATA_SCOPES = "dataScopes";
|
||||
|
||||
/**
|
||||
* 权限(角色Code)集合
|
||||
@@ -36,8 +38,11 @@ public interface JwtClaimConstants {
|
||||
String AUTHORITIES = "authorities";
|
||||
|
||||
/**
|
||||
* 安全版本号,用于按用户失效历史令牌
|
||||
* Token 版本号
|
||||
* <p>
|
||||
* 用于用户级会话失效,当用户修改密码、被禁用、强制下线时递增版本号,
|
||||
* 使该用户之前签发的所有 Token 失效。
|
||||
*/
|
||||
String SECURITY_VERSION = "securityVersion";
|
||||
String TOKEN_VERSION = "tokenVersion";
|
||||
|
||||
}
|
||||
|
||||
@@ -26,18 +26,19 @@ public interface RedisConstants {
|
||||
* 认证模块
|
||||
*/
|
||||
interface Auth {
|
||||
// 存储访问令牌对应的用户信息(accessToken -> OnlineUser)
|
||||
// 存储访问令牌对应的用户会话信息(accessToken -> UserSession)
|
||||
String ACCESS_TOKEN_USER = "auth:token:access:{}";
|
||||
// 存储刷新令牌对应的用户信息(refreshToken -> OnlineUser)
|
||||
// 存储刷新令牌对应的用户会话信息(refreshToken -> UserSession)
|
||||
String REFRESH_TOKEN_USER = "auth:token:refresh:{}";
|
||||
// 用户与访问令牌的映射(userId -> accessToken)
|
||||
String USER_ACCESS_TOKEN = "auth:user:access:{}";
|
||||
// 用户与刷新令牌的映射(userId -> refreshToken
|
||||
String USER_REFRESH_TOKEN = "auth:user:refresh:{}";
|
||||
// 黑名单 Token(用于退出登录或注销)
|
||||
// 已撤销 Token 的 JTI(单端退出/会话注销):如果 jti 在撤销列表中,则 Token 立即无效
|
||||
String BLACKLIST_TOKEN = "auth:token:blacklist:{}";
|
||||
// 用户安全版本号(用于按用户失效历史 JWT)
|
||||
String USER_SECURITY_VERSION = "auth:user:security_version:{}";
|
||||
String REVOKED_JTI = BLACKLIST_TOKEN;
|
||||
// 用户 Token 版本号(用于按用户失效历史 JWT):token.tokenVersion != redis.tokenVersion => token 无效
|
||||
String USER_TOKEN_VERSION = "auth:user:token_version:{}";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.youlai.boot.common.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import com.youlai.boot.common.base.IBaseEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 操作类型枚举
|
||||
*
|
||||
* @author Ray
|
||||
* @since 2.10.0
|
||||
*/
|
||||
@Schema(enumAsRef = true)
|
||||
@Getter
|
||||
public enum ActionTypeEnum implements IBaseEnum<Integer> {
|
||||
|
||||
LOGIN(1, "登录"),
|
||||
LOGOUT(2, "登出"),
|
||||
INSERT(3, "新增"),
|
||||
UPDATE(4, "修改"),
|
||||
DELETE(5, "删除"),
|
||||
GRANT(6, "授权"),
|
||||
EXPORT(7, "导出"),
|
||||
IMPORT(8, "导入"),
|
||||
UPLOAD(9, "上传"),
|
||||
DOWNLOAD(10, "下载"),
|
||||
CHANGE_PASSWORD(11, "修改密码"),
|
||||
RESET_PASSWORD(12, "重置密码"),
|
||||
ENABLE(13, "启用"),
|
||||
DISABLE(14, "禁用"),
|
||||
LIST(15, "查询列表"),
|
||||
OTHER(99, "其他");
|
||||
|
||||
@EnumValue
|
||||
private final Integer value;
|
||||
|
||||
@JsonValue
|
||||
private final String label;
|
||||
|
||||
ActionTypeEnum(Integer value, String label) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return this.label;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 数据权限枚举
|
||||
* <p>
|
||||
* 多角色数据权限合并策略:取并集(OR),即用户能看到所有角色权限范围内的数据。
|
||||
* 如果任一角色是 ALL,则直接跳过数据权限过滤。
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2.3.0
|
||||
@@ -13,12 +16,31 @@ import lombok.Getter;
|
||||
public enum DataScopeEnum implements IBaseEnum<Integer> {
|
||||
|
||||
/**
|
||||
* value 越小,数据权限范围越大
|
||||
* 所有数据权限 - 最高权限,可查看所有数据
|
||||
*/
|
||||
ALL(1, "所有数据"),
|
||||
|
||||
/**
|
||||
* 部门及子部门数据 - 可查看本部门及其下属所有部门的数据
|
||||
*/
|
||||
DEPT_AND_SUB(2, "部门及子部门数据"),
|
||||
|
||||
/**
|
||||
* 本部门数据 - 仅可查看本部门的数据
|
||||
*/
|
||||
DEPT(3, "本部门数据"),
|
||||
SELF(4, "本人数据");
|
||||
|
||||
/**
|
||||
* 本人数据 - 仅可查看自己的数据
|
||||
*/
|
||||
SELF(4, "本人数据"),
|
||||
|
||||
/**
|
||||
* 自定义部门数据 - 可查看指定部门的数据
|
||||
* <p>
|
||||
* 需要配合 sys_role_dept 表使用,存储角色可访问的部门ID列表
|
||||
*/
|
||||
CUSTOM(5, "自定义部门数据");
|
||||
|
||||
private final Integer value;
|
||||
|
||||
@@ -28,4 +50,32 @@ public enum DataScopeEnum implements IBaseEnum<Integer> {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为全部数据权限
|
||||
*
|
||||
* @param value 数据权限值
|
||||
* @return 是否为全部数据权限
|
||||
*/
|
||||
public static boolean isAll(Integer value) {
|
||||
return ALL.getValue().equals(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据值获取枚举
|
||||
*
|
||||
* @param value 数据权限值
|
||||
* @return 枚举对象,未找到则返回 null
|
||||
*/
|
||||
public static DataScopeEnum getByValue(Integer value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
for (DataScopeEnum dataScope : values()) {
|
||||
if (dataScope.getValue().equals(value)) {
|
||||
return dataScope;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.youlai.boot.common.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import com.youlai.boot.common.base.IBaseEnum;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -12,22 +14,39 @@ import lombok.Getter;
|
||||
*/
|
||||
@Schema(enumAsRef = true)
|
||||
@Getter
|
||||
public enum LogModuleEnum {
|
||||
public enum LogModuleEnum implements IBaseEnum<Integer> {
|
||||
|
||||
EXCEPTION("异常"),
|
||||
LOGIN("登录"),
|
||||
USER("用户"),
|
||||
DEPT("部门"),
|
||||
ROLE("角色"),
|
||||
MENU("菜单"),
|
||||
DICT("字典"),
|
||||
SETTING("系统配置"),
|
||||
OTHER("其他");
|
||||
LOGIN(1, "登录"),
|
||||
USER(2, "用户管理"),
|
||||
ROLE(3, "角色管理"),
|
||||
DEPT(4, "部门管理"),
|
||||
MENU(5, "菜单管理"),
|
||||
DICT(6, "字典管理"),
|
||||
CONFIG(7, "系统配置"),
|
||||
FILE(8, "文件管理"),
|
||||
NOTICE(9, "通知公告"),
|
||||
LOG(10, "日志管理"),
|
||||
CODEGEN(11, "代码生成"),
|
||||
OTHER(99, "其他");
|
||||
|
||||
@EnumValue
|
||||
private final Integer value;
|
||||
|
||||
@JsonValue
|
||||
private final String moduleName;
|
||||
private final String label;
|
||||
|
||||
LogModuleEnum(String moduleName) {
|
||||
this.moduleName = moduleName;
|
||||
LogModuleEnum(Integer value, String label) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return this.label;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.youlai.boot.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum RequestMethodEnum {
|
||||
/**
|
||||
* 搜寻 @AnonymousGetMapping
|
||||
*/
|
||||
GET("GET"),
|
||||
|
||||
/**
|
||||
* 搜寻 @AnonymousPostMapping
|
||||
*/
|
||||
POST("POST"),
|
||||
|
||||
/**
|
||||
* 搜寻 @AnonymousPutMapping
|
||||
*/
|
||||
PUT("PUT"),
|
||||
|
||||
/**
|
||||
* 搜寻 @AnonymousPatchMapping
|
||||
*/
|
||||
PATCH("PATCH"),
|
||||
|
||||
/**
|
||||
* 搜寻 @AnonymousDeleteMapping
|
||||
*/
|
||||
DELETE("DELETE"),
|
||||
|
||||
/**
|
||||
* 否则就是所有 Request 接口都放行
|
||||
*/
|
||||
ALL("All");
|
||||
|
||||
/**
|
||||
* Request 类型
|
||||
*/
|
||||
private final String type;
|
||||
|
||||
public static RequestMethodEnum find(String type) {
|
||||
for (RequestMethodEnum value : RequestMethodEnum.values()) {
|
||||
if (value.getType().equals(type)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return ALL;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.youlai.boot.core.exception;
|
||||
package com.youlai.boot.common.exception;
|
||||
|
||||
import com.youlai.boot.core.web.IResultCode;
|
||||
import com.youlai.boot.common.result.IResultCode;
|
||||
import lombok.Getter;
|
||||
import org.slf4j.helpers.MessageFormatter;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.core.web;
|
||||
package com.youlai.boot.common.result;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.core.web;
|
||||
package com.youlai.boot.common.result;
|
||||
|
||||
/**
|
||||
* 响应码接口
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.youlai.boot.core.web;
|
||||
package com.youlai.boot.common.result;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
@@ -21,13 +20,7 @@ public class PageResult<T> implements Serializable {
|
||||
|
||||
private String msg;
|
||||
|
||||
private List<T> data;
|
||||
|
||||
/**
|
||||
* 分页元信息;非分页接口不显示此字段
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
private Page page;
|
||||
private PageData<T> data;
|
||||
|
||||
/**
|
||||
* 构建分页结果(MyBatis-Plus {@link IPage})。
|
||||
@@ -43,13 +36,10 @@ public class PageResult<T> implements Serializable {
|
||||
(page == null || page.getRecords() == null)
|
||||
? Collections.emptyList()
|
||||
: page.getRecords();
|
||||
result.setData(records);
|
||||
|
||||
Page pageMeta = new Page();
|
||||
pageMeta.setPageNum(page != null ? page.getCurrent() : 1L);
|
||||
pageMeta.setPageSize(page != null ? page.getSize() : 0L);
|
||||
pageMeta.setTotal(page != null ? page.getTotal() : 0L);
|
||||
result.setPage(pageMeta);
|
||||
PageData<T> pageData = new PageData<>();
|
||||
pageData.setList(records);
|
||||
pageData.setTotal(page != null ? page.getTotal() : 0L);
|
||||
result.setData(pageData);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -63,17 +53,17 @@ public class PageResult<T> implements Serializable {
|
||||
PageResult<T> result = new PageResult<>();
|
||||
result.setCode(ResultCode.SUCCESS.getCode());
|
||||
result.setMsg(ResultCode.SUCCESS.getMsg());
|
||||
result.setData(list != null ? list : Collections.emptyList());
|
||||
result.setPage(null);
|
||||
PageData<T> pageData = new PageData<>();
|
||||
pageData.setList(list != null ? list : Collections.emptyList());
|
||||
pageData.setTotal(0L);
|
||||
result.setData(pageData);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Page {
|
||||
public static class PageData<T> {
|
||||
|
||||
private long pageNum;
|
||||
|
||||
private long pageSize;
|
||||
private List<T> list;
|
||||
|
||||
private long total;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.core.web;
|
||||
package com.youlai.boot.common.result;
|
||||
|
||||
import cn.hutool.extra.servlet.JakartaServletUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
@@ -10,7 +10,7 @@ import org.springframework.http.MediaType;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Web响应写入器
|
||||
* 响应写入器
|
||||
* <p>
|
||||
* 用于在过滤器、Security处理器等无法使用 @RestControllerAdvice 的场景中统一写入HTTP响应。
|
||||
* 支持写入成功响应和错误响应。
|
||||
@@ -20,12 +20,12 @@ import java.nio.charset.StandardCharsets;
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
public final class WebResponseWriter {
|
||||
public final class ResponseWriter {
|
||||
|
||||
/**
|
||||
* 私有构造函数,防止实例化
|
||||
*/
|
||||
private WebResponseWriter() {
|
||||
private ResponseWriter() {
|
||||
throw new UnsupportedOperationException("工具类不允许实例化");
|
||||
}
|
||||
|
||||
@@ -103,6 +103,9 @@ public final class WebResponseWriter {
|
||||
|
||||
/**
|
||||
* 根据业务结果码映射HTTP状态码
|
||||
* 401: 未认证(token无效/过期)
|
||||
* 403: 权限不足
|
||||
* 400: 其他业务错误
|
||||
*
|
||||
* @param resultCode 业务结果码
|
||||
* @return HTTP状态码
|
||||
@@ -112,11 +115,8 @@ public final class WebResponseWriter {
|
||||
case ACCESS_UNAUTHORIZED,
|
||||
ACCESS_TOKEN_INVALID,
|
||||
REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
|
||||
case ACCESS_PERMISSION_EXCEPTION -> HttpStatus.FORBIDDEN.value();
|
||||
default -> HttpStatus.BAD_REQUEST.value();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.core.web;
|
||||
package com.youlai.boot.common.result;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.Data;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.core.web;
|
||||
package com.youlai.boot.common.result;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
|
||||
package com.youlai.boot.common.util;
|
||||
|
||||
import cn.hutool.core.date.DateTime;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* 日期工具类
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2.4.2
|
||||
*/
|
||||
public class DateUtils {
|
||||
|
||||
/**
|
||||
* 区间日期格式化为数据库日期格式
|
||||
* <p>
|
||||
* eg:2021-01-01 → 2021-01-01 00:00:00
|
||||
*
|
||||
* @param obj 要处理的对象
|
||||
* @param startTimeFieldName 起始时间字段名
|
||||
* @param endTimeFieldName 结束时间字段名
|
||||
*/
|
||||
public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) {
|
||||
Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName);
|
||||
Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName);
|
||||
|
||||
if (startTimeField != null) {
|
||||
processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00");
|
||||
}
|
||||
|
||||
if (endTimeField != null) {
|
||||
processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理日期字段
|
||||
*
|
||||
* @param obj 要处理的对象
|
||||
* @param field 字段
|
||||
* @param fieldName 字段名
|
||||
* @param targetPattern 目标数据库日期格式
|
||||
*/
|
||||
private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) {
|
||||
Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName);
|
||||
if (fieldValue != null) {
|
||||
// 得到原始的日期格式
|
||||
String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd";
|
||||
// 转换为日期对象
|
||||
DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern);
|
||||
// 转换为目标数据库日期格式
|
||||
ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.core.validator;
|
||||
package com.youlai.boot.common.validator;
|
||||
|
||||
import com.youlai.boot.common.annotation.ValidField;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
@@ -18,16 +18,14 @@ public class FieldValidator implements ConstraintValidator<ValidField, String> {
|
||||
|
||||
@Override
|
||||
public void initialize(ValidField constraintAnnotation) {
|
||||
// 初始化允许的值列表
|
||||
this.allowedValues = constraintAnnotation.allowedValues();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
if (value == null) {
|
||||
return true; // 如果字段允许为空,可以返回 true
|
||||
return true;
|
||||
}
|
||||
// 检查值是否在允许列表中
|
||||
return Arrays.asList(allowedValues).contains(value);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.ValidatorFactory;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.hibernate.validator.HibernateValidator;
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Web 配置
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2020/10/16
|
||||
*/
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/**
|
||||
* 配置消息转换器
|
||||
*
|
||||
* @param converters 消息转换器列表
|
||||
*/
|
||||
@Override
|
||||
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
|
||||
MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// 注册 JavaTimeModule(替代手动注册 LocalDateTimeSerializer)
|
||||
JavaTimeModule javaTimeModule = new JavaTimeModule();
|
||||
// 返回指定字符串格式
|
||||
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
|
||||
// 反序列化,接受前端传来的格式
|
||||
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
|
||||
objectMapper.registerModule(javaTimeModule);
|
||||
|
||||
// 配置全局日期格式和时区
|
||||
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
|
||||
objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8"));
|
||||
|
||||
// 处理 Long/BigInteger 的精度问题
|
||||
SimpleModule simpleModule = new SimpleModule();
|
||||
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
|
||||
simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
|
||||
objectMapper.registerModule(simpleModule);
|
||||
|
||||
jackson2HttpMessageConverter.setObjectMapper(objectMapper);
|
||||
converters.add(1, jackson2HttpMessageConverter);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置校验器
|
||||
*
|
||||
* @param autowireCapableBeanFactory 用于注入 SpringConstraintValidatorFactory
|
||||
* @return Validator 实例
|
||||
*/
|
||||
@Bean
|
||||
public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) {
|
||||
try (ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
|
||||
.configure()
|
||||
.failFast(true) // failFast=true 时,遇到第一个校验失败则立即返回,false 表示校验所有参数
|
||||
.constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory))
|
||||
.buildValidatorFactory()) {
|
||||
|
||||
// 使用 try-with-resources 确保 ValidatorFactory 被正确关闭
|
||||
return validatorFactory.getValidator();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,293 +0,0 @@
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.security.model.SysUserDetails;
|
||||
import com.youlai.boot.security.token.TokenManager;
|
||||
import com.youlai.boot.platform.websocket.service.WebSocketService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
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 Ray.Hao
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@EnableWebSocketMessageBroker
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
private static final String WS_ENDPOINT = "/ws";
|
||||
private static final String APP_DESTINATION_PREFIX = "/app";
|
||||
private static final String USER_DESTINATION_PREFIX = "/user";
|
||||
private static final String[] BROKER_DESTINATIONS = {"/topic", "/queue"};
|
||||
|
||||
private final TokenManager tokenManager;
|
||||
private final WebSocketService webSocketService;
|
||||
|
||||
public WebSocketConfig(TokenManager tokenManager, @Lazy WebSocketService webSocketService) {
|
||||
this.tokenManager = tokenManager;
|
||||
this.webSocketService = webSocketService;
|
||||
log.info("✓ WebSocket 配置已加载");
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册 STOMP 端点
|
||||
*
|
||||
* 客户端通过该端点建立 WebSocket 连接
|
||||
*/
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry
|
||||
.addEndpoint(WS_ENDPOINT)
|
||||
.setAllowedOriginPatterns("*"); // 允许跨域(生产环境建议配置具体域名)
|
||||
|
||||
log.info("✓ STOMP 端点已注册: {}", WS_ENDPOINT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置消息代理
|
||||
*
|
||||
* - /app 前缀:客户端发送消息到服务端的前缀
|
||||
* - /topic 前缀:用于广播消息
|
||||
* - /queue 前缀:用于点对点消息
|
||||
* - /user 前缀:服务端发送给特定用户的消息前缀
|
||||
*/
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
// 客户端发送消息的请求前缀
|
||||
registry.setApplicationDestinationPrefixes(APP_DESTINATION_PREFIX);
|
||||
|
||||
// 启用简单消息代理,处理 /topic 和 /queue 前缀的消息
|
||||
registry.enableSimpleBroker(BROKER_DESTINATIONS);
|
||||
|
||||
// 服务端通知客户端的前缀
|
||||
registry.setUserDestinationPrefix(USER_DESTINATION_PREFIX);
|
||||
|
||||
log.info("✓ 消息代理已配置: app={}, broker={}, user={}",
|
||||
APP_DESTINATION_PREFIX, BROKER_DESTINATIONS, USER_DESTINATION_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置客户端入站通道拦截器
|
||||
*
|
||||
* 核心功能:
|
||||
* 1. 连接建立时:解析 JWT Token 并绑定用户身份
|
||||
* 2. 连接关闭时:触发用户下线通知
|
||||
* 3. 安全防护:拦截无效连接请求
|
||||
*/
|
||||
@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);
|
||||
|
||||
// 防御性检查:确保 accessor 不为空
|
||||
if (accessor == null) {
|
||||
log.warn("⚠ 收到异常消息:无法获取 StompHeaderAccessor");
|
||||
return ChannelInterceptor.super.preSend(message, channel);
|
||||
}
|
||||
|
||||
StompCommand command = accessor.getCommand();
|
||||
if (command == null) {
|
||||
return ChannelInterceptor.super.preSend(message, channel);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (command) {
|
||||
case CONNECT:
|
||||
handleConnect(accessor);
|
||||
break;
|
||||
|
||||
case DISCONNECT:
|
||||
handleDisconnect(accessor);
|
||||
break;
|
||||
|
||||
case SUBSCRIBE:
|
||||
handleSubscribe(accessor);
|
||||
break;
|
||||
|
||||
default:
|
||||
// 其他命令不需要特殊处理
|
||||
break;
|
||||
}
|
||||
} catch (AuthenticationException ex) {
|
||||
// 认证失败时强制关闭连接
|
||||
log.error("❌ 连接认证失败: {}", ex.getMessage());
|
||||
throw ex;
|
||||
} catch (Exception ex) {
|
||||
// 捕获其他未知异常
|
||||
log.error("❌ WebSocket 消息处理异常", ex);
|
||||
throw new MessagingException("消息处理失败: " + ex.getMessage());
|
||||
}
|
||||
|
||||
return ChannelInterceptor.super.preSend(message, channel);
|
||||
}
|
||||
});
|
||||
|
||||
log.info("✓ 客户端入站通道拦截器已配置");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端连接请求
|
||||
*
|
||||
* 安全校验流程:
|
||||
* 1. 提取 Authorization 头
|
||||
* 2. 验证 Bearer Token 格式
|
||||
* 3. 解析并验证 JWT 有效性
|
||||
* 4. 绑定用户身份到当前会话
|
||||
* 5. 记录用户上线状态
|
||||
*/
|
||||
private void handleConnect(StompHeaderAccessor accessor) {
|
||||
String authorization = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
|
||||
|
||||
// 安全检查:确保 Authorization 头存在且格式正确
|
||||
if (StrUtil.isBlank(authorization)) {
|
||||
log.warn("⚠ 非法连接请求:缺少 Authorization 头");
|
||||
throw new AuthenticationCredentialsNotFoundException("缺少 Authorization 头");
|
||||
}
|
||||
|
||||
if (!authorization.startsWith("Bearer ")) {
|
||||
log.warn("⚠ 非法连接请求:Authorization 头格式错误");
|
||||
throw new BadCredentialsException("Authorization 头格式错误");
|
||||
}
|
||||
|
||||
// 提取 JWT Token(移除 "Bearer " 前缀)
|
||||
String token = authorization.substring(7);
|
||||
|
||||
if (StrUtil.isBlank(token)) {
|
||||
log.warn("⚠ 非法连接请求:Token 为空");
|
||||
throw new BadCredentialsException("Token 为空");
|
||||
}
|
||||
|
||||
// 解析并验证 Token
|
||||
Authentication authentication;
|
||||
try {
|
||||
authentication = tokenManager.parseToken(token);
|
||||
} catch (Exception ex) {
|
||||
log.error("❌ Token 解析失败", ex);
|
||||
throw new BadCredentialsException("Token 无效: " + ex.getMessage());
|
||||
}
|
||||
|
||||
// 验证解析结果
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
log.warn("⚠ Token 解析失败:认证对象无效");
|
||||
throw new BadCredentialsException("Token 解析失败");
|
||||
}
|
||||
|
||||
// 获取用户详细信息
|
||||
Object principal = authentication.getPrincipal();
|
||||
if (!(principal instanceof SysUserDetails)) {
|
||||
log.error("❌ 无效的用户凭证类型: {}", principal.getClass().getName());
|
||||
throw new BadCredentialsException("用户凭证类型错误");
|
||||
}
|
||||
|
||||
SysUserDetails userDetails = (SysUserDetails) principal;
|
||||
String username = userDetails.getUsername();
|
||||
|
||||
if (StrUtil.isBlank(username)) {
|
||||
log.warn("⚠ 用户名为空");
|
||||
throw new BadCredentialsException("用户名为空");
|
||||
}
|
||||
|
||||
// 绑定用户身份到当前会话(重要:用于 @SendToUser 等注解)
|
||||
accessor.setUser(authentication);
|
||||
|
||||
// 获取会话 ID
|
||||
String sessionId = accessor.getSessionId();
|
||||
if (sessionId == null) {
|
||||
log.warn("⚠ 会话 ID 为空,使用临时 ID");
|
||||
sessionId = "temp-" + System.nanoTime();
|
||||
}
|
||||
|
||||
// 记录用户上线状态
|
||||
try {
|
||||
webSocketService.userConnected(username, sessionId);
|
||||
log.info("✓ WebSocket 连接建立成功: 用户[{}], 会话[{}]", username, sessionId);
|
||||
} catch (Exception ex) {
|
||||
log.error("❌ 记录用户上线状态失败: 用户[{}], 会话[{}]", username, sessionId, ex);
|
||||
// 不抛出异常,允许连接继续
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端断开连接事件
|
||||
*
|
||||
* 注意:
|
||||
* - 只有成功建立过认证的连接才会触发下线事件
|
||||
* - 防止未认证成功的连接产生脏数据
|
||||
*/
|
||||
private void handleDisconnect(StompHeaderAccessor accessor) {
|
||||
Authentication authentication = (Authentication) accessor.getUser();
|
||||
|
||||
// 防御性检查:只处理已认证的连接
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
log.debug("未认证的连接断开,跳过处理");
|
||||
return;
|
||||
}
|
||||
|
||||
Object principal = authentication.getPrincipal();
|
||||
if (!(principal instanceof SysUserDetails)) {
|
||||
log.warn("⚠ 断开连接时用户凭证类型异常");
|
||||
return;
|
||||
}
|
||||
|
||||
SysUserDetails userDetails = (SysUserDetails) principal;
|
||||
String username = userDetails.getUsername();
|
||||
|
||||
if (StrUtil.isNotBlank(username)) {
|
||||
try {
|
||||
webSocketService.userDisconnected(username);
|
||||
log.info("✓ WebSocket 连接断开: 用户[{}]", username);
|
||||
} catch (Exception ex) {
|
||||
log.error("❌ 记录用户下线状态失败: 用户[{}]", username, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理客户端订阅事件(可选)
|
||||
*
|
||||
* 用于记录订阅信息或实施订阅级别的权限控制
|
||||
*/
|
||||
private void handleSubscribe(StompHeaderAccessor accessor) {
|
||||
Authentication authentication = (Authentication) accessor.getUser();
|
||||
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
String destination = accessor.getDestination();
|
||||
String username = authentication.getName();
|
||||
|
||||
log.debug("用户[{}]订阅主题: {}", username, destination);
|
||||
|
||||
// TODO: 这里可以实现订阅级别的权限控制
|
||||
// 例如:检查用户是否有权限订阅某个主题
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package com.youlai.boot.config;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
|
||||
import cn.binarywang.wx.miniapp.config.WxMaConfig;
|
||||
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 配置微信 appId 和 appSecret
|
||||
*
|
||||
* @author wangtao
|
||||
* @since 2024/11/26 17:28
|
||||
*/
|
||||
@Setter
|
||||
@ConfigurationProperties(prefix = "wx.miniapp")
|
||||
@Configuration
|
||||
public class WxMiniAppConfig {
|
||||
|
||||
private String appId;
|
||||
|
||||
private String appSecret;
|
||||
|
||||
@Bean
|
||||
public WxMaConfig wxMaConfig() {
|
||||
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
|
||||
config.setAppid(appId);
|
||||
config.setSecret(appSecret);
|
||||
return config;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WxMaService wxMaService(WxMaConfig wxMaConfig) {
|
||||
WxMaService service = new WxMaServiceImpl();
|
||||
service.setWxMaConfig(wxMaConfig);
|
||||
return service;
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
package com.youlai.boot.core.aspect;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.date.TimeInterval;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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.aliyun.oss.HttpMethod;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.common.util.IPUtils;
|
||||
import com.youlai.boot.security.util.SecurityUtils;
|
||||
import com.youlai.boot.system.model.entity.Log;
|
||||
import com.youlai.boot.system.service.LogService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.*;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 日志切面
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2024/6/25
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LogAspect {
|
||||
private final LogService logService;
|
||||
private final HttpServletRequest request;
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
/**
|
||||
* 切点
|
||||
*/
|
||||
@Pointcut("@annotation(com.youlai.boot.common.annotation.Log)")
|
||||
public void logPointcut() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理完请求后执行
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
*/
|
||||
@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;
|
||||
|
||||
try {
|
||||
result = joinPoint.proceed();
|
||||
} catch (Exception e) {
|
||||
exception = e;
|
||||
throw e;
|
||||
} finally {
|
||||
long executionTime = timer.interval(); // 执行时长
|
||||
this.saveLog(joinPoint, exception, result, logAnnotation, executionTime, userId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 保存日志
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
* @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, Long userId) {
|
||||
String requestURI = request.getRequestURI();
|
||||
// 创建日志记录
|
||||
Log log = new Log();
|
||||
log.setExecutionTime(executionTime);
|
||||
if (logAnnotation == null && e != null) {
|
||||
log.setModule(LogModuleEnum.EXCEPTION);
|
||||
log.setContent("系统发生异常");
|
||||
this.setRequestParameters(joinPoint, log);
|
||||
log.setResponseContent(JSONUtil.toJsonStr(e.getStackTrace()));
|
||||
} else {
|
||||
log.setModule(logAnnotation.module());
|
||||
log.setContent(logAnnotation.value());
|
||||
// 请求参数
|
||||
if (logAnnotation.params()) {
|
||||
this.setRequestParameters(joinPoint, log);
|
||||
}
|
||||
// 响应结果
|
||||
if (logAnnotation.result() && jsonResult != null) {
|
||||
log.setResponseContent(JSONUtil.toJsonStr(jsonResult));
|
||||
}
|
||||
}
|
||||
log.setRequestUri(requestURI);
|
||||
log.setCreateBy(userId);
|
||||
String ipAddr = IPUtils.getIpAddr(request);
|
||||
if (StrUtil.isNotBlank(ipAddr)) {
|
||||
log.setIp(ipAddr);
|
||||
String region = IPUtils.getRegion(ipAddr);
|
||||
// 中国|0|四川省|成都市|电信 解析省和市
|
||||
if (StrUtil.isNotBlank(region)) {
|
||||
String[] regionArray = region.split("\\|");
|
||||
if (regionArray.length > 2) {
|
||||
log.setProvince(regionArray[2]);
|
||||
log.setCity(regionArray[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 获取浏览器和终端系统信息
|
||||
String userAgentString = request.getHeader("User-Agent");
|
||||
UserAgent userAgent = resolveUserAgent(userAgentString);
|
||||
if (Objects.nonNull(userAgent)) {
|
||||
// 系统信息
|
||||
log.setOs(userAgent.getOs().getName());
|
||||
// 浏览器信息
|
||||
log.setBrowser(userAgent.getBrowser().getName());
|
||||
log.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString));
|
||||
}
|
||||
//获取方法名
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
log.setMethod(methodName);
|
||||
// 保存日志到数据库
|
||||
logService.save(log);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求参数到日志对象中
|
||||
*
|
||||
* @param joinPoint 切点
|
||||
* @param log 操作日志
|
||||
*/
|
||||
private void setRequestParameters(JoinPoint joinPoint, Log log) {
|
||||
String requestMethod = request.getMethod();
|
||||
log.setRequestMethod(requestMethod);
|
||||
if (HttpMethod.GET.name().equalsIgnoreCase(requestMethod) || HttpMethod.PUT.name().equalsIgnoreCase(requestMethod) || HttpMethod.POST.name().equalsIgnoreCase(requestMethod)) {
|
||||
String params = convertArgumentsToString(joinPoint.getArgs());
|
||||
log.setRequestParams(StrUtil.sub(params, 0, 65535));
|
||||
} else {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
Map<?, ?> paramsMap = (Map<?, ?>) attributes.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
|
||||
log.setRequestParams(StrUtil.sub(paramsMap.toString(), 0, 65535));
|
||||
} else {
|
||||
log.setRequestParams("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将参数数组转换为字符串
|
||||
*
|
||||
* @param paramsArray 参数数组
|
||||
* @return 参数字符串
|
||||
*/
|
||||
private String convertArgumentsToString(Object[] paramsArray) {
|
||||
StringBuilder params = new StringBuilder();
|
||||
if (paramsArray != null) {
|
||||
for (Object param : paramsArray) {
|
||||
if (!shouldFilterObject(param)) {
|
||||
// 如果是基本类型或者枚举类型,直接添加到参数字符串中
|
||||
if(param.getClass().isPrimitive() || param.getClass().isEnum()) {
|
||||
params.append(param).append(" ");
|
||||
} else {
|
||||
params.append(JSONUtil.toJsonStr(param)).append(" ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return params.toString().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否需要过滤的对象。
|
||||
*
|
||||
* @param obj 对象信息。
|
||||
* @return 如果是需要过滤的对象,则返回true;否则返回false。
|
||||
*/
|
||||
private boolean shouldFilterObject(Object obj) {
|
||||
Class<?> clazz = obj.getClass();
|
||||
if (clazz.isArray()) {
|
||||
return MultipartFile.class.isAssignableFrom(clazz.getComponentType());
|
||||
} else if (Collection.class.isAssignableFrom(clazz)) {
|
||||
Collection<?> collection = (Collection<?>) obj;
|
||||
return collection.stream().anyMatch(item -> item instanceof MultipartFile);
|
||||
} else if (Map.class.isAssignableFrom(clazz)) {
|
||||
Map<?, ?> map = (Map<?, ?>) obj;
|
||||
return map.values().stream().anyMatch(value -> value instanceof MultipartFile);
|
||||
}
|
||||
return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析UserAgent
|
||||
*
|
||||
* @param userAgentString UserAgent字符串
|
||||
* @return UserAgent
|
||||
*/
|
||||
public UserAgent resolveUserAgent(String userAgentString) {
|
||||
if (StrUtil.isBlank(userAgentString)) {
|
||||
return null;
|
||||
}
|
||||
// 给userAgentStringMD5加密一次防止过长
|
||||
String userAgentStringMD5 = DigestUtil.md5Hex(userAgentString);
|
||||
//判断是否命中缓存
|
||||
UserAgent userAgent = Objects.requireNonNull(cacheManager.getCache("userAgent")).get(userAgentStringMD5, UserAgent.class);
|
||||
if (userAgent != null) {
|
||||
return userAgent;
|
||||
}
|
||||
userAgent = UserAgentUtil.parse(userAgentString);
|
||||
Objects.requireNonNull(cacheManager.getCache("userAgent")).put(userAgentStringMD5, userAgent);
|
||||
return userAgent;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.youlai.boot.platform.file.controller;
|
||||
package com.youlai.boot.file.controller;
|
||||
|
||||
import com.youlai.boot.core.web.Result;
|
||||
import com.youlai.boot.platform.file.service.FileService;
|
||||
import com.youlai.boot.platform.file.model.FileInfo;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.file.service.FileService;
|
||||
import com.youlai.boot.file.model.FileInfo;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@@ -19,7 +19,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
* @author Ray.Hao
|
||||
* @since 2022/10/16
|
||||
*/
|
||||
@Tag(name = "07.文件接口")
|
||||
@Tag(name = "10.文件接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/files")
|
||||
@RequiredArgsConstructor
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.platform.file.model;
|
||||
package com.youlai.boot.file.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.youlai.boot.platform.file.service;
|
||||
package com.youlai.boot.file.service;
|
||||
|
||||
import com.youlai.boot.platform.file.model.FileInfo;
|
||||
import com.youlai.boot.file.model.FileInfo;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.platform.file.service.impl;
|
||||
package com.youlai.boot.file.service.impl;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
@@ -8,8 +8,8 @@ import com.aliyun.oss.OSS;
|
||||
import com.aliyun.oss.OSSClientBuilder;
|
||||
import com.aliyun.oss.model.ObjectMetadata;
|
||||
import com.aliyun.oss.model.PutObjectRequest;
|
||||
import com.youlai.boot.platform.file.service.FileService;
|
||||
import com.youlai.boot.platform.file.model.FileInfo;
|
||||
import com.youlai.boot.file.service.FileService;
|
||||
import com.youlai.boot.file.model.FileInfo;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.youlai.boot.platform.file.service.impl;
|
||||
package com.youlai.boot.file.service.impl;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.youlai.boot.platform.file.model.FileInfo;
|
||||
import com.youlai.boot.platform.file.service.FileService;
|
||||
import com.youlai.boot.file.model.FileInfo;
|
||||
import com.youlai.boot.file.service.FileService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.youlai.boot.platform.file.service.impl;
|
||||
package com.youlai.boot.file.service.impl;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.core.exception.BusinessException;
|
||||
import com.youlai.boot.core.web.ResultCode;
|
||||
import com.youlai.boot.platform.file.model.FileInfo;
|
||||
import com.youlai.boot.platform.file.service.FileService;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.file.model.FileInfo;
|
||||
import com.youlai.boot.file.service.FileService;
|
||||
import io.minio.*;
|
||||
import io.minio.http.Method;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.plugin.knife4j;
|
||||
package com.youlai.boot.framework.apidoc;
|
||||
|
||||
|
||||
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.config;
|
||||
package com.youlai.boot.framework.apidoc;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.youlai.boot.config.property.SecurityProperties;
|
||||
import com.youlai.boot.framework.security.config.SecurityProperties;
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.config;
|
||||
package com.youlai.boot.framework.cache;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.youlai.boot.config;
|
||||
package com.youlai.boot.framework.cache;
|
||||
|
||||
import org.springframework.boot.autoconfigure.cache.CacheProperties;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.cache.autoconfigure.CacheProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -1,9 +1,13 @@
|
||||
package com.youlai.boot.config;
|
||||
package com.youlai.boot.framework.cache;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
|
||||
/**
|
||||
@@ -29,11 +33,21 @@ public class RedisConfig {
|
||||
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setConnectionFactory(redisConnectionFactory);
|
||||
|
||||
// Key 使用 String 序列化
|
||||
redisTemplate.setKeySerializer(RedisSerializer.string());
|
||||
redisTemplate.setValueSerializer(RedisSerializer.json());
|
||||
|
||||
redisTemplate.setHashKeySerializer(RedisSerializer.string());
|
||||
redisTemplate.setHashValueSerializer(RedisSerializer.json());
|
||||
|
||||
// Value 使用自定义 JSON 序列化(不写入类型信息,避免 HashSet 等集合被序列化成带 @class 的结构)
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
// 禁用类型信息写入,避免集合类型名被当成元素
|
||||
objectMapper.disableDefaultTyping();
|
||||
|
||||
Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
|
||||
|
||||
redisTemplate.setValueSerializer(jsonSerializer);
|
||||
redisTemplate.setHashValueSerializer(jsonSerializer);
|
||||
|
||||
redisTemplate.afterPropertiesSet();
|
||||
return redisTemplate;
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.youlai.boot.config;
|
||||
package com.youlai.boot.framework.captcha.config;
|
||||
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.captcha.generator.MathGenerator;
|
||||
import cn.hutool.captcha.generator.RandomGenerator;
|
||||
import com.youlai.boot.config.property.CaptchaProperties;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.config.property;
|
||||
package com.youlai.boot.framework.captcha.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -7,7 +7,7 @@ import org.springframework.stereotype.Component;
|
||||
/**
|
||||
* 验证码 属性配置
|
||||
*
|
||||
* @author haoxr
|
||||
* @author Ray.Hao
|
||||
* @since 2023/11/24
|
||||
*/
|
||||
@Component
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.youlai.boot.framework.captcha.exception;
|
||||
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 验证码异常
|
||||
*/
|
||||
@Getter
|
||||
public class CaptchaException extends RuntimeException {
|
||||
|
||||
private final ResultCode resultCode;
|
||||
|
||||
public CaptchaException(ResultCode resultCode) {
|
||||
super(resultCode.getMsg());
|
||||
this.resultCode = resultCode;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
package com.youlai.boot.auth.model.vo;
|
||||
package com.youlai.boot.framework.captcha.model;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 验证码信息
|
||||
*
|
||||
* @author Ray。Hao
|
||||
* @since 2023/03/24
|
||||
*/
|
||||
@Schema(description = "验证码信息")
|
||||
@Data
|
||||
@Builder
|
||||
public class CaptchaVO {
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "验证码信息")
|
||||
public class CaptchaInfo {
|
||||
|
||||
@Schema(description = "验证码缓存ID")
|
||||
private String captchaId;
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.youlai.boot.framework.captcha.service;
|
||||
|
||||
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.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.enums.CaptchaTypeEnum;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.framework.captcha.config.CaptchaProperties;
|
||||
import com.youlai.boot.framework.captcha.exception.CaptchaException;
|
||||
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.awt.Font;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 验证码服务
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class CaptchaService {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final CaptchaProperties captchaProperties;
|
||||
private final CodeGenerator codeGenerator;
|
||||
private final Font captchaFont;
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*/
|
||||
public CaptchaInfo generate() {
|
||||
String captchaType = captchaProperties.getType();
|
||||
int width = captchaProperties.getWidth();
|
||||
int height = captchaProperties.getHeight();
|
||||
int interfereCount = captchaProperties.getInterfereCount();
|
||||
int codeLength = captchaProperties.getCode().getLength();
|
||||
|
||||
AbstractCaptcha captcha;
|
||||
if (CaptchaTypeEnum.CIRCLE.name().equalsIgnoreCase(captchaType)) {
|
||||
captcha = CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount);
|
||||
} else if (CaptchaTypeEnum.GIF.name().equalsIgnoreCase(captchaType)) {
|
||||
captcha = CaptchaUtil.createGifCaptcha(width, height, codeLength);
|
||||
} else if (CaptchaTypeEnum.LINE.name().equalsIgnoreCase(captchaType)) {
|
||||
captcha = CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount);
|
||||
} else if (CaptchaTypeEnum.SHEAR.name().equalsIgnoreCase(captchaType)) {
|
||||
captcha = CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid captcha type: " + captchaType);
|
||||
}
|
||||
|
||||
captcha.setGenerator(codeGenerator);
|
||||
captcha.setTextAlpha(captchaProperties.getTextAlpha());
|
||||
captcha.setFont(captchaFont);
|
||||
|
||||
String captchaCode = captcha.getCode();
|
||||
String imageBase64Data = captcha.getImageBase64Data();
|
||||
|
||||
String captchaId = IdUtil.fastSimpleUUID();
|
||||
redisTemplate.opsForValue().set(
|
||||
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId),
|
||||
captchaCode,
|
||||
captchaProperties.getExpireSeconds(),
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
|
||||
return CaptchaInfo.builder()
|
||||
.captchaId(captchaId)
|
||||
.captchaBase64(imageBase64Data)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验验证码,失败抛异常
|
||||
*
|
||||
* @param captchaId 验证码ID
|
||||
* @param captchaCode 用户输入的验证码
|
||||
* @throws CaptchaException 验证码错误或过期
|
||||
*/
|
||||
public void validate(String captchaId, String captchaCode) {
|
||||
if (StrUtil.isBlank(captchaId) || StrUtil.isBlank(captchaCode)) {
|
||||
throw new CaptchaException(ResultCode.USER_VERIFICATION_CODE_ERROR);
|
||||
}
|
||||
|
||||
String cacheKey = StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId);
|
||||
String cachedCode = (String) redisTemplate.opsForValue().get(cacheKey);
|
||||
if (cachedCode == null) {
|
||||
throw new CaptchaException(ResultCode.USER_VERIFICATION_CODE_EXPIRED);
|
||||
}
|
||||
|
||||
if (!codeGenerator.verify(cachedCode, captchaCode)) {
|
||||
throw new CaptchaException(ResultCode.USER_VERIFICATION_CODE_ERROR);
|
||||
}
|
||||
|
||||
redisTemplate.delete(cacheKey);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.youlai.boot.config;
|
||||
package com.youlai.boot.framework.integration.mail.config;
|
||||
|
||||
import com.youlai.boot.config.property.MailProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.config.property;
|
||||
package com.youlai.boot.framework.integration.mail.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.youlai.boot.platform.mail.service.impl;
|
||||
package com.youlai.boot.framework.integration.mail.service;
|
||||
|
||||
import com.youlai.boot.config.property.MailProperties;
|
||||
import com.youlai.boot.platform.mail.service.MailService;
|
||||
import com.youlai.boot.framework.integration.mail.config.MailProperties;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -15,15 +14,15 @@ import org.springframework.stereotype.Service;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 邮件服务实现类
|
||||
* 邮件服务
|
||||
*
|
||||
* @author Ray
|
||||
* @author Ray.Hao
|
||||
* @since 2024/8/17
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class MailServiceImpl implements MailService {
|
||||
public class MailService {
|
||||
|
||||
private final JavaMailSender mailSender;
|
||||
|
||||
@@ -36,7 +35,6 @@ public class MailServiceImpl implements MailService {
|
||||
* @param subject 邮件主题
|
||||
* @param text 邮件内容
|
||||
*/
|
||||
@Override
|
||||
public void sendMail(String to, String subject, String text) {
|
||||
try {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
@@ -58,7 +56,6 @@ public class MailServiceImpl implements MailService {
|
||||
* @param text 邮件内容
|
||||
* @param filePath 附件路径
|
||||
*/
|
||||
@Override
|
||||
public void sendMailWithAttachment(String to, String subject, String text, String filePath) {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
try {
|
||||
@@ -73,7 +70,7 @@ public class MailServiceImpl implements MailService {
|
||||
|
||||
mailSender.send(message);
|
||||
} catch (MessagingException e) {
|
||||
log.error("发送邮件失败{}", e.getMessage());
|
||||
log.error("发送带附件的邮件失败{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.config.property;
|
||||
package com.youlai.boot.framework.integration.sms.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.platform.sms.enums;
|
||||
package com.youlai.boot.framework.integration.sms.enums;
|
||||
|
||||
import com.youlai.boot.common.base.IBaseEnum;
|
||||
import lombok.Getter;
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.youlai.boot.platform.sms.service;
|
||||
package com.youlai.boot.framework.integration.sms.service;
|
||||
|
||||
import com.youlai.boot.platform.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.platform.sms.service.impl;
|
||||
package com.youlai.boot.framework.integration.sms.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.aliyuncs.CommonRequest;
|
||||
@@ -8,9 +8,9 @@ import com.aliyuncs.IAcsClient;
|
||||
import com.aliyuncs.exceptions.ClientException;
|
||||
import com.aliyuncs.http.MethodType;
|
||||
import com.aliyuncs.profile.DefaultProfile;
|
||||
import com.youlai.boot.config.property.AliyunSmsProperties;
|
||||
import com.youlai.boot.platform.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.platform.sms.service.SmsService;
|
||||
import com.youlai.boot.framework.integration.sms.config.AliyunSmsProperties;
|
||||
import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.framework.integration.sms.service.SmsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.youlai.boot.framework.integration.wxma;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl;
|
||||
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 微信小程序配置
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2024/01/01
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(WxMaProperties.class)
|
||||
public class WxMaConfig {
|
||||
|
||||
/**
|
||||
* 微信小程序服务
|
||||
*
|
||||
* @param properties 微信小程序配置属性
|
||||
* @return {@link WxMaService}
|
||||
*/
|
||||
@Bean
|
||||
public WxMaService wxMaService(WxMaProperties properties) {
|
||||
WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
|
||||
config.setAppid(properties.getAppid());
|
||||
config.setSecret(properties.getSecret());
|
||||
|
||||
WxMaService service = new WxMaServiceImpl();
|
||||
service.setWxMaConfig(config);
|
||||
return service;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.youlai.boot.framework.integration.wxma;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 微信小程序配置属性
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "wx.miniapp")
|
||||
public class WxMaProperties {
|
||||
|
||||
/**
|
||||
* 小程序 AppID
|
||||
*/
|
||||
private String appid;
|
||||
|
||||
/**
|
||||
* 小程序 AppSecret
|
||||
*/
|
||||
private String secret;
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.config;
|
||||
package com.youlai.boot.framework.job;
|
||||
|
||||
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -1,15 +1,14 @@
|
||||
package com.youlai.boot.config;
|
||||
package com.youlai.boot.framework.mybatis.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
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.plugin.mybatis.MyDataPermissionHandler;
|
||||
import com.youlai.boot.plugin.mybatis.MyMetaObjectHandler;
|
||||
import com.youlai.boot.framework.mybatis.handler.MyMetaObjectHandler;
|
||||
import com.youlai.boot.framework.mybatis.interceptor.MyDataPermissionHandler;
|
||||
import org.apache.ibatis.mapping.DatabaseIdProvider;
|
||||
import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.youlai.boot.plugin.mybatis;
|
||||
package com.youlai.boot.framework.mybatis.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@@ -0,0 +1,267 @@
|
||||
package com.youlai.boot.framework.mybatis.interceptor;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringPool;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
|
||||
import com.youlai.boot.common.annotation.DataPermission;
|
||||
import com.youlai.boot.common.enums.DataScopeEnum;
|
||||
import com.youlai.boot.framework.security.model.RoleDataScope;
|
||||
import com.youlai.boot.framework.security.model.SysUserDetails;
|
||||
import com.youlai.boot.framework.security.util.SecurityUtils;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.expression.*;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||
import net.sf.jsqlparser.schema.Column;
|
||||
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据权限控制器
|
||||
* <p>
|
||||
* 支持多角色数据权限合并(并集策略):
|
||||
* - 如果任一角色是 ALL,则跳过数据权限过滤
|
||||
* - 否则用 OR 连接各角色的数据权限条件
|
||||
* <p>
|
||||
* 使用 JSQLParser 构建 SQL 条件,避免字符串拼接,提高代码安全性和可读性。
|
||||
*
|
||||
* @author zc
|
||||
* @since 2021-12-10 13:28
|
||||
*/
|
||||
@Slf4j
|
||||
public class MyDataPermissionHandler implements DataPermissionHandler {
|
||||
|
||||
private static final String DEPT_TABLE = "sys_dept";
|
||||
private static final String DEPT_ID_COLUMN = "id";
|
||||
private static final String DEPT_TREE_PATH_COLUMN = "tree_path";
|
||||
|
||||
/**
|
||||
* 获取数据权限的sql片段
|
||||
*
|
||||
* @param where 查询条件
|
||||
* @param mappedStatementId mapper接口方法的全路径
|
||||
* @return sql片段
|
||||
*/
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public Expression getSqlSegment(Expression where, String mappedStatementId) {
|
||||
// 如果是未登录,或者是定时任务执行的SQL,或者是超级管理员,直接返回
|
||||
if (SecurityUtils.getUserId() == null || SecurityUtils.isRoot()) {
|
||||
return where;
|
||||
}
|
||||
|
||||
// 获取当前用户的数据权限列表
|
||||
List<RoleDataScope> dataScopes = SecurityUtils.getUser()
|
||||
.map(SysUserDetails::getDataScopes)
|
||||
.orElse(List.of());
|
||||
|
||||
// 如果任一角色是 ALL,则跳过数据权限过滤(并集策略)
|
||||
if (hasAllDataScope(dataScopes)) {
|
||||
return where;
|
||||
}
|
||||
|
||||
// 如果没有数据权限,跳过过滤
|
||||
if (CollectionUtil.isEmpty(dataScopes)) {
|
||||
return where;
|
||||
}
|
||||
|
||||
// 获取当前执行的接口类
|
||||
Class<?> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT)));
|
||||
// 获取当前执行的方法名称
|
||||
String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1);
|
||||
// 获取当前执行的接口类里所有的方法
|
||||
Method[] methods = clazz.getDeclaredMethods();
|
||||
for (Method method : methods) {
|
||||
// 找到当前执行的方法
|
||||
if (method.getName().equals(methodName)) {
|
||||
DataPermission annotation = method.getAnnotation(DataPermission.class);
|
||||
// 判断当前执行的方法是否有权限注解,如果没有注解直接返回
|
||||
if (annotation == null) {
|
||||
return where;
|
||||
}
|
||||
// 使用并集策略过滤
|
||||
return dataScopeFilterWithUnion(mappedStatementId, annotation, dataScopes, where);
|
||||
}
|
||||
}
|
||||
return where;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否包含"全部数据"权限
|
||||
*
|
||||
* @param dataScopes 数据权限列表
|
||||
* @return 是否有全部数据权限
|
||||
*/
|
||||
private boolean hasAllDataScope(List<RoleDataScope> dataScopes) {
|
||||
if (CollectionUtil.isEmpty(dataScopes)) {
|
||||
return false;
|
||||
}
|
||||
return dataScopes.stream()
|
||||
.anyMatch(scope -> DataScopeEnum.ALL.getValue().equals(scope.getDataScope()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用并集策略进行数据权限过滤
|
||||
* <p>
|
||||
* 多个角色的数据权限通过 OR 连接,实现并集效果
|
||||
*
|
||||
* @param annotation 数据权限注解
|
||||
* @param dataScopes 数据权限列表
|
||||
* @param where 原始查询条件
|
||||
* @return 追加权限过滤后的查询条件
|
||||
*/
|
||||
@SneakyThrows
|
||||
private Expression dataScopeFilterWithUnion(String mappedStatementId, DataPermission annotation, List<RoleDataScope> dataScopes, Expression where) {
|
||||
String deptAlias = annotation.deptAlias();
|
||||
String deptIdColumnName = annotation.deptIdColumnName();
|
||||
String userAlias = annotation.userAlias();
|
||||
String userIdColumnName = annotation.userIdColumnName();
|
||||
|
||||
// 构建各角色的数据权限条件,使用 OR 连接实现并集
|
||||
Expression unionExpression = null;
|
||||
for (RoleDataScope dataScope : dataScopes) {
|
||||
Expression roleExpression = buildRoleDataScopeExpression(
|
||||
deptAlias, deptIdColumnName, userAlias, userIdColumnName, dataScope);
|
||||
if (roleExpression != null) {
|
||||
if (unionExpression == null) {
|
||||
unionExpression = roleExpression;
|
||||
} else {
|
||||
// 使用 OR 连接各角色的条件(并集)
|
||||
unionExpression = new OrExpression(unionExpression, roleExpression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unionExpression == null) {
|
||||
return where;
|
||||
}
|
||||
|
||||
// 用括号包裹并集条件
|
||||
Expression finalExpression = CCJSqlParserUtil.parseCondExpression("(" + unionExpression + ")");
|
||||
|
||||
if (where == null) {
|
||||
log.debug("DataPermission applied. mappedStatementId={}, segment={}", mappedStatementId, finalExpression);
|
||||
return finalExpression;
|
||||
}
|
||||
|
||||
Expression combined = new AndExpression(where, finalExpression);
|
||||
log.debug("DataPermission applied. mappedStatementId={}, originWhere={}, segment={}, combined={}",
|
||||
mappedStatementId, where, finalExpression, combined);
|
||||
return combined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建单个角色的数据权限SQL条件
|
||||
* <p>
|
||||
* 使用 JSQLParser 构建 Expression,避免字符串拼接
|
||||
*
|
||||
* @param deptAlias 部门表别名
|
||||
* @param deptIdColumnName 部门ID字段名
|
||||
* @param userAlias 用户表别名
|
||||
* @param userIdColumnName 用户ID字段名
|
||||
* @param roleDataScope 角色数据权限
|
||||
* @return 数据权限条件表达式
|
||||
*/
|
||||
private Expression buildRoleDataScopeExpression(String deptAlias, String deptIdColumnName,
|
||||
String userAlias, String userIdColumnName,
|
||||
RoleDataScope roleDataScope) {
|
||||
Column deptColumn = buildColumn(deptAlias, deptIdColumnName);
|
||||
Column userColumn = buildColumn(userAlias, userIdColumnName);
|
||||
|
||||
Long deptId = SecurityUtils.getDeptId();
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
|
||||
DataScopeEnum dataScopeEnum = DataScopeEnum.getByValue(roleDataScope.getDataScope());
|
||||
if (dataScopeEnum == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (dataScopeEnum) {
|
||||
case ALL -> null; // 全部数据权限,不添加过滤条件
|
||||
case DEPT_AND_SUB -> buildDeptAndSubExpression(deptColumn, deptId);
|
||||
case DEPT -> buildEqualsExpression(deptColumn, deptId);
|
||||
case SELF -> buildEqualsExpression(userColumn, userId);
|
||||
case CUSTOM -> buildCustomDeptExpression(deptColumn, roleDataScope.getCustomDeptIds());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建列引用
|
||||
*
|
||||
* @param alias 表别名
|
||||
* @param columnName 列名
|
||||
* @return 列引用
|
||||
*/
|
||||
private Column buildColumn(String alias, String columnName) {
|
||||
if (StrUtil.isNotBlank(alias)) {
|
||||
return new Column(alias + StringPool.DOT + columnName);
|
||||
}
|
||||
return new Column(columnName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建等于条件
|
||||
*
|
||||
* @param column 列
|
||||
* @param value 值
|
||||
* @return 等于表达式
|
||||
*/
|
||||
private Expression buildEqualsExpression(Column column, Long value) {
|
||||
EqualsTo equalsTo = new EqualsTo();
|
||||
equalsTo.setLeftExpression(column);
|
||||
equalsTo.setRightExpression(new LongValue(value));
|
||||
return equalsTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建部门及子部门数据权限条件
|
||||
* <p>
|
||||
* SQL: dept_id IN (SELECT id FROM sys_dept WHERE id = ? OR FIND_IN_SET(?, tree_path))
|
||||
*
|
||||
* @param deptColumn 部门列
|
||||
* @param deptId 部门ID
|
||||
* @return IN 子查询表达式
|
||||
*/
|
||||
@SneakyThrows
|
||||
private Expression buildDeptAndSubExpression(Column deptColumn, Long deptId) {
|
||||
// 使用字符串解析,避免不同 JSqlParser 版本下 InExpression/ItemsList 渲染差异导致 SQL 语法错误
|
||||
// SQL: dept_id IN (SELECT id FROM sys_dept WHERE id = ? OR FIND_IN_SET(?, tree_path))
|
||||
String columnName = deptColumn.toString();
|
||||
String sql = columnName + " IN (SELECT " + DEPT_ID_COLUMN + " FROM " + DEPT_TABLE +
|
||||
" WHERE " + DEPT_ID_COLUMN + " = " + deptId +
|
||||
" OR FIND_IN_SET(" + deptId + ", " + DEPT_TREE_PATH_COLUMN + "))";
|
||||
return CCJSqlParserUtil.parseCondExpression(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建自定义部门数据权限条件
|
||||
* <p>
|
||||
* SQL: dept_id IN (?, ?, ...)
|
||||
*
|
||||
* @param deptColumn 部门列
|
||||
* @param customDeptIds 自定义部门ID列表
|
||||
* @return IN 表达式,如果没有部门则返回 1=0
|
||||
*/
|
||||
@SneakyThrows
|
||||
private Expression buildCustomDeptExpression(Column deptColumn, List<Long> customDeptIds) {
|
||||
if (CollectionUtil.isEmpty(customDeptIds)) {
|
||||
// 没有自定义部门,返回 1=0(无权限)
|
||||
EqualsTo falseCondition = new EqualsTo();
|
||||
falseCondition.setLeftExpression(new LongValue(1));
|
||||
falseCondition.setRightExpression(new LongValue(0));
|
||||
return falseCondition;
|
||||
}
|
||||
|
||||
// 使用字符串解析,确保渲染始终为 IN (..)
|
||||
String columnName = deptColumn.toString();
|
||||
String ids = customDeptIds.stream().map(String::valueOf).reduce((a, b) -> a + "," + b).orElse("");
|
||||
String sql = columnName + " IN (" + ids + ")";
|
||||
return CCJSqlParserUtil.parseCondExpression(sql);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.boot.config;
|
||||
package com.youlai.boot.framework.security.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -1,19 +1,17 @@
|
||||
package com.youlai.boot.config;
|
||||
package com.youlai.boot.framework.security.config;
|
||||
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import com.youlai.boot.framework.captcha.service.CaptchaService;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.youlai.boot.config.property.SecurityProperties;
|
||||
import com.youlai.boot.core.filter.RateLimiterFilter;
|
||||
import com.youlai.boot.security.filter.CaptchaValidationFilter;
|
||||
import com.youlai.boot.security.filter.TokenAuthenticationFilter;
|
||||
import com.youlai.boot.security.handler.MyAccessDeniedHandler;
|
||||
import com.youlai.boot.security.handler.MyAuthenticationEntryPoint;
|
||||
import com.youlai.boot.security.provider.SmsAuthenticationProvider;
|
||||
import com.youlai.boot.security.provider.WxMiniAppCodeAuthenticationProvider;
|
||||
import com.youlai.boot.security.provider.WxMiniAppPhoneAuthenticationProvider;
|
||||
import com.youlai.boot.security.token.TokenManager;
|
||||
import com.youlai.boot.security.service.SysUserDetailsService;
|
||||
import com.youlai.boot.framework.web.filter.RateLimiterFilter;
|
||||
import com.youlai.boot.framework.security.filter.CaptchaValidationFilter;
|
||||
import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter;
|
||||
import com.youlai.boot.framework.security.handler.MyAccessDeniedHandler;
|
||||
import com.youlai.boot.framework.security.handler.MyAuthenticationEntryPoint;
|
||||
import com.youlai.boot.framework.security.provider.SmsAuthenticationProvider;
|
||||
import com.youlai.boot.framework.security.provider.WxMaAuthenticationProvider;
|
||||
import com.youlai.boot.framework.security.token.TokenManager;
|
||||
import com.youlai.boot.framework.security.service.SysUserDetailsService;
|
||||
import com.youlai.boot.system.service.ConfigService;
|
||||
import com.youlai.boot.system.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -50,11 +48,10 @@ public class SecurityConfig {
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
private final TokenManager tokenManager;
|
||||
private final WxMaService wxMaService;
|
||||
private final UserService userService;
|
||||
private final SysUserDetailsService userDetailsService;
|
||||
|
||||
private final CodeGenerator codeGenerator;
|
||||
private final CaptchaService captchaService;
|
||||
private final ConfigService configService;
|
||||
private final SecurityProperties securityProperties;
|
||||
|
||||
@@ -63,10 +60,9 @@ public class SecurityConfig {
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
|
||||
return http
|
||||
.authorizeHttpRequests(requestMatcherRegistry -> {
|
||||
// 配置无需登录即可访问的公开接口
|
||||
// 配置无需登录即可访问的公开接口(配置文件方式)
|
||||
String[] ignoreUrls = securityProperties.getIgnoreUrls();
|
||||
if (ArrayUtil.isNotEmpty(ignoreUrls)) {
|
||||
requestMatcherRegistry.requestMatchers(ignoreUrls).permitAll();
|
||||
@@ -93,7 +89,7 @@ public class SecurityConfig {
|
||||
// 限流过滤器
|
||||
.addFilterBefore(new RateLimiterFilter(redisTemplate, configService), UsernamePasswordAuthenticationFilter.class)
|
||||
// 验证码校验过滤器
|
||||
.addFilterBefore(new CaptchaValidationFilter(redisTemplate, codeGenerator), UsernamePasswordAuthenticationFilter.class)
|
||||
.addFilterBefore(new CaptchaValidationFilter(captchaService), UsernamePasswordAuthenticationFilter.class)
|
||||
// 验证和解析过滤器
|
||||
.addFilterBefore(new TokenAuthenticationFilter(tokenManager), UsernamePasswordAuthenticationFilter.class)
|
||||
.build();
|
||||
@@ -124,22 +120,6 @@ public class SecurityConfig {
|
||||
return daoAuthenticationProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序Code认证Provider
|
||||
*/
|
||||
@Bean
|
||||
public WxMiniAppCodeAuthenticationProvider wxMiniAppCodeAuthenticationProvider() {
|
||||
return new WxMiniAppCodeAuthenticationProvider(userService, wxMaService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序手机号认证Provider
|
||||
*/
|
||||
@Bean
|
||||
public WxMiniAppPhoneAuthenticationProvider wxMiniAppPhoneAuthenticationProvider() {
|
||||
return new WxMiniAppPhoneAuthenticationProvider(userService, wxMaService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 短信验证码认证 Provider
|
||||
*/
|
||||
@@ -148,21 +128,31 @@ public class SecurityConfig {
|
||||
return new SmsAuthenticationProvider(userService, redisTemplate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序认证 Provider
|
||||
*/
|
||||
@Bean
|
||||
public WxMaAuthenticationProvider wechatMiniAuthenticationProvider(
|
||||
WxMaService wxMaService,
|
||||
SysUserDetailsService sysUserDetailsService
|
||||
) {
|
||||
return new WxMaAuthenticationProvider(wxMaService, sysUserDetailsService);
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证管理器
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(
|
||||
DaoAuthenticationProvider daoAuthenticationProvider,
|
||||
WxMiniAppCodeAuthenticationProvider wxMiniAppCodeAuthenticationProvider,
|
||||
WxMiniAppPhoneAuthenticationProvider wxMiniAppPhoneAuthenticationProvider,
|
||||
SmsAuthenticationProvider smsAuthenticationProvider
|
||||
SmsAuthenticationProvider smsAuthenticationProvider,
|
||||
WxMaAuthenticationProvider wxMaAuthenticationProvider
|
||||
) {
|
||||
return new ProviderManager(
|
||||
daoAuthenticationProvider,
|
||||
wxMiniAppCodeAuthenticationProvider,
|
||||
wxMiniAppPhoneAuthenticationProvider,
|
||||
smsAuthenticationProvider
|
||||
smsAuthenticationProvider,
|
||||
wxMaAuthenticationProvider
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package com.youlai.boot.config.property;
|
||||
package com.youlai.boot.framework.security.config;
|
||||
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -53,7 +54,8 @@ public class SecurityProperties {
|
||||
* <li>redis-token - 基于Redis的有状态认证</li>
|
||||
* </ul>
|
||||
*/
|
||||
@NotNull
|
||||
@NotNull(message = "会话类型不能为空")
|
||||
@Pattern(regexp = "jwt|redis-token", message = "会话类型只能是 jwt 或 redis-token")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user