52 Commits

Author SHA1 Message Date
5c2bec0bcc Merge branch 'master' of ssh://gitea.ttstd.com:2222/tt/youlai-boot 2026-06-15 09:35:01 +08:00
4f03889412 Merge branch 'master' of gitee.com:youlaiorg/youlai-boot 2026-06-15 09:34:29 +08:00
31599928a9 feat: add upload file api 2026-06-14 13:06:38 +08:00
Ray
eb75bee40f !51 fix: 代码生成器报错 BUG
Merge pull request !51 from Qiuyun/master
2026-06-12 16:17:59 +00:00
Qiuyun
ce5d7686c2 fix: 代码生成器报错 BUG 2026-06-12 18:13:12 +08:00
Ray.Hao
a8403613fa token过期改为返回401;CaptchaValidationException重命名为SmsCaptchaException;v4.3.1 2026-06-09 15:53:35 +08:00
33fbee9a00 feat: 增加sn管理接口 2026-06-01 08:24:02 +08:00
Ray.Hao
06857f6c88 fix: codegen.yml VO 命名规范修正 2026-05-25 17:26:57 +08:00
Ray.Hao
d6ce39639e docs: 更新官方文档地址 2026-05-24 20:24:56 +08:00
Ray.Hao
1e789e0c46 Redis DB dev→0, prod→1 2026-05-23 15:12:48 +08:00
Ray.Hao
eebb287095 feat(exception): 添加数据权限异常及通用数据库执行异常处理 2026-05-23 10:30:44 +08:00
Ray.Hao
65333b9e8e refactor(redis): 适配 Spring Data Redis 4.0 序列化器变更
Jackson2JsonRedisSerializer 在 Spring Data Redis 4.0 中已标记为弃用并计划移除。
替换为 JacksonJsonRedisSerializer,对应 Jackson 3 的 API 变更:
2026-05-20 16:02:25 +08:00
Ray.Hao
481152ee1f chore: 更新项目 logo 2026-05-18 22:38:49 +08:00
Ray.Hao
13079f3d13 fix: 修复非超管用户路由参数(params)丢失问题 2026-05-14 16:56:06 +08:00
Ray.Hao
7a43e9c38c fix: 已知问题修复 2026-04-25 16:29:32 +08:00
Ray.Hao
b0422ea695 docs: 更新 README 项目介绍和徽章 2026-04-23 13:09:25 +08:00
Ray.Hao
c818cad89b docs: 更新项目简介与文档结构 2026-04-23 12:49:55 +08:00
Ray.Hao
eefcaf10c2 feat(codegen): 为生成模板添加ID字段并移除未使用导入 2026-04-23 12:49:46 +08:00
Ray.Hao
63c34a4218 refactor: 移除多数据库支持并简化配置 2026-04-22 13:36:46 +08:00
Ray.Hao
9993302d5c fix: 统一前后端权限标识后缀(add/edit/query→create/update/list) 2026-04-17 23:35:52 +08:00
Ray.Hao
c2a6edbcef docs: 更新README.md系统预览图片布局为双列显示 2026-04-12 23:14:22 +08:00
Ray.Hao
0e0fe9ca0f docs: 移除 README.md 中多余的横线 2026-04-12 23:06:51 +08:00
Ray.Hao
1fcdc90a2b docs:添加系统预览截图并移除技术栈图示 2026-04-12 23:06:17 +08:00
Ray.Hao
959c4dd6ec Merge branch 'master' of https://gitee.com/youlaiorg/youlai-boot 2026-04-12 20:55:58 +08:00
Ray.Hao
b5515bdda8 fix: 修复Token管理、代码生成模板及文档问题 2026-04-12 20:55:56 +08:00
Theo
533217aa10 ```
feat(dept): 添加部门编辑时父子关系验证逻辑

- 实现部门是否存在检查
- 添加上级部门不能为自己的校验
- 实现上级部门不能为当前子部门的递归校验
- 优化部门树路径查询逻辑
```
2026-04-12 17:33:01 +08:00
Ray.Hao
ed914a5562 Merge branch 'master' of https://gitee.com/youlaiorg/youlai-boot 2026-04-10 19:55:54 +08:00
Ray.Hao
3f6c7c177c wip: 临时提交 2026-04-10 19:55:51 +08:00
Theo
734f86d4de fix(codegen): 修正后端查询模板导入路径
- 将BaseQuery导入路径从${packageName}.common.base修改为com.youlai.boot.common.base
2026-04-10 18:59:27 +08:00
Ray.Hao
d2a37b48d6 Merge branch 'master' of https://gitee.com/youlaiorg/youlai-boot 2026-04-10 18:35:56 +08:00
Ray.Hao
f243a8d69e fix(codegen): 修正模板字符串转义语法 2026-04-10 18:35:48 +08:00
Theo
fa0ab86445 refactor(codegen): 更新后端控制器模板中的导入路径
- 将 PageResult 和 Result 的导入路径从 core.web 更新为 common.result
- 保持其他导入和注解功能不变
- 统一结果类的包结构到 common 模块下
2026-04-10 18:34:14 +08:00
Ray.Hao
c23abf1b96 refactor: 优化代码生成器前端API文件路径结构及日志统计重命名IP数为访客数(UV) 2026-04-05 20:23:53 +08:00
Ray.Hao
c6f9dbb182 refactor: 修改日志控制器路由,将访问趋势和访问统计概览的路径分别更改为 analytics/trend 和 analytics/overview 2026-04-04 17:43:45 +08:00
Ray.Hao
9927546b78 fix: SSE连接关闭时机提前至容器关闭最早阶段
添加@Order(Ordered.HIGHEST_PRECEDENCE)确保最高优先级执行,
在Tomcat关闭前主动断开所有SSE连接,避免阻塞应用停止
2026-04-03 10:14:11 +08:00
theo
b411baaddd refactor(message): 优化SSE会话注册表的容器关闭事件处理
- 移除@PreDestroy注解
- 添加ContextClosedEvent事件监听器
- 使用Spring事件机制替代JSR-250生命周期回调
- 提高容器关闭时SSE连接清理的可靠性
- 增强代码的Spring框架集成一致性
2026-04-03 09:49:33 +08:00
Ray.Hao
1635f976c3 Merge branch 'master' of https://gitee.com/youlaiorg/youlai-boot 2026-04-03 09:34:11 +08:00
Ray.Hao
18e46cb5f2 refactor: 统一operator_id为可空字段及安全白名单配置 2026-04-03 09:34:07 +08:00
theo
cf1bfa773e alter(table): 修改操作日志表操作人ID字段为可选
- 将 operator_id 字段从 NOT NULL 约束修改为可选字段
- 保持字段类型为 BIGINT 不变
- 更新字段注释说明操作人ID的含义
2026-04-03 09:16:58 +08:00
Ray.Hao
6f94cf9c84 fix: SSE连接阻塞应用关闭,添加@PreDestroy优雅断开所有连接 2026-04-02 23:35:37 +08:00
Ray.Hao
6e09aa273d Merge branch 'master' of https://gitee.com/youlaiorg/youlai-boot 2026-04-02 09:25:15 +08:00
Ray.Hao
c7c6799af4 refactor: 项目结构优化与代码生成模板修复 2026-04-02 09:24:24 +08:00
Ray.Hao
10eb81ccd1 feat: 重构微信小程序认证及优化代码生成模板
- 将微信小程序认证相关类重命名(WechatMini -> WxMa)
- 新增 WxMaAuthenticationToken 和 WxMaAuthenticationProvider
- 调整代码生成前端模板样式与导入路径
2026-03-30 07:48:38 +08:00
Ray.Hao
9cd3ff88f8 refactor: 重构目录结构统一规范
- shared/ → common/(constant、enums、model)
- framework/cache/config/ → framework/cache/(扁平化)
- framework/integration/captcha/ → framework/captcha/
- config/property/ → 各模块 config/ 下
- interfaces/ → module/(sse、mail、sms)
- 移除冗余枚举 LogModuleEnum
2026-03-28 09:00:35 +08:00
Ray.Hao
234b12f297 refactor: 移除Token参数支持,统一异常响应状态码为200或500 2026-03-24 15:23:03 +08:00
Ray.Hao
8f5c1fc8e4 refactor: 优化响应状态码映射,权限不足时返回403 Forbidden 2026-03-24 10:57:05 +08:00
Ray.Hao
c71becea68 fix: 移除数据权限单元测试 2026-03-24 09:28:10 +08:00
Ray.Hao
8188c82c3d feat: 重构项目结构并新增微信小程序认证模块 2026-03-24 07:52:05 +08:00
Ray.Hao
465e63c99d feat: WebSocket 迁移到 SSE 实现实时推送 2026-03-18 17:41:05 +08:00
Ray.Hao
ba6203424a build: 移除 Spring Boot Admin 依赖和配置 2026-03-16 14:33:20 +08:00
Ray.Hao
69f062d6b9 更新微信小程序配置为占位符 2026-03-16 08:10:11 +08:00
Ray.Hao
63c8cbc873 集成 Spring Boot Admin 监控服务端及客户端依赖,并新增 AdminServerConfig 配置类启用应用监控。 2026-03-16 08:09:03 +08:00
301 changed files with 7267 additions and 4123 deletions

View File

@@ -1,35 +0,0 @@
# 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
View File

@@ -1,14 +0,0 @@
# 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

View File

@@ -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脚本)

View File

@@ -20,4 +20,4 @@ CMD java \
-jar /app.jar
# 暴露端口
EXPOSE 8989
EXPOSE 8000

346
README.md
View File

@@ -1,183 +1,197 @@
<div align="center">
<img alt="logo" width="100" height="100" src="https://foruda.gitee.com/images/1733417239320800627/3c5290fe_716974.png">
<h2>youlai-boot</h2>
<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="Gitee" src="https://gitee.com/youlaiorg/youlai-boot/badge/star.svg"/>
</a>
<a href="https://github.com/haoxianrui/youlai-boot" target="_blank">
<img alt="GitHub" src="https://img.shields.io/github/stars/haoxianrui/youlai-boot.svg?style=social&label=Stars"/>
</a>
<a href="https://gitcode.com/youlai/youlai-boot" target="_blank">
<img alt="GitCode" src="https://gitcode.com/youlai/youlai-boot/star/badge.svg"/>
</a>
<img alt="youlai-boot" width="80" src="./docs/images/logo/logo.png">
# youlai-boot
**Spring Boot 4 企业级权限管理系统后端**
[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-4.0.5-6DB33F?logo=spring-boot)](https://spring.io/projects/spring-boot)
[![JDK](https://img.shields.io/badge/JDK-17%2B-007396?logo=openjdk)](https://openjdk.org/)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue?logo=apache)](LICENSE)
[![Gitee Star](https://gitee.com/youlaiorg/youlai-boot/badge/star.svg)](https://gitee.com/youlaiorg/youlai-boot/stargazers)
[![GitHub Star](https://img.shields.io/github/stars/youlaitech/youlai-boot?style=social)](https://github.com/youlaitech/youlai-boot)
[![GitCode Star](https://gitcode.com/youlai/youlai-boot/star/badge.svg)](https://gitcode.com/youlai/youlai-boot/stargazers)
</div>
![](https://foruda.gitee.com/images/1708618984641188532/a7cca095_716974.png "rainbow.png")
<div align="center">
[🖥️ 在线预览](https://vue.youlai.tech) | [📲 移动端预览](https://app.youlai.tech) | [📖 文档](https://www.youlai.tech/docs/server/spring-boot/)
</div>
## 简介
**youlai-boot** 是一套基于 Spring Boot 4 的企业级权限管理系统后端,配套前端 [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) 和移动端 [youlai-app](https://gitee.com/youlaiorg/youlai-app),并提供 **6 种语言实现**Java / Node.js / Go / Python / PHP / C#),共享同一套 API 规范与数据库结构。适用于企业中后台管理系统的学习参考与二次开发。
## 核心特性
- 🔐 **安全体系** — Spring Security + JWT/Redis Token 双会话模式、令牌续期、多端互斥
- 🛡️ **细粒度权限** — RBAC 五级:数据 → 菜单 → 按钮 → 接口 → 字段
-**代码生成器** — 一键生成前后端 CRUD 代码
- 📦 **模块齐全** — 用户、角色、菜单、部门、字典、文件、定时任务、消息中心、操作日志
- 🌐 **多租户 SaaS** — 数据隔离 + 租户配置,独立 [youlai-boot-tenant](https://gitee.com/youlaiorg/youlai-boot-tenant) 版本
- 🔌 **实时通信** — SSE 推送:在线用户数、字典同步、通知广播
## 技术架构
<p align="center">
<a target="_blank" href="https://vue.youlai.tech/">🖥️ 在线预览</a>
<span>&nbsp;|&nbsp;</span>
<a target="_blank" href="https://www.youlai.tech/youlai-boot">📑 官方文档</a>
<span>&nbsp;|&nbsp;</span>
<a target="_blank" href="https://www.youlai.tech">🌐 官网</a>
<img alt="youlai-boot 技术架构图" width="900" src="./docs/images/architecture/architecture.png">
</p>
---
## 系统预览
## 📢 项目简介
**PC 端**
基于 **JDK 17 + Spring Boot 4 + Spring Security** 构建的前后端分离权限管理系统,是 [**vue3-element-admin**](https://gitee.com/youlaiorg/vue3-element-admin) 的 Java 后端实现。
<table align="center">
<tr>
<td><img alt="PC预览1" width="400" src="./docs/images/preview/pc-01.png"></td>
<td><img alt="PC预览2" width="400" src="./docs/images/preview/pc-02.png"></td>
</tr>
<tr>
<td><img alt="PC预览3" width="400" src="./docs/images/preview/pc-03.png"></td>
<td><img alt="PC预览4" width="400" src="./docs/images/preview/pc-04.png"></td>
</tr>
<tr>
<td><img alt="PC预览3" width="400" src="./docs/images/preview/pc-05.png"></td>
<td><img alt="PC预览4" width="400" src="./docs/images/preview/pc-06.png"></td>
</tr>
</table>
### 为什么选择 youlai-boot
**移动端**
| 特性 | 说明 |
| ----------------- | ----------------------------------------------------- |
| 🚀 **最新技术栈** | Spring Boot 4 + JDK 17持续跟进最新版本 |
| 🔐 **企业级认证** | Spring Security + JWT + Redis支持令牌续期、多端互斥 |
| 🔑 **细粒度权限** | RBAC 模型,接口级 + 按钮级权限控制 |
| 🛠️ **开箱即用** | 用户、角色、菜单、部门、字典等核心模块 |
| 📦 **代码生成** | 内置代码生成器,快速构建 CRUD 功能 |
| 🌐 **完整生态** | Web 管理前端 + 移动端配套项目,多语言后端支持 |
<table align="center">
<tr>
<td><img alt="APP预览1" width="200" src="./docs/images/preview/app-01.png"></td>
<td><img alt="APP预览2" width="200" src="./docs/images/preview/app-02.png"></td>
<td><img alt="APP预览3" width="200" src="./docs/images/preview/app-03.png"></td>
<td><img alt="APP预览4" width="200" src="./docs/images/preview/app-04.png"></td>
</tr>
</table>
## 🌈 相关项目
## 快速开始
**环境要求**JDK 17+ · MySQL 8.0+ · Redis 6.0+
1. 克隆项目:`git clone https://gitee.com/youlaiorg/youlai-boot.git`
2. 导入数据库:`sql/youlai-admin.sql`
3. 修改配置(可选,默认已配置线上只读数据源):`src/main/resources/application-dev.yml`
4. 启动服务,访问 http://localhost:8000/doc.html
默认账号:`admin` / `123456`
**Docker 部署**`cd deploy/docker`,然后 `docker-compose up -d`
详细指南:[部署文档](https://www.youlai.tech/docs/server/spring-boot/deploy) · [开发规范](https://www.youlai.tech/docs/server/spring-boot/dev-standards)
## 技术栈
| 技术 | 版本 | 说明 |
|:-----|:-----|:-----|
| Spring Boot | 4.0.5 | 核心框架 |
| Spring Security | 6.x | 认证授权 |
| MyBatis-Plus | 3.5.15 | ORM 框架 |
| Druid | 1.2.24 | 数据库连接池 |
| Redis + Redisson | 6.0+ / 4.1.0 | 缓存 · 会话 · 分布式锁 |
| Caffeine | 2.9.3 | 本地缓存 |
| XXL-Job | 3.2.0 | 分布式定时任务 |
| Knife4j | 4.5.0 | API 文档 |
| MapStruct | 1.6.3 | 对象映射 |
| MinIO | 8.5.10 | 对象存储 |
## 目录结构
```
youlai-boot/
├── deploy/
│ └── docker/ # Docker 部署编排
├── docs/ # 项目文档与图片资源
├── sql/ # 数据库初始化脚本
├── src/main/java/com/youlai/boot/
│ ├── YouLaiBootApplication.java # 启动类
│ ├── auth/ # 认证授权(登录/登出/令牌)
│ ├── codegen/ # 代码生成器
│ ├── common/ # 公共模块(常量/枚举/统一响应)
│ ├── file/ # 文件服务MinIO/本地/OSS
│ ├── framework/ # 技术框架层
│ │ ├── apidoc/ # OpenAPI / Knife4j
│ │ ├── cache/ # Redis / Caffeine 缓存
│ │ ├── captcha/ # 图形验证码
│ │ ├── integration/ # 短信 / 邮件 / 微信
│ │ ├── job/ # XXL-Job 定时任务
│ │ ├── mybatis/ # MyBatis-Plus 配置
│ │ ├── security/ # Security / JWT / Token
│ │ └── web/ # 全局异常 / 跨域 / 限流
│ ├── message/ # SSE 消息推送
│ └── system/ # 系统业务(用户/角色/菜单/部门)
└── pom.xml # Maven 依赖管理
```
## 生态矩阵
**前端**
| 项目 | 技术栈 | 说明 |
| --- | --- | --- |
| [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-uniapp](https://gitee.com/youlaiorg/youlai-uniapp) | Vue 3 + uni-app | 移动端应用 |
|:-----|:-------|:-----|
| [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin) | Vue 3 + Element Plus | PC 管理前端(主推) |
| [youlai-app](https://gitee.com/youlaiorg/youlai-app) | Vue 3 + UniApp | 移动端 App |
**后端**
| 项目 | 技术栈 | 说明 |
|:-----|:-------|:-----|
| [youlai-nest](https://gitee.com/youlaiorg/youlai-nest) | NestJS + TypeORM | Node.js |
| [youlai-gin](https://gitee.com/youlaiorg/youlai-gin) | Go + Gorm | Go |
| [youlai-django](https://gitee.com/youlaiorg/youlai-django) | Django + DRF | Python |
| [youlai-thinkphp](https://gitee.com/youlaiorg/youlai-thinkphp) | ThinkPHP 8 | PHP |
| [youlai-aspnet](https://gitee.com/youlaiorg/youlai-aspnet) | ASP.NET Core | C# |
> **youlai-boot** 还提供以下变种和分支版本:[多租户](https://gitee.com/youlaiorg/youlai-boot-tenant)Spring Boot 4· [MyBatis-Flex](https://gitee.com/youlaiorg/youlai-boot-flex)Spring Boot 4· [Spring Boot 3](https://gitee.com/youlaiorg/youlai-boot/tree/spring-boot-3) · [PostgreSQL](https://gitee.com/youlaiorg/youlai-boot/tree/db-pg) · [多模块](https://gitee.com/youlaiorg/youlai-boot/tree/multi-module)
>
> 六种后端共享同一套 **RESTful API 规范** 和 **数据库结构**,前端可无缝切换。
## 文档资源
| 资源 | 地址 |
|:-----|:-----|
| 📖 完整文档站 | [www.youlai.tech](https://www.youlai.tech/) |
| 🖥️ PC 端在线预览 | [vue.youlai.tech](https://vue.youlai.tech) |
| 📱 移动端在线预览 | [app.youlai.tech](https://app.youlai.tech) |
| 🔗 Apifox 接口文档 | [apifox.com](https://www.apifox.cn/apidoc/shared-195e783f-4d85-4235-a038-eec696de4ea5) |
| 🔗 本地接口文档 | [localhost:8000/doc.html](http://localhost:8000/doc.html) |
## 参与贡献
欢迎提交 Issue 和 Pull Request详见 [贡献指南](https://www.youlai.tech/faq/help)。
[![Contributors](https://contrib.rocks/image?repo=haoxianrui/youlai-boot)](https://github.com/haoxianrui/youlai-boot/graphs/contributors)
## 开源协议
本项目基于 [Apache License 2.0](LICENSE) 开源,可免费用于商业项目。
---
## 🚀 快速开始
<table align="center">
<tr>
<td align="center">
<img src="./docs/images/qr/wechat-offical.png" height="180" alt="公众号「有来技术」"><br>
<sub>公众号「有来技术」</sub>
</td>
<td>&nbsp;&nbsp;&nbsp;&nbsp;</td>
<td align="center">
<img src="./docs/images/qr/wechat-mp.jpg" height="180" alt="小程序「有来技术」"><br>
<sub>小程序「有来技术」</sub>
</td>
<td>&nbsp;&nbsp;&nbsp;&nbsp;</td>
<td align="center">
<img src="./docs/images/qr/wechat-personal.png" height="180" alt="添加作者微信"><br>
<sub>添加作者微信</sub>
</td>
</tr>
</table>
### 环境要求
- JDK 17+
- MySQL 5.7+/8.0+
- Redis 6.0+
### 启动步骤
**1. 克隆项目**
```bash
git clone https://gitee.com/youlaiorg/youlai-boot.git
```
**2. 初始化数据库**
执行 [youlai_admin.sql](sql/mysql/youlai_admin.sql) 创建数据库和基础数据。
**3. 修改配置**
编辑 [application-dev.yml](src/main/resources/application-dev.yml),配置 MySQL 和 Redis
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/youlai_admin # 数据库连接地址
username: root # 数据库用户名
password: 123456 # 数据库密码
data:
redis:
host: localhost # Redis 地址
port: 6379 # Redis 端口
```
> 💡 默认连接线上环境(仅读权限),可直接启动体验。
**4. 启动项目**
运行 [YoulaiBootApplication.java](src/main/java/com/youlai/boot/YouLaiBootApplication.java),访问 http://localhost:8000/doc.html 查看接口文档。
---
## 📁 项目结构
```
youlai-boot
├── docker/ # Docker 部署
├── sql/ # 数据库脚本
├── src/main/java/com/youlai/boot/
│ ├── auth/ # 认证模块
│ ├── common/ # 公共模块
│ ├── config/ # 配置模块
│ ├── core/ # 核心模块AOP、异常、过滤器
│ ├── file/ # 文件服务
│ ├── plugin/ # 插件扩展Knife4j、MyBatis
│ ├── security/ # 安全模块JWT、Token
│ ├── support/ # 支撑服务邮件、短信、WebSocket
│ ├── system/ # 系统模块(用户、角色、菜单、部门)
│ ├── tool/ # 工具模块(代码生成)
│ └── 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) 协议开源,可免费用于商业项目。
---
## ✅ 项目统计
![](https://repobeats.axiom.co/api/embed/544c5c0b5b3611a6c4d5ef0faa243a9066b89659.svg)
---
## 🤝 贡献者
[![](https://contrib.rocks/image?repo=haoxianrui/youlai-boot)](https://github.com/haoxianrui/youlai-boot/graphs/contributors)
---
## 💖 技术交流
关注「有来技术」公众号,点击菜单【交流群】获取微信群二维码(为防营销广告,实属无奈,望理解):
<div align="center">
<img src="https://foruda.gitee.com/images/1737108820762592766/3390ed0d_716974.png" width="280">
</div>
**微信交流**:添加 **`haoxianrui`**,备注「前端/后端/全栈」
---
如果项目对你有帮助,欢迎 ⭐️ Star 支持!
<div align="center">
<a href="https://gitee.com/youlaiorg/youlai-boot"><b>⭐ Gitee</b></a> &nbsp;•&nbsp;
<a href="https://github.com/haoxianrui/youlai-boot"><b>⭐ GitHub</b></a> &nbsp;•&nbsp;
<a href="https://atomgit.com/youlai/youlai-boot"><b>⭐ AtomGit</b></a>
<br/>
<a href="https://www.youlai.tech"><b>🌐 官网</b></a> &nbsp;•&nbsp;
<a href="https://youlai.blog.csdn.net/"><b>📝 博客</b></a>
</div>
<p align="center"><em>技术交流 · 问题反馈 · 商务合作</em></p>

View File

@@ -15,7 +15,7 @@ services:
volumes:
- ./mysql/conf/my.cnf:/etc/my.cnf # 挂载 my.cnf 文件到容器的指定路径
- ./mysql/data:/var/lib/mysql # 持久化 MySQL 数据
- ../sql/mysql:/docker-entrypoint-initdb.d # 初始化 SQL 脚本目录
- ../sql:/docker-entrypoint-initdb.d # 初始化 SQL 脚本目录
ports:
- 3306:3306
networks:

0
docs/README.md Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 KiB

BIN
docs/images/logo/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

31
pom.xml
View File

@@ -6,13 +6,13 @@
<groupId>com.youlai</groupId>
<artifactId>youlai-boot</artifactId>
<version>4.1.0</version>
<version>4.3.1</version>
<description>基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.1</version> <!-- lookup parent from repository -->
<version>4.0.5</version> <!-- lookup parent from repository -->
<relativePath/>
</parent>
@@ -63,6 +63,8 @@
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
<weixin-java-miniapp.version>4.8.1.B</weixin-java-miniapp.version>
</properties>
<dependencies>
@@ -115,6 +117,11 @@
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
@@ -131,11 +138,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>
@@ -286,6 +288,21 @@
<version>${dynamic-datasource.version}</version>
</dependency>-->
<!--以5.3.0版本为例-->
<!-- jiguang-sdk -->
<dependency>
<groupId>io.github.jpush</groupId>
<artifactId>jiguang-sdk</artifactId>
<version>5.3.0</version>
</dependency>
<!-- 腾讯云短信 -->
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java-sms</artifactId>
<version>3.1.1451</version>
</dependency>
</dependencies>
<build>

View File

@@ -1,520 +0,0 @@
# 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', '18812345678', 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` 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 '创建时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_create_time` (`create_time`)
) ENGINE=MyISAM 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 '通知等级字典codenotice_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);

View File

@@ -372,7 +372,7 @@ CREATE TABLE `sys_user` (
-- 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', '18812345678', 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);
@@ -406,26 +406,30 @@ INSERT IGNORE INTO `sys_user_role` VALUES (7, 7);
-- ----------------------------
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

View File

@@ -2,6 +2,7 @@ package com.youlai.boot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 应用启动类
@@ -9,6 +10,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
* @author Ray.Hao
* @since 0.0.1
*/
@EnableScheduling
@SpringBootApplication
public class YouLaiBootApplication {

View File

@@ -0,0 +1,63 @@
package com.youlai.boot.app.controller;
import com.youlai.boot.app.model.req.MobileLoginReq;
import com.youlai.boot.app.model.req.MobileRegisterReq;
import com.youlai.boot.app.service.AppAuthService;
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 jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 认证控制层
*
*/
@Tag(name = "01.认证中心")
@RestController
@RequestMapping("/api/v1/app/auth/")
@RequiredArgsConstructor
@Slf4j
public class AppAuthController {
private final AppAuthService appAuthService;
@Operation(summary = "手机号注册")
@PostMapping("/register/mobile")
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.REGISTER)
public Result<AuthenticationToken> registerByMobile(@RequestBody @Valid MobileRegisterReq request) {
AuthenticationToken authenticationToken = appAuthService.registerByMobile(
request.getMobile(),
request.getCode(),
request.getPassword(),
request.getNickname()
);
return Result.success(authenticationToken);
}
@Operation(summary = "发送注册短信验证码")
@PostMapping("/register/sms/code")
public Result<Void> sendRegisterSmsCode(
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile
) {
return Result.judge(appAuthService.sendRegisterSmsCode(mobile));
}
@Operation(summary = "手机号验证码登录")
@PostMapping("/login/mobile")
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
public Result<AuthenticationToken> loginByMobile(@RequestBody @Valid MobileLoginReq request) {
AuthenticationToken authenticationToken = appAuthService.loginBySms(request.getMobile(), request.getCode());
return Result.success(authenticationToken);
}
}

View File

@@ -0,0 +1,57 @@
package com.youlai.boot.app.controller;
import com.youlai.boot.app.model.vo.AppFileInfo;
import com.youlai.boot.app.service.AppFileService;
import com.youlai.boot.common.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件控制层
*
* @author Ray.Hao
* @since 2022/10/16
*/
@Tag(name = "10.文件接口")
@RestController
@RequestMapping("/api/v1/app/files")
@RequiredArgsConstructor
public class AppFileController {
private final AppFileService fileService;
@PostMapping
@Operation(summary = "文件上传")
public Result<AppFileInfo> uploadFile(
@Parameter(
name = "file",
description = "表单文件对象",
required = true,
in = ParameterIn.DEFAULT,
schema = @Schema(name = "file", format = "binary")
)
@RequestPart(value = "file") MultipartFile file
) {
Assert.isTrue(!file.isEmpty(), "上传文件不能为空文件");
AppFileInfo fileInfo = fileService.uploadFile(file);
return Result.success(fileInfo);
}
@DeleteMapping
@Operation(summary = "文件删除")
@SneakyThrows
public Result<?> deleteFile(
@Parameter(description = "文件路径") @RequestParam String filePath
) {
boolean result = fileService.deleteFile(filePath);
return Result.judge(result);
}
}

View File

@@ -0,0 +1,47 @@
package com.youlai.boot.app.converter;
import com.youlai.boot.app.model.entity.AppUser;
import com.youlai.boot.app.model.form.AppUserForm;
import com.youlai.boot.common.model.Option;
import com.youlai.boot.system.model.entity.SysUser;
import com.youlai.boot.system.model.form.UserImportForm;
import com.youlai.boot.system.model.form.UserProfileForm;
import com.youlai.boot.system.model.vo.CurrentUserVO;
import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import java.util.List;
/**
* 用户对象转换器
*
* @author Ray.Hao
* @since 2022/6/8
*/
@Mapper(componentModel = "spring")
public interface AppUserConverter {
AppUserForm toForm(AppUser entity);
@InheritInverseConfiguration(name = "toForm")
AppUser toEntity(AppUserForm entity);
@Mappings({
@Mapping(target = "userId", source = "id")
})
CurrentUserVO toCurrentUserVo(AppUser entity);
AppUser toEntity(UserImportForm vo);
AppUser toEntity(UserProfileForm formData);
@Mappings({
@Mapping(target = "label", source = "nickname"),
@Mapping(target = "value", source = "id")
})
Option<String> toOption(AppUser entity);
List<Option<String>> toOptions(List<AppUser> list);
}

View File

@@ -0,0 +1,84 @@
package com.youlai.boot.app.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.app.model.entity.AppUser;
import com.youlai.boot.app.model.form.AppUserForm;
import com.youlai.boot.common.annotation.DataPermission;
import com.youlai.boot.framework.security.model.UserAuthInfo;
import com.youlai.boot.system.model.query.UserQuery;
import com.youlai.boot.system.model.vo.UserExportVO;
import com.youlai.boot.system.model.vo.UserPageVO;
import com.youlai.boot.system.model.vo.UserProfileVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 用户持久层接口
*
*/
@Mapper
public interface AppUserMapper extends BaseMapper<AppUser> {
/**
* 获取用户分页列表
*
* @param page 分页参数
* @param queryParams 查询参数
* @return 用户分页列表
*/
@DataPermission(deptAlias = "u", userAlias = "u")
Page<UserPageVO> getUserPage(Page<UserPageVO> page, @Param("queryParams") UserQuery queryParams);
/**
* 获取用户表单详情
*
* @param userId 用户ID
* @return 用户表单详情
*/
AppUserForm getUserFormData(Long userId);
/**
* 根据用户名获取认证信息
*
* @param username 用户名
* @return 认证信息
*/
UserAuthInfo getAuthInfoByUsername(String username);
default UserAuthInfo getAuthCredentialsByUsername(String username) {
return getAuthInfoByUsername(username);
}
/**
* 根据手机号获取用户认证信息
*
* @param mobile 手机号
* @return 认证信息
*/
UserAuthInfo getAuthInfoByMobile(String mobile);
default UserAuthInfo getAuthCredentialsByMobile(String mobile) {
return getAuthInfoByMobile(mobile);
}
/**
* 获取导出用户列表
*
* @param queryParams 查询参数
* @return 导出用户列表
*/
@DataPermission(deptAlias = "u", userAlias = "u")
List<UserExportVO> listExportUsers(UserQuery queryParams);
/**
* 获取用户个人中心信息
*
* @param userId 用户ID
* @return 用户个人中心信息
*/
UserProfileVO getUserProfile(Long userId);
}

View File

@@ -0,0 +1,65 @@
package com.youlai.boot.app.model.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.youlai.boot.common.base.BaseEntity;
import lombok.Getter;
import lombok.Setter;
/**
* 移动端用户实体
*/
@TableName("app_user")
@Getter
@Setter
public class AppUser extends BaseEntity {
/**
* 用户名
*/
private String username;
/**
* 昵称
*/
private String nickname;
/**
* 性别((1-男 2-女 0-保密)
*/
private Integer gender;
/**
* 密码
*/
private String password;
/**
* 用户头像
*/
private String avatar;
/**
* 绑定手机
*/
private String mobile;
/**
* 绑定微信
*/
private String wechatOpenid;
/**
* 状态((1-正常 0-禁用)
*/
private Integer status;
/**
* 用户邮箱
*/
private String email;
/**
* 是否删除(0-否 1-是)
*/
private Integer isDeleted;
}

View File

@@ -0,0 +1,58 @@
package com.youlai.boot.app.model.form;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import java.util.List;
/**
* 用户表单对象
*
* @author haoxr
* @since 2022/4/12 11:04
*/
@Schema(description = "用户表单对象")
@Data
public class AppUserForm {
@Schema(description="用户ID")
private Long id;
@Schema(description="用户名")
@NotBlank(message = "用户名不能为空")
private String username;
@Schema(description="昵称")
@NotBlank(message = "昵称不能为空")
private String nickname;
@Schema(description="手机号码")
@Pattern(regexp = "^$|^1(3\\d|4[5-9]|5[0-35-9]|6[2567]|7[0-8]|8\\d|9[0-35-9])\\d{8}$", message = "手机号码格式不正确")
private String mobile;
@Schema(description="性别")
private Integer gender;
@Schema(description="用户头像")
private String avatar;
@Schema(description="邮箱")
private String email;
@Schema(description="用户状态(1:正常;0:禁用)")
@Range(min = 0, max = 1, message = "用户状态不正确")
private Integer status;
@Schema(description="部门ID")
private Long deptId;
@Schema(description="角色ID集合")
@NotEmpty(message = "用户角色不能为空")
private List<Long> roleIds;
}

View File

@@ -0,0 +1,24 @@
package com.youlai.boot.app.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
/**
* 手机号登录请求参数
*
*/
@Schema(description = "手机号登录请求参数")
@Data
public class MobileLoginReq {
@Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18888888888")
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String mobile;
@Schema(description = "验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotBlank(message = "验证码不能为空")
private String code;
}

View File

@@ -0,0 +1,32 @@
package com.youlai.boot.app.model.req;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
/**
* 手机号注册请求参数
*
*/
@Schema(description = "手机号注册请求参数")
@Data
public class MobileRegisterReq {
@Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18888888888")
@NotBlank(message = "手机号不能为空")
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String mobile;
@Schema(description = "验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotBlank(message = "验证码不能为空")
private String code;
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotBlank(message = "密码不能为空")
private String password;
@Schema(description = "昵称", example = "用户昵称")
private String nickname;
}

View File

@@ -0,0 +1,23 @@
package com.youlai.boot.app.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 文件信息对象
*
* @author Ray.Hao
* @since 1.0.0
*/
@Schema(description = "文件对象")
@Data
public class AppFileInfo {
@Schema(description = "文件名称")
private String name;
@Schema(description = "文件URL")
private String url;
}

View File

@@ -0,0 +1,70 @@
package com.youlai.boot.app.service;
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
import com.youlai.boot.framework.security.model.AuthenticationToken;
public interface AppAuthService {
/**
* 获取验证码
*/
CaptchaInfo getCaptcha();
/**
* 发送注册短信验证码
*
* @param mobile 手机号
*/
boolean sendRegisterSmsCode(String mobile);
/**
* 手机号注册
*
* @param mobile 手机号
* @param code 验证码
* @param password 密码
* @param nickname 昵称
* @return 认证令牌
*/
AuthenticationToken registerByMobile(String mobile, String code, String password, String nickname);
/**
* 发送登录短信验证码
*
* @param mobile 手机号
*/
void sendLoginSmsCode(String mobile);
/**
* 短信验证码登录
*
* @param mobile 手机号
* @param code 验证码
* @return 认证令牌
*/
AuthenticationToken loginBySms(String mobile, String code);
/**
* 账号密码登录
*
* @param username 用户名
* @param password 密码
* @return 认证令牌
*/
AuthenticationToken login(String username, String password);
/**
* 退出登录
*/
void logout();
/**
* 刷新令牌
*
* @param refreshToken 刷新令牌
* @return 认证令牌
*/
AuthenticationToken refreshToken(String refreshToken);
}

View File

@@ -0,0 +1,30 @@
package com.youlai.boot.app.service;
import com.youlai.boot.app.model.vo.AppFileInfo;
import org.springframework.web.multipart.MultipartFile;
/**
* 对象存储服务接口层
*
* @author haoxr
* @since 2022/11/19
*/
public interface AppFileService {
/**
* 上传文件
* @param file 表单文件对象
* @return 文件信息
*/
AppFileInfo uploadFile(MultipartFile file);
/**
* 删除文件
*
* @param filePath 文件完整URL
* @return 删除结果
*/
boolean deleteFile(String filePath);
}

View File

@@ -0,0 +1,198 @@
package com.youlai.boot.app.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.boot.app.model.entity.AppUser;
import com.youlai.boot.app.model.form.AppUserForm;
import com.youlai.boot.common.model.Option;
import com.youlai.boot.framework.security.model.UserAuthInfo;
import com.youlai.boot.system.model.entity.SysUser;
import com.youlai.boot.system.model.form.*;
import com.youlai.boot.system.model.query.UserQuery;
import com.youlai.boot.system.model.vo.CurrentUserVO;
import com.youlai.boot.system.model.vo.UserExportVO;
import com.youlai.boot.system.model.vo.UserPageVO;
import com.youlai.boot.system.model.vo.UserProfileVO;
import java.util.List;
/**
* 用户业务接口
*
* @author Ray.Hao
* @since 2022/1/14
*/
public interface AppUserService extends IService<AppUser> {
/**
* 用户分页列表
*
* @return {@link IPage<UserPageVO>} 用户分页列表
*/
IPage<UserPageVO> getUserPage(UserQuery queryParams);
/**
* 获取用户表单数据
*
* @param userId 用户ID
* @return {@link AppUserForm} 用户表单数据
*/
AppUserForm getUserFormData(Long userId);
/**
* 新增用户
*
* @param userForm 用户表单对象
* @return {@link Boolean} 是否新增成功
*/
boolean saveUser(AppUserForm userForm);
/**
* 修改用户
*
* @param userId 用户ID
* @param userForm 用户表单对象
* @return {@link Boolean} 是否修改成功
*/
boolean updateUser(Long userId, AppUserForm userForm);
/**
* 删除用户
*
* @param idsStr 用户ID多个以英文逗号(,)分割
* @return {@link Boolean} 是否删除成功
*/
boolean deleteUsers(String idsStr);
/**
* 根据用户名获取认证信息
*
* @param username 用户名
* @return {@link UserAuthInfo}
*/
UserAuthInfo getAuthInfoByUsername(String username);
default UserAuthInfo getAuthCredentialsByUsername(String username) {
return getAuthInfoByUsername(username);
}
/**
* 获取导出用户列表
*
* @param queryParams 查询参数
* @return {@link List<UserExportVO>} 导出用户列表
*/
List<UserExportVO> listExportUsers(UserQuery queryParams);
/**
* 获取登录用户信息
*
* @return {@link CurrentUserVO} 登录用户信息
*/
CurrentUserVO getCurrentUserInfo();
/**
* 获取个人中心用户信息
*
* @return {@link UserProfileVO} 个人中心用户信息
*/
UserProfileVO getUserProfile(Long userId);
/**
* 修改个人中心用户信息
*
* @param formData 表单数据
* @return {@link Boolean} 是否修改成功
*/
boolean updateUserProfile(UserProfileForm formData);
/**
* 修改指定用户密码
*
* @param userId 用户ID
* @param data 修改密码表单数据
* @return {@link Boolean} 是否修改成功
*/
boolean changeUserPassword(Long userId, PasswordUpdateForm data);
/**
* 重置指定用户密码
*
* @param userId 用户ID
* @param password 重置后的密码
* @return {@link Boolean} 是否重置成功
*/
boolean resetUserPassword(Long userId, String password);
/**
* 发送短信验证码(绑定或更换手机号)
*
* @param mobile 手机号
* @return {@link Boolean} 是否发送成功
*/
boolean sendMobileCode(String mobile);
/**
* 修改当前用户手机号
*
* @param data 表单数据
* @return {@link Boolean} 是否修改成功
*/
boolean bindOrChangeMobile(MobileUpdateForm data);
/**
* 发送邮箱验证码(绑定或更换邮箱)
*
* @param email 邮箱
*/
void sendEmailCode(String email);
/**
* 绑定或更换邮箱
*
* @param data 表单数据
* @return {@link Boolean} 是否绑定成功
*/
boolean bindOrChangeEmail(EmailUpdateForm data);
/**
* 解绑手机号
*
* @param data 表单数据
* @return {@link Boolean} 是否解绑成功
*/
boolean unbindMobile(PasswordVerifyForm data);
/**
* 解绑邮箱
*
* @param data 表单数据
* @return {@link Boolean} 是否解绑成功
*/
boolean unbindEmail(PasswordVerifyForm data);
/**
* 获取用户选项列表
*
* @return {@link List<Option<String>>} 用户选项列表
*/
// List<Option<String>> listUserOptions();
/**
* 根据手机号获取用户认证信息
*
* @param mobile 手机号
* @return {@link UserAuthInfo}
*/
UserAuthInfo getAuthInfoByMobile(String mobile);
default UserAuthInfo getAuthCredentialsByMobile(String mobile) {
return getAuthInfoByMobile(mobile);
}
}

View File

@@ -0,0 +1,198 @@
package com.youlai.boot.app.service.impl;
import cn.hutool.core.util.StrUtil;
import com.youlai.boot.app.model.entity.AppUser;
import com.youlai.boot.app.service.AppAuthService;
import com.youlai.boot.app.service.AppUserService;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.util.CodeGeneratorUtil;
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
import com.youlai.boot.framework.captcha.service.CaptchaService;
import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
import com.youlai.boot.framework.integration.sms.service.SmsService;
import com.youlai.boot.framework.integration.sms.service.impl.AliyunSmsService;
import com.youlai.boot.framework.integration.sms.service.impl.TencentSmsService;
import com.youlai.boot.framework.security.model.AuthenticationToken;
import com.youlai.boot.framework.security.token.TokenManager;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
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.security.crypto.password.PasswordEncoder;
import com.youlai.boot.common.exception.BusinessException;
import cn.hutool.core.lang.Assert;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 认证服务实现类
*
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class AppAuthServiceImpl implements AppAuthService {
private final AuthenticationManager authenticationManager;
private final TokenManager tokenManager;
private final AliyunSmsService smsService;
private final RedisTemplate<String, Object> redisTemplate;
private final CaptchaService captchaService;
private final AppUserService userService;
private final PasswordEncoder passwordEncoder;
@Override
public CaptchaInfo getCaptcha() {
return null;
}
/**
* 发送注册短信验证码
*
* @param mobile 手机号
*/
@Override
public boolean sendRegisterSmsCode(String mobile) {
// 检查手机号是否已注册
long count = userService.count(new LambdaQueryWrapper<AppUser>()
.eq(AppUser::getMobile, mobile));
if (count > 0) {
throw new BusinessException("该手机号已被注册");
}
String code = CodeGeneratorUtil.generateNumericCode(6);
// 发送短信验证码
Map<String, String> templateParams = new HashMap<>();
templateParams.put("code", code);
boolean success = smsService.sendSms(mobile, SmsTypeEnum.REGISTER, templateParams);
if (success) {
// 缓存验证码至Redis用于注册校验
redisTemplate.opsForValue().set(StrUtil.format(RedisConstants.Captcha.SMS_REGISTER_CODE, mobile), code, 5, TimeUnit.MINUTES);
} else {
log.warn("短信发送失败,手机号: {}", mobile);
}
return success;
}
/**
* 手机号注册
*
* @param mobile 手机号
* @param code 验证码
* @param password 密码
* @param nickname 昵称
* @return 认证令牌
*/
@Override
public AuthenticationToken registerByMobile(String mobile, String code, String password, String nickname) {
// 1. 校验验证码
String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_REGISTER_CODE, mobile);
String cachedCode = (String) redisTemplate.opsForValue().get(cacheKey);
if (StrUtil.isBlank(cachedCode)) {
throw new BusinessException("验证码已过期");
}
if (!Objects.equals(code, cachedCode)) {
throw new BusinessException("验证码错误");
}
// 2. 检查手机号是否已注册
long count = userService.count(new LambdaQueryWrapper<AppUser>()
.eq(AppUser::getMobile, mobile));
Assert.isTrue(count == 0, "该手机号已被注册");
// 3. 创建新用户
AppUser user = new AppUser();
user.setUsername(mobile); // 使用手机号作为用户名
user.setMobile(mobile);
user.setPassword(passwordEncoder.encode(password));
user.setNickname(StrUtil.isNotBlank(nickname) ? nickname : mobile.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"));
user.setStatus(1); // 正常状态
boolean saveResult = userService.save(user);
if (!saveResult) {
throw new BusinessException("注册失败");
}
// 4. 删除验证码
redisTemplate.delete(cacheKey);
// 5. 自动登录并生成token
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(mobile, password);
Authentication authentication = authenticationManager.authenticate(authenticationToken);
AuthenticationToken authenticationTokenResponse =
tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
return authenticationTokenResponse;
}
@Override
public void sendLoginSmsCode(String mobile) {
String code = "1234";
Map<String, String> templateParams = new HashMap<>();
templateParams.put("code", code);
boolean success = false;
// 方式1: 使用阿里云短信(默认)
// try {
// success = aliyunSmsService.sendSms(mobile, SmsTypeEnum.LOGIN, templateParams);
// log.info("阿里云短信发送结果: {}", success ? "成功" : "失败");
// } catch (Exception e) {
// log.error("阿里云短信发送异常", e);
// }
// 方式2: 使用腾讯云短信(需要时取消下面注释,并注释掉上面的阿里云代码)
try {
success = smsService.sendSms(mobile, SmsTypeEnum.LOGIN, templateParams);
log.info("腾讯云短信发送结果: {}", success ? "成功" : "失败");
} catch (Exception e) {
log.error("腾讯云短信发送异常", e);
}
if (success) {
redisTemplate.opsForValue().set(StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile), code, 5, TimeUnit.MINUTES);
} else {
log.warn("短信发送失败,手机号: {}", mobile);
}
}
@Override
public AuthenticationToken loginBySms(String mobile, String code) {
return null;
}
@Override
public AuthenticationToken login(String username, String password) {
return null;
}
@Override
public void logout() {
}
@Override
public AuthenticationToken refreshToken(String refreshToken) {
return null;
}
}

View File

@@ -0,0 +1,683 @@
package com.youlai.boot.app.service.impl;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.app.converter.AppUserConverter;
import com.youlai.boot.app.mapper.AppUserMapper;
import com.youlai.boot.app.model.entity.AppUser;
import com.youlai.boot.app.model.form.AppUserForm;
import com.youlai.boot.app.service.AppUserService;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.constant.SystemConstants;
import com.youlai.boot.common.exception.BusinessException;
import com.youlai.boot.common.model.Option;
import com.youlai.boot.framework.integration.mail.service.MailService;
import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
import com.youlai.boot.framework.integration.sms.service.SmsService;
import com.youlai.boot.framework.security.model.RoleDataScope;
import com.youlai.boot.framework.security.model.UserAuthInfo;
import com.youlai.boot.framework.security.token.TokenManager;
import com.youlai.boot.framework.security.util.SecurityUtils;
import com.youlai.boot.system.converter.UserConverter;
import com.youlai.boot.system.enums.DictCodeEnum;
import com.youlai.boot.system.mapper.UserMapper;
import com.youlai.boot.system.model.entity.Dept;
import com.youlai.boot.system.model.entity.DictItem;
import com.youlai.boot.system.model.entity.Role;
import com.youlai.boot.system.model.form.*;
import com.youlai.boot.system.model.query.UserQuery;
import com.youlai.boot.system.model.vo.CurrentUserVO;
import com.youlai.boot.system.model.vo.UserExportVO;
import com.youlai.boot.system.model.vo.UserPageVO;
import com.youlai.boot.system.model.vo.UserProfileVO;
import com.youlai.boot.system.service.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* app用户业务实现类
*
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class AppUserServiceImpl extends ServiceImpl<AppUserMapper, AppUser> implements AppUserService {
private final PasswordEncoder passwordEncoder;
private final UserRoleService userRoleService;
private final DeptService deptService;
private final RoleService roleService;
private final RoleMenuService roleMenuService;
private final SmsService smsService;
private final MailService mailService;
private final StringRedisTemplate redisTemplate;
private final TokenManager tokenManager;
private final DictItemService dictItemService;
private final AppUserConverter userConverter;
/**
* 获取用户分页列表
*
* @param queryParams 查询参数
* @return {@link IPage<UserPageVO>} 用户分页列表
*/
@Override
public IPage<UserPageVO> getUserPage(UserQuery queryParams) {
// 参数构建
int pageNum = queryParams.getPageNum();
int pageSize = queryParams.getPageSize();
Page<UserPageVO> page = new Page<>(pageNum, pageSize);
boolean isRoot = SecurityUtils.isRoot();
queryParams.setIsRoot(isRoot);
// 查询数据
return this.baseMapper.getUserPage(page, queryParams);
}
/**
* 获取用户表单数据
*
* @param userId 用户ID
* @return {@link AppUserForm} 用户表单数据
*/
@Override
public AppUserForm getUserFormData(Long userId) {
return this.baseMapper.getUserFormData(userId);
}
/**
* 新增用户
*
* @param userForm 用户表单对象
* @return true|false
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveUser(AppUserForm userForm) {
String username = userForm.getUsername();
// 实体转换 form->entity
AppUser entity = userConverter.toEntity(userForm);
// 检查用户名是否已存在
long count = this.count(new LambdaQueryWrapper<AppUser>()
.eq(AppUser::getUsername, username));
Assert.isTrue(count == 0, "用户名已存在");
// 设置默认加密密码
String defaultEncryptPwd = passwordEncoder.encode(SystemConstants.DEFAULT_PASSWORD);
entity.setPassword(defaultEncryptPwd);
// entity.setCreateBy(SecurityUtils.getUserId());
// 新增用户
boolean result = this.save(entity);
if (result) {
// 保存用户角色
userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds());
}
return result;
}
/**
* 更新用户
*
* @param userId 用户ID
* @param userForm 用户表单对象
* @return true|false
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateUser(Long userId, AppUserForm userForm) {
String username = userForm.getUsername();
// 获取原用户信息
AppUser oldUser = this.getById(userId);
Assert.notNull(oldUser, "用户不存在");
// 检查用户名是否已存在(排除当前用户)
long count = this.count(new LambdaQueryWrapper<AppUser>()
.eq(AppUser::getUsername, username)
.ne(AppUser::getId, userId)
);
Assert.isTrue(count == 0, "用户名已存在");
// form -> entity
AppUser entity = userConverter.toEntity(userForm);
// entity.setUpdateBy(SecurityUtils.getUserId());
// 修改用户
boolean result = this.updateById(entity);
if (result) {
// 保存用户角色
userRoleService.saveUserRoles(entity.getId(), userForm.getRoleIds());
}
return result;
}
/**
* 删除用户
*
* @param idsStr 用户ID多个以英文逗号(,)分割
* @return true|false
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteUsers(String idsStr) {
Assert.isTrue(StrUtil.isNotBlank(idsStr), "删除的用户数据为空");
// 逻辑删除
List<Long> ids = Arrays.stream(idsStr.split(","))
.map(Long::parseLong)
.collect(Collectors.toList());
boolean result = this.removeByIds(ids);
return result;
}
/**
* 根据用户名获取认证凭证信息
*
* @param username 用户名
* @return 用户认证凭证信息 {@link UserAuthInfo}
*/
@Override
public UserAuthInfo getAuthInfoByUsername(String username) {
UserAuthInfo userAuthInfo = this.baseMapper.getAuthInfoByUsername(username);
if (userAuthInfo != null) {
Set<String> roles = userAuthInfo.getRoles();
// 获取数据权限列表(用于并集策略)
List<RoleDataScope> dataScopes = roleService.getRoleDataScopes(roles);
userAuthInfo.setDataScopes(dataScopes);
}
return userAuthInfo;
}
/**
* 根据手机号获取用户认证信息
*
* @param mobile 手机号
* @return 用户认证信息
*/
@Override
public UserAuthInfo getAuthInfoByMobile(String mobile) {
if (StrUtil.isBlank(mobile)) {
return null;
}
UserAuthInfo userAuthInfo = this.baseMapper.getAuthInfoByMobile(mobile);
if (userAuthInfo != null) {
Set<String> roles = userAuthInfo.getRoles();
// 获取数据权限列表(用于并集策略)
List<RoleDataScope> dataScopes = roleService.getRoleDataScopes(roles);
userAuthInfo.setDataScopes(dataScopes);
}
return userAuthInfo;
}
/**
* 获取导出用户列表
*
* @param queryParams 查询参数
* @return {@link List<UserExportVO>} 导出用户列表
*/
@Override
public List<UserExportVO> listExportUsers(UserQuery queryParams) {
boolean isRoot = SecurityUtils.isRoot();
queryParams.setIsRoot(isRoot);
List<UserExportVO> exportUsers = this.baseMapper.listExportUsers(queryParams);
if (CollectionUtil.isNotEmpty(exportUsers)) {
//获取性别的字典项
Map<String, String> genderMap = dictItemService.list(
new LambdaQueryWrapper<DictItem>().eq(DictItem::getDictCode,
DictCodeEnum.GENDER.getValue())
).stream()
.collect(Collectors.toMap(DictItem::getValue, DictItem::getLabel)
);
exportUsers.forEach(item -> {
String gender = item.getGender();
if (StrUtil.isBlank(gender)) {
return;
}
// 判断map是否为空
if (genderMap.isEmpty()) {
return;
}
item.setGender(genderMap.get(gender));
});
}
return exportUsers;
}
/**
* 获取登录用户信息
*
* @return {@link CurrentUserVO} 用户信息
*/
@Override
public CurrentUserVO getCurrentUserInfo() {
String username = SecurityUtils.getUsername();
// 获取登录用户基础信息
AppUser user = this.getOne(new LambdaQueryWrapper<AppUser>()
.eq(AppUser::getUsername, username)
.select(
AppUser::getId,
AppUser::getUsername,
AppUser::getNickname,
AppUser::getAvatar,
AppUser::getGender
// AppUser::getDeptId
)
);
// entity->Vo
CurrentUserVO userInfoVo = userConverter.toCurrentUserVo(user);
// 性别
userInfoVo.setGender(user.getGender());
// 部门名称
// if (user.getDeptId() != null) {
// Dept dept = deptService.getById(user.getDeptId());
// if (dept != null) {
// userInfoVo.setDeptName(dept.getName());
// }
// }
// 用户角色集合
Set<String> roles = SecurityUtils.getRoles();
userInfoVo.setRoles(roles);
// 用户角色名称集合
if (CollectionUtil.isNotEmpty(roles)) {
Set<String> roleNames = roleService.list(new LambdaQueryWrapper<Role>()
.in(Role::getCode, roles)
.select(Role::getName)
).stream()
.map(Role::getName)
.filter(StrUtil::isNotBlank)
.collect(Collectors.toCollection(LinkedHashSet::new));
userInfoVo.setRoleNames(roleNames);
}
// 用户权限集合
if (CollectionUtil.isNotEmpty(roles)) {
Set<String> perms = roleMenuService.getRolePermsByRoleCodes(roles);
userInfoVo.setPerms(perms);
}
return userInfoVo;
}
/**
* 获取个人中心用户信息
*
* @param userId 用户ID
* @return {@link UserProfileVO} 个人中心用户信息
*/
@Override
public UserProfileVO getUserProfile(Long userId) {
return this.baseMapper.getUserProfile(userId);
}
/**
* 修改个人中心用户信息
*
* @param formData 表单数据
* @return true|false
*/
@Override
public boolean updateUserProfile(UserProfileForm formData) {
Long userId = SecurityUtils.getUserId();
if (formData.getNickname() == null && formData.getAvatar() == null && formData.getGender() == null) {
throw new BusinessException("请修改至少一个字段");
}
return this.update(new LambdaUpdateWrapper<AppUser>()
.eq(AppUser::getId, userId)
.set(formData.getNickname() != null, AppUser::getNickname, formData.getNickname())
.set(formData.getAvatar() != null, AppUser::getAvatar, formData.getAvatar())
.set(formData.getGender() != null, AppUser::getGender, formData.getGender())
);
}
/**
* 修改指定用户密码
*
* @param userId 用户ID
* @param data 密码修改表单数据
* @return true|false
*/
@Override
public boolean changeUserPassword(Long userId, PasswordUpdateForm data) {
AppUser user = this.getById(userId);
if (user == null) {
throw new BusinessException("用户不存在");
}
String oldPassword = data.getOldPassword();
// 校验原密码
if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
throw new BusinessException("原密码错误");
}
// 新旧密码不能相同
if (passwordEncoder.matches(data.getNewPassword(), user.getPassword())) {
throw new BusinessException("新密码不能与原密码相同");
}
// 判断新密码和确认密码是否一致
if (!Objects.equals(data.getNewPassword(), data.getConfirmPassword())) {
throw new BusinessException("新密码和确认密码不一致");
}
String newPassword = data.getNewPassword();
boolean result = this.update(new LambdaUpdateWrapper<AppUser>()
.eq(AppUser::getId, userId)
.set(AppUser::getPassword, passwordEncoder.encode(newPassword))
);
if (result) {
// 密码变更后,使当前用户的所有会话失效,强制重新登录
tokenManager.invalidateUserSessions(userId);
}
return result;
}
/**
* 重置指定用户密码
*
* @param userId 用户ID
* @param password 密码重置表单数据
* @return true|false
*/
@Override
public boolean resetUserPassword(Long userId, String password) {
boolean result = this.update(new LambdaUpdateWrapper<AppUser>()
.eq(AppUser::getId, userId)
.set(AppUser::getPassword, passwordEncoder.encode(password))
);
if (result) {
// 管理员重置用户密码后,使该用户的所有会话失效
tokenManager.invalidateUserSessions(userId);
}
return result;
}
/**
* 发送短信验证码(绑定或更换手机号)
*
* @param mobile 手机号
* @return true|false
*/
@Override
public boolean sendMobileCode(String mobile) {
Long currentUserId = SecurityUtils.getUserId();
long mobileCount = this.count(new LambdaQueryWrapper<AppUser>()
.eq(AppUser::getMobile, mobile)
.ne(AppUser::getId, currentUserId)
);
if (mobileCount > 0) {
throw new BusinessException("手机号已被其他账号绑定");
}
// String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
// TODO 为了方便测试,验证码固定为 123456实际开发中在配置了厂商短信服务后可以使用上面的随机验证码
String code = "123456";
Map<String, String> templateParams = new HashMap<>();
templateParams.put("code", code);
boolean result = smsService.sendSms(mobile, SmsTypeEnum.CHANGE_MOBILE, templateParams);
if (result) {
// 缓存验证码5分钟有效用于更换手机号校验
String redisCacheKey = StrUtil.format(RedisConstants.Captcha.MOBILE_CODE, mobile);
redisTemplate.opsForValue().set(redisCacheKey, code, 5, TimeUnit.MINUTES);
}
return result;
}
/**
* 绑定或更换手机号
*
* @param form 表单数据
* @return true|false
*/
@Override
public boolean bindOrChangeMobile(MobileUpdateForm form) {
Long currentUserId = SecurityUtils.getUserId();
AppUser currentUser = this.getById(currentUserId);
if (currentUser == null) {
throw new BusinessException("用户不存在");
}
if (!passwordEncoder.matches(form.getPassword(), currentUser.getPassword())) {
throw new BusinessException("当前密码错误");
}
// 校验验证码
String inputVerifyCode = form.getCode();
String mobile = form.getMobile();
String cacheKey = StrUtil.format(RedisConstants.Captcha.MOBILE_CODE, mobile);
String cachedVerifyCode = redisTemplate.opsForValue().get(cacheKey);
if (StrUtil.isBlank(cachedVerifyCode)) {
throw new BusinessException("验证码已过期");
}
if (!inputVerifyCode.equals(cachedVerifyCode)) {
throw new BusinessException("验证码错误");
}
long mobileCount = this.count(new LambdaQueryWrapper<AppUser>()
.eq(AppUser::getMobile, mobile)
.ne(AppUser::getId, currentUserId)
);
if (mobileCount > 0) {
throw new BusinessException("手机号已被其他账号绑定");
}
redisTemplate.delete(cacheKey);
// 更新手机号码
return this.update(
new LambdaUpdateWrapper<AppUser>()
.eq(AppUser::getId, currentUserId)
.set(AppUser::getMobile, mobile)
);
}
/**
* 发送邮箱验证码(绑定或更换邮箱)
*
* @param email 邮箱
*/
@Override
public void sendEmailCode(String email) {
Long currentUserId = SecurityUtils.getUserId();
long emailCount = this.count(new LambdaQueryWrapper<AppUser>()
.eq(AppUser::getEmail, email)
.ne(AppUser::getId, currentUserId)
);
if (emailCount > 0) {
throw new BusinessException("邮箱已被其他账号绑定");
}
// String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
// TODO 为了方便测试,验证码固定为 123456实际开发中在配置了邮箱服务后可以使用上面的随机验证码
String code = "123456";
mailService.sendMail(email, "邮箱验证码", "您的验证码为:" + code + "请在5分钟内使用");
// 缓存验证码5分钟有效用于更换邮箱校验
String redisCacheKey = StrUtil.format(RedisConstants.Captcha.EMAIL_CODE, email);
redisTemplate.opsForValue().set(redisCacheKey, code, 5, TimeUnit.MINUTES);
}
/**
* 修改当前用户邮箱
*
* @param form 表单数据
* @return true|false
*/
@Override
public boolean bindOrChangeEmail(EmailUpdateForm form) {
Long currentUserId = SecurityUtils.getUserId();
AppUser currentUser = this.getById(currentUserId);
if (currentUser == null) {
throw new BusinessException("用户不存在");
}
if (!passwordEncoder.matches(form.getPassword(), currentUser.getPassword())) {
throw new BusinessException("当前密码错误");
}
// 获取前端输入的验证码
String inputVerifyCode = form.getCode();
// 获取缓存的验证码
String email = form.getEmail();
String redisCacheKey = StrUtil.format(RedisConstants.Captcha.EMAIL_CODE, email);
String cachedVerifyCode = redisTemplate.opsForValue().get(redisCacheKey);
if (StrUtil.isBlank(cachedVerifyCode)) {
throw new BusinessException("验证码已过期");
}
if (!inputVerifyCode.equals(cachedVerifyCode)) {
throw new BusinessException("验证码错误");
}
long emailCount = this.count(new LambdaQueryWrapper<AppUser>()
.eq(AppUser::getEmail, email)
.ne(AppUser::getId, currentUserId)
);
if (emailCount > 0) {
throw new BusinessException("邮箱已被其他账号绑定");
}
redisTemplate.delete(redisCacheKey);
// 更新邮箱地址
return this.update(
new LambdaUpdateWrapper<AppUser>()
.eq(AppUser::getId, currentUserId)
.set(AppUser::getEmail, email)
);
}
/**
* 解绑手机号
*
* @param form 表单数据
* @return true|false
*/
@Override
public boolean unbindMobile(PasswordVerifyForm form) {
Long currentUserId = SecurityUtils.getUserId();
AppUser currentUser = this.getById(currentUserId);
if (currentUser == null) {
throw new BusinessException("用户不存在");
}
if (StrUtil.isBlank(currentUser.getMobile())) {
throw new BusinessException("当前账号未绑定手机号");
}
if (!passwordEncoder.matches(form.getPassword(), currentUser.getPassword())) {
throw new BusinessException("当前密码错误");
}
return this.update(new LambdaUpdateWrapper<AppUser>()
.eq(AppUser::getId, currentUserId)
.set(AppUser::getMobile, null)
);
}
/**
* 解绑邮箱
*
* @param form 表单数据
* @return true|false
*/
@Override
public boolean unbindEmail(PasswordVerifyForm form) {
Long currentUserId = SecurityUtils.getUserId();
AppUser currentUser = this.getById(currentUserId);
if (currentUser == null) {
throw new BusinessException("用户不存在");
}
if (StrUtil.isBlank(currentUser.getEmail())) {
throw new BusinessException("当前账号未绑定邮箱");
}
if (!passwordEncoder.matches(form.getPassword(), currentUser.getPassword())) {
throw new BusinessException("当前密码错误");
}
return this.update(new LambdaUpdateWrapper<AppUser>()
.eq(AppUser::getId, currentUserId)
.set(AppUser::getEmail, null)
);
}
/**
* 获取用户选项列表
*
* @return {@link List<Option<String>>} 用户选项列表
*/
// @Override
// public List<Option<String>> listUserOptions() {
// List<AppUser> list = this.list(new LambdaQueryWrapper<AppUser>()
// .eq(AppUser::getStatus, 1)
// );
// return userConverter.toOptions(list);
// }
}

View File

@@ -0,0 +1,93 @@
package com.youlai.boot.app.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.app.model.vo.AppFileInfo;
import com.youlai.boot.app.service.AppFileService;
import com.youlai.boot.file.service.FileService;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.InputStream;
import java.time.LocalDateTime;
/**
* 本地存储服务类
*
* @author Theo
* @since 2024-12-09 17:11
*/
@Data
@Slf4j
@Component
//@ConditionalOnProperty(value = "oss.type", havingValue = "local")
//@ConfigurationProperties(prefix = "oss.local")
@RequiredArgsConstructor
public class LocalAppFileService implements AppFileService {
@Value("${oss.local.storage-path}")
private String storagePath;
/**
* 上传文件方法
*
* @param file 表单文件对象
* @return 文件信息
*/
@Override
public AppFileInfo uploadFile(MultipartFile file) {
// 获取文件名
String originalFilename = file.getOriginalFilename();
// 获取文件后缀
String suffix = FileUtil.getSuffix(originalFilename);
// 生成uuid
String fileName = IdUtil.simpleUUID()+ "." + suffix;;
// 生成文件名(日期文件夹)
String folder = DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
String filePrefix = storagePath.endsWith(File.separator) ? storagePath : storagePath + File.separator;
// try-with-resource 语法糖自动释放流
try (InputStream inputStream = file.getInputStream()) {
// 上传文件
FileUtil.writeFromStream(inputStream, filePrefix + folder + File.separator + fileName);
} catch (Exception e) {
log.error("文件上传失败", e);
throw new RuntimeException("文件上传失败");
}
// 获取文件访问路径,因为这里是本地存储,所以直接返回文件的相对路径,需要前端自行处理访问前缀
String fileUrl = File.separator + folder + File.separator + fileName;
AppFileInfo fileInfo = new AppFileInfo();
fileInfo.setName(originalFilename);
fileInfo.setUrl(fileUrl);
return fileInfo;
}
/**
* 删除文件
* @param filePath 文件完整URL
* @return 是否删除成功
*/
@Override
public boolean deleteFile(String filePath) {
//判断文件是否为空
if (filePath == null || filePath.isEmpty()) {
return false;
}
// 判断filepath是否为文件夹
if (FileUtil.isDirectory(storagePath + filePath)) {
// 禁止删除文件夹
return false;
}
// 删除文件
return FileUtil.del(storagePath + filePath);
}
}

View File

@@ -1,12 +1,13 @@
package com.youlai.boot.auth.controller;
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;
@@ -32,24 +33,24 @@ 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<AuthenticationToken> login(@RequestBody @Valid LoginRequest request) {
@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 = "18888888888") @RequestParam String mobile,
@Parameter(description = "验证码", example = "123456") @RequestParam String code
) {
AuthenticationToken loginResult = authService.loginBySms(mobile, code);
@@ -59,7 +60,7 @@ public class AuthController {
@Operation(summary = "发送登录短信验证码")
@PostMapping("/sms/code")
public Result<Void> sendSmsCode(
@Parameter(description = "手机号", example = "18812345678") @RequestParam String mobile
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile
) {
authService.sendSmsCode(mobile);
return Result.success();
@@ -67,7 +68,7 @@ public class AuthController {
@Operation(summary = "退出登录")
@DeleteMapping("/logout")
@Log(value = "退出登录", module = LogModuleEnum.LOGIN)
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGOUT)
public Result<Void> logout() {
authService.logout();
return Result.success();

View File

@@ -1,21 +1,27 @@
package com.youlai.boot.auth.controller;
import com.youlai.boot.auth.model.vo.WechatMiniappLoginResult;
import com.youlai.boot.auth.service.WechatMiniappAuthService;
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.core.web.Result;
import com.youlai.boot.security.model.AuthenticationToken;
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;
/**
* 微信小程序认证控制层
*
@@ -24,12 +30,12 @@ import org.springframework.web.bind.annotation.RestController;
*/
@Tag(name = "13.微信小程序认证")
@RestController
@RequestMapping("/api/v1/wechat/miniapp/auth")
@RequestMapping("/api/v1/wxma/auth")
@RequiredArgsConstructor
@Slf4j
public class WechatMiniappAuthController {
public class WxMaAuthController {
private final WechatMiniappAuthService wechatMiniappAuthService;
private final WxMaAuthService wxMaAuthService;
/**
* 静默登录
@@ -42,12 +48,12 @@ public class WechatMiniappAuthController {
*/
@Operation(summary = "静默登录", description = "通过微信 code 登录,已绑定用户直接返回 token未绑定用户返回 openid 需绑定手机号")
@PostMapping("/silent-login")
@Log(value = "微信小程序静默登录", module = LogModuleEnum.LOGIN)
public Result<WechatMiniappLoginResult> silentLogin(
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN)
public Result<WxMaLoginResp> silentLogin(
@Parameter(description = "微信登录凭证wx.login 获取)", required = true, example = "0xxx")
@RequestParam String code
) {
WechatMiniappLoginResult result = wechatMiniappAuthService.silentLogin(code);
WxMaLoginResp result = wxMaAuthService.silentLogin(code);
return Result.success(result);
}
@@ -60,14 +66,9 @@ public class WechatMiniappAuthController {
*/
@Operation(summary = "手机号快捷登录", description = "同时使用微信 code 和手机号授权 code 登录,适用于企业认证小程序")
@PostMapping("/phone-login")
@Log(value = "微信小程序手机号快捷登录", module = LogModuleEnum.LOGIN)
public Result<AuthenticationToken> phoneLogin(
@Parameter(description = "微信登录凭证wx.login 获取)", required = true, example = "0xxx")
@RequestParam String loginCode,
@Parameter(description = "手机号授权凭证getPhoneNumber 事件获取)", required = true, example = "0xxx")
@RequestParam String phoneCode
) {
AuthenticationToken result = wechatMiniappAuthService.phoneLogin(loginCode, phoneCode);
@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);
}
@@ -80,16 +81,9 @@ public class WechatMiniappAuthController {
*/
@Operation(summary = "绑定手机号", description = "为静默登录用户绑定手机号,绑定成功后自动登录")
@PostMapping("/bind-mobile")
@Log(value = "微信小程序绑定手机号", module = LogModuleEnum.LOGIN)
public Result<AuthenticationToken> bindMobile(
@Parameter(description = "微信用户唯一标识(静默登录返回)", required = true)
@RequestParam String openid,
@Parameter(description = "手机号码", required = true, example = "18812345678")
@RequestParam String mobile,
@Parameter(description = "短信验证码", required = true, example = "123456")
@RequestParam String smsCode
) {
AuthenticationToken result = wechatMiniappAuthService.bindMobile(openid, mobile, smsCode);
@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);
}
}

View File

@@ -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 = "用户名不能为空")
@@ -29,4 +29,3 @@ public class LoginRequest {
@Schema(description = "验证码", example = "123456")
private String captchaCode;
}

View File

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

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

View File

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

View File

@@ -1,63 +0,0 @@
package com.youlai.boot.auth.model.vo;
import com.youlai.boot.security.model.AuthenticationToken;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 微信小程序登录结果
*
* @author Ray.Hao
* @since 2.4.0
*/
@Data
@Schema(description = "微信小程序登录结果")
public class WechatMiniappLoginResult {
@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;
/**
* 创建需要绑定手机号的结果
*/
public static WechatMiniappLoginResult needBindMobile(String openid) {
WechatMiniappLoginResult result = new WechatMiniappLoginResult();
result.setIsNewUser(true);
result.setNeedBindMobile(true);
result.setOpenid(openid);
return result;
}
/**
* 创建登录成功的结果
*/
public static WechatMiniappLoginResult success(AuthenticationToken token) {
WechatMiniappLoginResult result = new WechatMiniappLoginResult();
result.setIsNewUser(false);
result.setNeedBindMobile(false);
result.setAccessToken(token.getAccessToken());
result.setRefreshToken(token.getRefreshToken());
result.setTokenType(token.getTokenType());
result.setExpiresIn(token.getExpiresIn());
return result;
}
}

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.auth.service;
import com.youlai.boot.auth.model.vo.CaptchaVO;
import com.youlai.boot.security.model.AuthenticationToken;
import com.youlai.boot.framework.captcha.model.CaptchaInfo;
import com.youlai.boot.framework.security.model.AuthenticationToken;
/**
* 认证服务接口
@@ -43,10 +43,8 @@ public interface AuthService {
/**
* 获取验证码
*
* @return 验证码
*/
CaptchaVO getCaptcha();
CaptchaInfo getCaptcha();
/**
* 刷新令牌

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.auth.service;
import com.youlai.boot.auth.model.vo.WechatMiniappLoginResult;
import com.youlai.boot.security.model.AuthenticationToken;
import com.youlai.boot.auth.model.WxMaLoginResp;
import com.youlai.boot.framework.security.model.AuthenticationToken;
/**
* 微信小程序认证服务接口
@@ -9,7 +9,7 @@ import com.youlai.boot.security.model.AuthenticationToken;
* @author Ray.Hao
* @since 2.4.0
*/
public interface WechatMiniappAuthService {
public interface WxMaAuthService {
/**
* 静默登录
@@ -21,7 +21,7 @@ public interface WechatMiniappAuthService {
* @param code 微信登录凭证wx.login 获取
* @return 登录结果成功返回 token需绑定返回 openid
*/
WechatMiniappLoginResult silentLogin(String code);
WxMaLoginResp silentLogin(String code);
/**
* 手机号快捷登录

View File

@@ -1,24 +1,16 @@
package com.youlai.boot.auth.service.impl;
import cn.binarywang.wx.miniapp.api.WxMaService;
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.vo.CaptchaVO;
import com.youlai.boot.auth.service.AuthService;
import com.youlai.boot.common.constant.RedisConstants;
import com.youlai.boot.common.enums.CaptchaTypeEnum;
import com.youlai.boot.config.property.CaptchaProperties;
import com.youlai.boot.security.model.AuthenticationToken;
import com.youlai.boot.security.model.SmsAuthenticationToken;
import com.youlai.boot.security.token.TokenManager;
import com.youlai.boot.security.util.SecurityUtils;
import com.youlai.boot.support.sms.enums.SmsTypeEnum;
import com.youlai.boot.support.sms.service.SmsService;
import com.youlai.boot.system.service.UserSocialService;
import com.youlai.boot.system.service.UserService;
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;
@@ -28,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;
@@ -47,12 +38,10 @@ 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;
/**
* 用户名密码登录
*
@@ -124,7 +113,6 @@ public class AuthServiceImpl implements AuthService {
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
AuthenticationToken authenticationToken = tokenManager.generateToken(authentication);
SecurityContextHolder.getContext().setAuthentication(authentication);
return authenticationToken;
}
@@ -143,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();
}
/**

View File

@@ -5,16 +5,16 @@ 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.vo.WechatMiniappLoginResult;
import com.youlai.boot.auth.service.WechatMiniappAuthService;
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.security.exception.NeedBindMobileException;
import com.youlai.boot.security.model.AuthenticationToken;
import com.youlai.boot.security.model.SysUserDetails;
import com.youlai.boot.security.model.WechatMiniAuthenticationToken;
import com.youlai.boot.security.token.TokenManager;
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.User;
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;
@@ -35,12 +35,12 @@ import java.util.Collections;
* 微信小程序认证服务实现
*
* @author Ray.Hao
* @since 2.4.0
* @since 4.0.0
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class WechatMiniappAuthServiceImpl implements WechatMiniappAuthService {
public class WxMaAuthServiceImpl implements WxMaAuthService {
private final WxMaService wxMaService;
private final AuthenticationManager authenticationManager;
@@ -54,16 +54,27 @@ public class WechatMiniappAuthServiceImpl implements WechatMiniappAuthService {
* 静默登录
*/
@Override
public WechatMiniappLoginResult silentLogin(String code) {
WechatMiniAuthenticationToken token = new WechatMiniAuthenticationToken(code);
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 WechatMiniappLoginResult.success(authToken);
return WxMaLoginResp.builder()
.isNewUser(false)
.needBindMobile(false)
.accessToken(authToken.getAccessToken())
.refreshToken(authToken.getRefreshToken())
.tokenType(authToken.getTokenType())
.expiresIn(authToken.getExpiresIn())
.build();
} catch (NeedBindMobileException e) {
return WechatMiniappLoginResult.needBindMobile(e.getOpenid());
return WxMaLoginResp.builder()
.isNewUser(true)
.needBindMobile(true)
.openid(e.getOpenid())
.build();
}
}
@@ -83,7 +94,7 @@ public class WechatMiniappAuthServiceImpl implements WechatMiniappAuthService {
log.info("微信小程序手机号快捷登录openid={}, mobile={}", openid, mobile);
// 3. 查询或创建用户
User user = findOrCreateUser(mobile);
SysUser user = findOrCreateUser(mobile);
// 4. 绑定微信 openid
bindWechatOpenid(user, session);
@@ -102,7 +113,7 @@ public class WechatMiniappAuthServiceImpl implements WechatMiniappAuthService {
validateSmsCode(mobile, smsCode);
// 2. 查询或创建用户
User user = findOrCreateUser(mobile);
SysUser user = findOrCreateUser(mobile);
// 3. 绑定微信 openid
userSocialService.bindOrUpdate(
@@ -148,9 +159,9 @@ public class WechatMiniappAuthServiceImpl implements WechatMiniappAuthService {
/**
* 查询或创建用户
*/
private User findOrCreateUser(String mobile) {
User user = userService.lambdaQuery()
.eq(User::getMobile, mobile)
private SysUser findOrCreateUser(String mobile) {
SysUser user = userService.lambdaQuery()
.eq(SysUser::getMobile, mobile)
.one();
if (user == null) {
@@ -167,8 +178,8 @@ public class WechatMiniappAuthServiceImpl implements WechatMiniappAuthService {
* 新用户默认分配 GUEST访问游客角色
* </p>
*/
private User createNewUser(String mobile) {
User user = new User();
private SysUser createNewUser(String mobile) {
SysUser user = new SysUser();
user.setMobile(mobile);
user.setUsername("wx_" + IdUtil.fastSimpleUUID().substring(0, 8));
user.setNickname("微信用户");
@@ -187,7 +198,7 @@ public class WechatMiniappAuthServiceImpl implements WechatMiniappAuthService {
/**
* 绑定微信 openid
*/
private void bindWechatOpenid(User user, WxMaJscode2SessionResult session) {
private void bindWechatOpenid(SysUser user, WxMaJscode2SessionResult session) {
try {
userSocialService.bindOrUpdate(
user.getId(),

View File

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

View File

@@ -1,17 +1,18 @@
package com.youlai.boot.tool.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.tool.codegen.service.CodegenService;
import com.youlai.boot.tool.codegen.model.form.GenConfigForm;
import com.youlai.boot.tool.codegen.model.query.TableQuery;
import com.youlai.boot.tool.codegen.model.vo.CodegenPreviewVO;
import com.youlai.boot.tool.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.tool.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,7 +81,6 @@ 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,
@RequestParam(value = "type", required = false, defaultValue = "ts") String type) {
@@ -91,7 +90,7 @@ public class CodegenController {
@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 = "type", required = false, defaultValue = "ts") String type) {
@@ -106,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);
}
}
}

View File

@@ -1,8 +1,8 @@
package com.youlai.boot.tool.codegen.converter;
package com.youlai.boot.codegen.converter;
import com.youlai.boot.tool.codegen.model.entity.GenTable;
import com.youlai.boot.tool.codegen.model.entity.GenTableColumn;
import com.youlai.boot.tool.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;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.tool.codegen.enums;
package com.youlai.boot.codegen.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.tool.codegen.enums;
package com.youlai.boot.codegen.enums;
import lombok.Getter;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.tool.codegen.enums;
package com.youlai.boot.codegen.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonCreator;

View File

@@ -1,11 +1,11 @@
package com.youlai.boot.tool.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.tool.codegen.model.bo.ColumnMetaData;
import com.youlai.boot.tool.codegen.model.bo.TableMetaData;
import com.youlai.boot.tool.codegen.model.query.TableQuery;
import com.youlai.boot.tool.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);
}

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.tool.codegen.mapper;
package com.youlai.boot.codegen.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.boot.tool.codegen.model.entity.GenTableColumn;
import com.youlai.boot.codegen.model.entity.GenTableColumn;
import org.apache.ibatis.annotations.Mapper;
/**

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.tool.codegen.mapper;
package com.youlai.boot.codegen.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.youlai.boot.tool.codegen.model.entity.GenTable;
import com.youlai.boot.codegen.model.entity.GenTable;
import org.apache.ibatis.annotations.Mapper;
/**

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.tool.codegen.model.entity;
package com.youlai.boot.codegen.model.entity;
import com.baomidou.mybatisplus.annotation.*;

View File

@@ -1,12 +1,12 @@
package com.youlai.boot.tool.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.tool.codegen.enums.FormTypeEnum;
import com.youlai.boot.tool.codegen.enums.QueryTypeEnum;
import com.youlai.boot.codegen.enums.FormTypeEnum;
import com.youlai.boot.codegen.enums.QueryTypeEnum;
import lombok.Getter;
import lombok.Setter;

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.tool.codegen.model.form;
package com.youlai.boot.codegen.model.form;
import com.youlai.boot.tool.codegen.enums.FormTypeEnum;
import com.youlai.boot.tool.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;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.tool.codegen.model.query;
package com.youlai.boot.codegen.model.query;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.youlai.boot.common.base.BaseQuery;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.tool.codegen.model.query;
package com.youlai.boot.codegen.model.query;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.youlai.boot.common.base.BaseQuery;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.tool.codegen.model.vo;
package com.youlai.boot.codegen.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

View File

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

View File

@@ -1,8 +1,7 @@
package com.youlai.boot.tool.codegen.model.bo;
package com.youlai.boot.codegen.model.vo;
import lombok.Data;
/**
* 数据表元数据
*
@@ -10,36 +9,18 @@ import lombok.Data;
* @since 2.10.0
*/
@Data
public class TableMetaData {
public class TableMetaVO {
/**
* 表名称
*/
private String tableName;
/**
* 表描述
*/
private String tableComment;
/**
* 排序规则
*/
private String tableCollation;
/**
* 存储引擎
*/
private String engine;
/**
* 字符集
*/
private String charset;
/**
* 创建时间
*/
private String createTime;
}

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.tool.codegen.model.vo;
package com.youlai.boot.codegen.model.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

View File

@@ -1,9 +1,9 @@
package com.youlai.boot.tool.codegen.service;
package com.youlai.boot.codegen.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.youlai.boot.tool.codegen.model.query.TableQuery;
import com.youlai.boot.tool.codegen.model.vo.CodegenPreviewVO;
import com.youlai.boot.tool.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;

View File

@@ -1,7 +1,7 @@
package com.youlai.boot.tool.codegen.service;
package com.youlai.boot.codegen.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.boot.tool.codegen.model.entity.GenTableColumn;
import com.youlai.boot.codegen.model.entity.GenTableColumn;
/**
* 代码生成配置接口

View File

@@ -1,8 +1,8 @@
package com.youlai.boot.tool.codegen.service;
package com.youlai.boot.codegen.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.youlai.boot.tool.codegen.model.entity.GenTable;
import com.youlai.boot.tool.codegen.model.form.GenConfigForm;
import com.youlai.boot.codegen.model.entity.GenTable;
import com.youlai.boot.codegen.model.form.GenConfigForm;
/**
* 代码生成配置接口

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.tool.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.tool.codegen.enums.JavaTypeEnum;
import com.youlai.boot.config.property.CodegenProperties;
import com.youlai.boot.tool.codegen.service.GenTableService;
import com.youlai.boot.tool.codegen.service.GenTableColumnService;
import com.youlai.boot.tool.codegen.service.CodegenService;
import com.youlai.boot.core.exception.BusinessException;
import com.youlai.boot.tool.codegen.mapper.DatabaseMapper;
import com.youlai.boot.tool.codegen.model.entity.GenTable;
import com.youlai.boot.tool.codegen.model.entity.GenTableColumn;
import com.youlai.boot.tool.codegen.model.query.TableQuery;
import com.youlai.boot.tool.codegen.model.vo.CodegenPreviewVO;
import com.youlai.boot.tool.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;
@@ -224,11 +224,9 @@ public class CodegenServiceImpl implements CodegenService {
} else if ("MapperXml".equals(templateName)) {
return entityName + "Mapper" + extension;
} else if ("API".equals(templateName)) {
// 生成 user.ts 命名
return StrUtil.toSymbolCase(entityName, '-') + extension;
return "index" + extension;
} else if ("API_TYPES".equals(templateName)) {
// 生成 types/api/user.ts
return StrUtil.toSymbolCase(entityName, '-') + extension;
return "types" + extension;
} else if ("VIEW".equals(templateName)) {
return "index.vue";
}
@@ -255,18 +253,18 @@ public class CodegenServiceImpl implements CodegenService {
+ File.separator + moduleName
);
} else if ("API".equals(templateName)) {
// path = "src/api/system";
path = (codegenProperties.getFrontendAppName()
+ File.separator + "src"
+ File.separator + subPackageName
+ File.separator + "api"
+ File.separator + moduleName
+ File.separator + StrUtil.toSymbolCase(entityName, '-')
);
} else if ("API_TYPES".equals(templateName)) {
// path = "src/types/api";
path = (codegenProperties.getFrontendAppName()
+ File.separator + "src"
+ File.separator + "types"
+ File.separator + "api"
+ File.separator + moduleName
+ File.separator + StrUtil.toSymbolCase(entityName, '-')
);
} else if ("VIEW".equals(templateName)) {
// path = "src/views/system/user";
@@ -325,6 +323,7 @@ public class CodegenServiceImpl implements CodegenService {
bindMap.put("entityKebab", entityKebab);
bindMap.put("entityUpperSnake", entityUpperSnake);
bindMap.put("businessName", genTable.getBusinessName());
bindMap.put("entityComment", genTable.getBusinessName());
bindMap.put("fieldConfigs", fieldConfigs);
boolean hasLocalDateTime = false;
@@ -390,7 +389,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);
}
}
@@ -421,7 +420,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);
}
}
}

View File

@@ -1,9 +1,9 @@
package com.youlai.boot.tool.codegen.service.impl;
package com.youlai.boot.codegen.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.youlai.boot.tool.codegen.mapper.GenTableColumnMapper;
import com.youlai.boot.tool.codegen.model.entity.GenTableColumn;
import com.youlai.boot.tool.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;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.tool.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.tool.codegen.enums.FormTypeEnum;
import com.youlai.boot.tool.codegen.enums.JavaTypeEnum;
import com.youlai.boot.tool.codegen.enums.QueryTypeEnum;
import com.youlai.boot.core.exception.BusinessException;
import com.youlai.boot.config.property.CodegenProperties;
import com.youlai.boot.tool.codegen.converter.CodegenConverter;
import com.youlai.boot.tool.codegen.mapper.DatabaseMapper;
import com.youlai.boot.tool.codegen.mapper.GenTableMapper;
import com.youlai.boot.tool.codegen.model.bo.ColumnMetaData;
import com.youlai.boot.tool.codegen.model.bo.TableMetaData;
import com.youlai.boot.tool.codegen.model.entity.GenTable;
import com.youlai.boot.tool.codegen.model.entity.GenTableColumn;
import com.youlai.boot.tool.codegen.model.form.GenConfigForm;
import com.youlai.boot.tool.codegen.service.GenTableService;
import com.youlai.boot.tool.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;
}

View File

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

View File

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

View File

@@ -0,0 +1,145 @@
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);
String sn = request.getHeader("X-Device-SN");
// 解析 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 != null ? userId : 99);
logEntity.setOperatorName(username != null ? username : sn);
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());
}
}
}

View File

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

View File

@@ -0,0 +1,46 @@
package com.youlai.boot.common.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
public class FilePath {
// @Value("${file.upload-dir-unix}")
private static final String unixUploadDir = "/data/uploads" ;
// @Value("${file.upload-dir-windows}")
private static final String windowsUploadDir = "uploadFile";
private static Logger logger = LoggerFactory.getLogger(FilePath.class);
public static final String TABLET_PATH = "tablet";
public static final String AVATAR_PATH = "avatar";
public static final String APK_ICON_PATH = "apkIcon";
public static final String SCREENSHOT_PATH = "screenshot";
public static String getRootPath() {
String osName = System.getProperty("os.name");
logger.info("osName: {}", osName);
if (osName.contains("Windows")) {
String projectPath = System.getProperty("user.dir");
logger.info("projectPath: {}", projectPath);
return projectPath + File.separator + windowsUploadDir;
} else {
return unixUploadDir;
}
}
public static String getAvatarPath() {
return getRootPath() + File.separator + TABLET_PATH + File.separator + AVATAR_PATH + File.separator;
}
public static String getApkIconPath() {
return getRootPath() + File.separator + TABLET_PATH + File.separator + APK_ICON_PATH + File.separator;
}
public static String getScreenshotPath() {
return getRootPath() + File.separator + TABLET_PATH + File.separator + SCREENSHOT_PATH + File.separator;
}
}

View File

@@ -0,0 +1,65 @@
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, "查询列表"),
REGISTER(16, "注册"),
VIEW(17, "查看"),
REFRESH(18, "刷新"),
SCREENSHOT(19, "截图"),
REBOOT(20, "重启"),
SHUTDOWN(21, "关机"),
LOCATE(22, "定位"),
RESTORE(23, "重置"),
DEVELOPER(24, "开发者选项"),
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;
}
}

View File

@@ -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,41 @@ 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, "代码生成"),
DEVICE(15, "设备SN管理"),
MOBILE(16, "移动设备管理"),
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;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
package com.youlai.boot.common.exception;
import lombok.Getter;
/**
* 数据权限异常
* <p>
* 当数据权限拦截器拼接SQL条件失败时抛出属于系统级异常非业务异常。
*
* @author zc
* @since 2.0.0
*/
@Getter
public class DataPermissionException extends RuntimeException {
private final String mappedStatementId;
public DataPermissionException(String mappedStatementId, String message) {
super(message);
this.mappedStatementId = mappedStatementId;
}
public DataPermissionException(String mappedStatementId, String message, Throwable cause) {
super(message, cause);
this.mappedStatementId = mappedStatementId;
}
}

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.web;
package com.youlai.boot.common.result;
import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.web;
package com.youlai.boot.common.result;
/**
* 响应码接口

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.web;
package com.youlai.boot.common.result;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.Data;

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
package com.youlai.boot.core.web;
package com.youlai.boot.common.result;
import java.io.Serializable;
@@ -99,6 +99,16 @@ public enum ResultCode implements IResultCode, Serializable {
UPLOAD_FILE_EXCEPTION("A0700", "上传文件异常"),
DELETE_FILE_EXCEPTION("A0710", "删除文件异常"),
/** A08xx移动设备认证异常 */
MOBILE_DEVICE_ID_REQUIRED("A0801", "设备标识不能为空"),
MOBILE_NONCE_REQUIRED("A0802", "随机数不能为空"),
MOBILE_TIMESTAMP_REQUIRED("A0803", "时间戳不能为空"),
MOBILE_TIMESTAMP_INVALID("A0804", "时间戳格式无效"),
MOBILE_TIMESTAMP_EXPIRED("A0805", "请求已过期,请重试"),
MOBILE_SIGN_REQUIRED("A0806", "签名不能为空"),
MOBILE_SIGN_INVALID("A0807", "签名验证失败"),
MOBILE_DEVICE_NOT_REGISTERED("A0808", "设备未注册"),
/** 一级宏观错误码:系统端错误(服务端内部异常/超时/不可用等,需后端排查修复) */
SYSTEM_ERROR("B0001", "系统执行出错"),
@@ -111,6 +121,7 @@ public enum ResultCode implements IResultCode, Serializable {
/** 二级宏观错误码:第三方服务具体错误(按号段细分,便于定位是接口不存在/数据库异常等) */
INTERFACE_NOT_EXIST("C0113", "接口不存在"),
DATABASE_SERVICE_ERROR("C0300", "数据库服务出错"),
DATABASE_EXECUTION_ERROR("C0310", "数据库执行异常"),
DATABASE_EXECUTION_SYNTAX_ERROR("C0313", "数据库执行语法错误"),
INTEGRITY_CONSTRAINT_VIOLATION("C0342", "违反了完整性约束"),
DATABASE_ACCESS_DENIED("C0351", "演示环境已禁用数据库写入功能请本地部署修改数据库链接或开启Mock模式进行体验");

View File

@@ -0,0 +1,70 @@
package com.youlai.boot.common.util;
import java.security.SecureRandom;
/**
* 验证码工具类
*/
public class CodeGeneratorUtil {
private static final SecureRandom secureRandom = new SecureRandom();
// 数字字符集
private static final String DIGITS = "0123456789";
// 字母字符集
private static final String LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
// 数字和字母组合字符集
private static final String ALPHANUMERIC = DIGITS + LETTERS;
/**
* 生成指定长度的纯数字验证码
*
* @param length 验证码长度
* @return 验证码字符串
*/
public static String generateNumericCode(int length) {
return generateCode(DIGITS, length);
}
/**
* 生成指定长度的字母验证码
*
* @param length 验证码长度
* @return 验证码字符串
*/
public static String generateLetterCode(int length) {
return generateCode(LETTERS, length);
}
/**
* 生成指定长度的字母数字混合验证码
*
* @param length 验证码长度
* @return 验证码字符串
*/
public static String generateAlphanumericCode(int length) {
return generateCode(ALPHANUMERIC, length);
}
/**
* 根据指定字符集生成验证码
*
* @param charSet 字符集
* @param length 验证码长度
* @return 验证码字符串
*/
private static String generateCode(String charSet, int length) {
if (length <= 0) {
throw new IllegalArgumentException("验证码长度必须大于0");
}
StringBuilder code = new StringBuilder(length);
for (int i = 0; i < length; i++) {
int index = secureRandom.nextInt(charSet.length());
code.append(charSet.charAt(index));
}
return code.toString();
}
}

View File

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

View File

@@ -0,0 +1,138 @@
package com.youlai.boot.common.util;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class HashUtils {
public static String calculateMultipartFileMd5(MultipartFile file) throws NoSuchAlgorithmException, IOException {
MessageDigest digest = MessageDigest.getInstance("MD5");
// 使用try-with-resources自动管理输入流
try (InputStream inputStream = file.getInputStream()) {
byte[] buffer = new byte[4096]; // 统一缓冲区大小为4KB
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
}
byte[] hashBytes = digest.digest();
return bytesToHex(hashBytes); // 复用现有工具方法
}
/**
* 计算 MultipartFile 的 SHA1 哈希值
*
* @param file 上传的文件
* @return SHA1 哈希值(小写十六进制字符串)
* @throws NoSuchAlgorithmException 算法不存在异常
* @throws IOException IO异常
*/
public static String calculateMultipartFileSha1(MultipartFile file) throws NoSuchAlgorithmException, IOException {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
try (InputStream inputStream = file.getInputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
}
byte[] hashBytes = digest.digest();
return bytesToHex(hashBytes);
}
/**
* 计算 MultipartFile 的 SHA256 哈希值
*
* @param file 上传的文件
* @return SHA256 哈希值(小写十六进制字符串)
* @throws NoSuchAlgorithmException 算法不存在异常
* @throws IOException IO异常
*/
public static String calculateMultipartFileSha256(MultipartFile file) throws NoSuchAlgorithmException, IOException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream inputStream = file.getInputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
}
byte[] hashBytes = digest.digest();
return bytesToHex(hashBytes);
}
public static String getFileMD5(File file) throws NoSuchAlgorithmException, IOException {
MessageDigest md = MessageDigest.getInstance("MD5");
FileInputStream fis = new FileInputStream(file);
byte[] dataBytes = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(dataBytes)) != -1) {
md.update(dataBytes, 0, bytesRead);
}
byte[] mdBytes = md.digest();
StringBuilder sb = new StringBuilder();
for (byte mdByte : mdBytes) {
sb.append(Integer.toString((mdByte & 0xff) + 0x100, 16).substring(1));
}
return sb.toString();
}
public static String calculateSHA1(File file) throws NoSuchAlgorithmException, IOException {
InputStream inputStream = new FileInputStream(file);
return calculateSHA1(inputStream);
}
public static String calculateSHA1(InputStream inputStream) throws NoSuchAlgorithmException, IOException {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
byte[] hashBytes = digest.digest();
return bytesToHex(hashBytes, true);
}
public static String calculateSHA256(File file) throws NoSuchAlgorithmException, IOException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream inputStream = new FileInputStream(file)) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
digest.update(buffer, 0, bytesRead);
}
}
byte[] hashBytes = digest.digest();
return bytesToHex(hashBytes);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = String.format("%02x", b & 0xFF);
/*大写*/
// String hex = String.format("%02X", b & 0xFF);
hexString.append(hex);
}
return hexString.toString();
}
private static String bytesToHex(byte[] bytes, boolean upcase) {
String format = upcase ? "%02X" : "%02x";
StringBuilder hexString = new StringBuilder();
for (byte b : bytes) {
String hex = String.format(format, b & 0xFF);
hexString.append(hex);
}
return hexString.toString();
}
}

View File

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

View File

@@ -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.support.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: 这里可以实现订阅级别的权限控制
// 例如:检查用户是否有权限订阅某个主题
}
}
}

View File

@@ -1,243 +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.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);
// 设置日志模块和内容
log.setModule(logAnnotation.module());
// 异常情况:追加异常信息到日志内容
if (e != null) {
log.setContent(logAnnotation.value() + "(失败:" + e.getMessage() + "");
// 请求参数(异常时也记录,便于排查问题)
this.setRequestParameters(joinPoint, log);
// 异常堆栈(截取前 2000 字符,避免过长)
String stackTrace = JSONUtil.toJsonStr(e.getStackTrace());
log.setResponseContent(StrUtil.sub(stackTrace, 0, 2000));
} else {
// 正常情况
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;
}
}

View File

@@ -0,0 +1,85 @@
package com.youlai.boot.device.controller;
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.common.result.ResultCode;
import com.youlai.boot.device.model.form.ContactForm;
import com.youlai.boot.device.model.vo.ContactVO;
import com.youlai.boot.device.service.ContactService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "16.移动设备管理")
@RestController
@RequestMapping("/api/v1/sn/contact")
@RequiredArgsConstructor
public class ContactController {
private final ContactService contactService;
@Operation(summary = "联系人分页列表")
@GetMapping("/list")
// @PreAuthorize("@ss.hasPerm('sys:contact:list')")
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.LIST)
public Result<List<ContactVO>> getContactList(@RequestHeader(value = "X-Device-SN") String sn) {
List<ContactVO> result = contactService.getAllContacts(sn);
return Result.success(result);
}
@Operation(summary = "新增联系人")
@PostMapping("/insert")
// @PreAuthorize("@ss.hasPerm('sys:contact:create')")
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.INSERT)
public Result<?> saveContact(
@RequestHeader(value = "X-Device-SN") String sn,
@Valid @RequestBody ContactForm contactForm
) {
Long contactId = contactService.saveContact(sn, contactForm);
if (contactId == null) {
return Result.failed(ResultCode.DUPLICATE_SUBMISSION, "该手机号已存在于当前设备的联系人中", contactForm);
}
return Result.success(contactId);
}
@Operation(summary = "获取联系人表单数据")
@GetMapping("/form")
// @PreAuthorize("@ss.hasPerm('sys:contact:query')")
public Result<ContactForm> getContactForm(
@Parameter(description = "联系人ID") @RequestParam Long id,
@RequestHeader(value = "X-Device-SN") String sn
) {
ContactForm formData = contactService.getContactForm(id, sn);
return Result.success(formData);
}
@Operation(summary = "修改联系人")
@PutMapping("/update")
// @PreAuthorize("@ss.hasPerm('sys:contact:update')")
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.UPDATE)
public Result<?> updateContact(
@Parameter(description = "联系人ID") @RequestParam Long id,
@RequestHeader(value = "X-Device-SN") String sn,
@Valid @RequestBody ContactForm contactForm
) {
return Result.judge(contactService.updateContact(id, sn, contactForm));
}
@Operation(summary = "删除联系人")
@DeleteMapping("/delete")
// @PreAuthorize("@ss.hasPerm('sys:contact:delete')")
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DELETE)
public Result<?> deleteContact(
@RequestParam Long id,
@RequestHeader(value = "X-Device-SN") String sn
) {
return Result.judge(contactService.deleteContact(id, sn));
}
}

View File

@@ -0,0 +1,150 @@
package com.youlai.boot.device.controller;
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.PageResult;
import com.youlai.boot.common.result.Result;
import com.youlai.boot.device.model.entity.SnDeviceInfo;
import com.youlai.boot.device.model.form.DeveloperForm;
import com.youlai.boot.device.model.query.DeviceQuery;
import com.youlai.boot.device.model.vo.DeviceHardwareVO;
import com.youlai.boot.device.model.vo.DevicePageVO;
import com.youlai.boot.device.service.DeviceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
/**
* 设备控制层
* @author TTSTD
* @since 2026/04/05
*/
@Tag(name = "15.SN管理")
@RestController
@RequestMapping("/api/v1/device")
@RequiredArgsConstructor
public class DeviceController {
private final DeviceService deviceService;
@Operation(summary = "SN列表")
@GetMapping
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.LIST)
public PageResult<DevicePageVO> getSnList(@Valid DeviceQuery deviceQuery) {
return PageResult.success(deviceService.getSnPage(deviceQuery));
}
@Operation(summary = "获取SN绑定激活信息")
@GetMapping("/{sn}/info")
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.VIEW)
// @PreAuthorize("@ss.hasPerm('sys:sn:view')")
public Result<DevicePageVO> getSnBindInfo(@PathVariable String sn) {
DevicePageVO detail = deviceService.getSnBindInfo(sn);
return Result.success(detail);
}
@Operation(summary = "获取SN基础信息")
@GetMapping("/{sn}/hardware_info")
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.VIEW)
// @PreAuthorize("@ss.hasPerm('sys:sn:view')")
public Result<DeviceHardwareVO> getSnHardwareInfo(@PathVariable String sn) {
DeviceHardwareVO info = deviceService.getSnHardwareInfo(sn);
return Result.success(info);
}
@Operation(summary = "新增SN")
@PostMapping("/add")
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.INSERT)
// @PreAuthorize("@ss.hasPerm('sys:sn:create')")
public Result<Void> addSn(@RequestBody SnDeviceInfo snDeviceInfo) {
deviceService.addSn(snDeviceInfo);
return Result.success();
}
@Operation(summary = "删除SN")
@DeleteMapping("/{sn}")
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.DELETE)
@PreAuthorize("@ss.hasPerm('sys:sn:delete')")
public Result<Void> deleteSn(@PathVariable String sn) {
deviceService.deleteSn(sn);
return Result.success();
}
@Operation(summary = "设备刷新")
@PostMapping("/refresh")
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.REFRESH)
public Result<?> devicerefresh(@RequestParam String sn) {
boolean result = deviceService.deviceRefresh(sn);
return Result.judge(result);
}
@Operation(summary = "设备截图")
@PostMapping("/screenshot")
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.SCREENSHOT)
public Result<?> screenSnapshot(@RequestParam String sn) {
boolean result = deviceService.screenSnapshot(sn);
return Result.judge(result);
}
@Operation(summary = "设备重启")
@PostMapping("/reboot")
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.REBOOT)
public Result<?> reboot(@RequestParam String sn) {
boolean result = deviceService.deviceReboot(sn);
return Result.judge(result);
}
@Operation(summary = "设备关机")
@PostMapping("/shutdown")
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.SHUTDOWN)
public Result<?> shutdown(@RequestParam String sn) {
boolean result = deviceService.deviceShutdown(sn);
return Result.judge(result);
}
@Operation(summary = "设备定位")
@PostMapping("/locate")
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.LOCATE)
public Result<?> deviceLocate(@RequestParam String sn) {
boolean result = deviceService.deviceLocate(sn);
return Result.judge(result);
}
@Operation(summary = "设备重置")
@PostMapping("/restore")
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.RESTORE)
public Result<?> deviceRestore(@RequestParam String sn) {
boolean result = deviceService.restore(sn);
return Result.judge(result);
}
@Operation(summary = "开发者模式")
@PostMapping("/developer")
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.DEVELOPER)
public Result<?> deviceDeveloper(@RequestParam String sn) {
boolean result = deviceService.setDeviceDeveloper(sn);
return Result.judge(result);
}
@Operation(summary = "新增开发者选项配置")
@PostMapping("/developer/config")
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.INSERT)
public Result<Void> addDeveloperConfig(@Valid @RequestBody DeveloperForm developerForm) {
boolean result = deviceService.addDeveloperConfig(
developerForm.getSn(),
developerForm.getDeveloperOptions()
);
return Result.judge(result);
}
@Operation(summary = "删除开发者选项配置")
@DeleteMapping("/developer/config")
@Log(module = LogModuleEnum.DEVICE, value = ActionTypeEnum.DELETE)
public Result<Void> deleteDeveloperConfig(@RequestParam String sn) {
boolean result = deviceService.deleteDeveloperConfig(sn);
return Result.judge(result);
}
}

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