Compare commits
13 Commits
9993302d5c
...
33fbee9a00
| Author | SHA1 | Date | |
|---|---|---|---|
| 33fbee9a00 | |||
|
|
06857f6c88 | ||
|
|
d6ce39639e | ||
|
|
1e789e0c46 | ||
|
|
eebb287095 | ||
|
|
65333b9e8e | ||
|
|
481152ee1f | ||
|
|
13079f3d13 | ||
|
|
7a43e9c38c | ||
|
|
b0422ea695 | ||
|
|
c818cad89b | ||
|
|
eefcaf10c2 | ||
|
|
63c34a4218 |
@@ -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
@@ -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
|
||||
265
README.md
@@ -1,172 +1,197 @@
|
||||
<p align="center">
|
||||
<img alt="youlai-boot" width="120" src="https://foruda.gitee.com/images/1733417239320800627/3c5290fe_716974.png">
|
||||
</p>
|
||||
<div align="center">
|
||||
|
||||
<h1 align="center">youlai-boot</h1>
|
||||
<img alt="youlai-boot" width="80" src="./docs/images/logo/logo.png">
|
||||
|
||||
# youlai-boot
|
||||
|
||||
**Spring Boot 4 企业级权限管理系统后端**
|
||||
|
||||
[](https://spring.io/projects/spring-boot)
|
||||
[](https://openjdk.org/)
|
||||
[](LICENSE)
|
||||
[](https://gitee.com/youlaiorg/youlai-boot/stargazers)
|
||||
[](https://github.com/youlaitech/youlai-boot)
|
||||
[](https://gitcode.com/youlai/youlai-boot/stargazers)
|
||||
|
||||
</div>
|
||||
|
||||

|
||||
|
||||
<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">
|
||||
<strong>Spring Boot 4 企业级权限管理系统后端</strong>
|
||||
<img alt="youlai-boot 技术架构图" width="900" src="./docs/images/architecture/architecture.png">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.youlai.tech/docs/admin/backend/java/"><img src="https://img.shields.io/badge/文档-youlai.tech-blue?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2Ij48cGF0aCBmaWxsPSIjMzc4M2E0IiBkPSJNMjQ4IDExMUwzMSAxNDljMTQuNSA0LjkgMjkuNiAyMi41IDQ0LjIgMS44IDguNyAzLjEgMTcuNCAxIDEyLjhjLTIuOSA3LjItNi43IDEzLjUtMTIuOCAxNy40eiIvPjwvc3ZnPg==" alt="Documentation"></a>
|
||||
<a href="https://vue.youlai.tech"><img src="https://img.shields.io/badge/在线预览-vue.youlai.tech-10B981?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTYgMjU2Ij48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMjQ4IDExMUwzMSAxNDljMTQuNSA0LjkgMjkuNiAyMi41IDQ0LjIgMS44IDguNyAzLjEgMTcuNCAxIDEyLjhjLTIuOSA3LjItNi43IDEzLjUtMTIuOCAxNy40eiIvPjwvc3ZnPg==" alt="Demo"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue?style=flat-square"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://gitee.com/youlaiorg/youlai-boot/stargazers"><img src="https://gitee.com/youlaiorg/youlai-boot/badge/star.svg?style=flat-square"></a>
|
||||
<a href="https://github.com/haoxianrui/youlai-boot"><img src="https://img.shields.io/github/stars/haoxianrui/youlai-boot?style=social&label=Star"></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
> [English](#) | 简体中文
|
||||
|
||||
---
|
||||
|
||||
## 🎯 项目定位
|
||||
|
||||
一套 **Spring Boot 4 后端权限管理系统**,配套前端 [vue3-element-admin](https://gitee.com/youlaiorg/vue3-element-admin),并提供 **6 种语言实现**(Java / Node.js / Go / Python / PHP / C#),共享同一套 API 规范与数据库结构。
|
||||
|
||||
**适合场景**:企业中后台管理系统的后端学习参考、二次开发基础脚手架。
|
||||
|
||||
---
|
||||
|
||||
## ✨ 核心能力
|
||||
|
||||
| 能力 | 说明 |
|
||||
|------|------|
|
||||
| 🔐 **安全体系** | Spring Security + JWT + Redis 多端互斥、令牌续期、验证码防刷 |
|
||||
| 🛡️ **细粒度权限** | RBAC 五级:数据权限 → 菜单 → 按钮 → 接口 → 字段级 |
|
||||
| ⚡ **代码生成器** | 可视化配置表单,一键生成 Entity/VO/Controller/Service/CRUD 前后端代码 |
|
||||
| 📦 **模块齐全** | 用户、角色、菜单、部门、字典、文件、定时任务、消息中心、操作日志 |
|
||||
| 🌐 **多租户 SaaS** | 数据隔离 + 租户配置,独立 [youlai-boot-tenant](https://gitee.com/youlaiorg/youlai-boot-tenant) 版本 |
|
||||
| 🔌 **实时通信** | 内置 SSE 推送服务(在线用户数、字典同步、通知广播) |
|
||||
| 📱 **生态完整** | 配套移动端 [youlai-app](https://gitee.com/youlaiorg/youlai-app)(UniApp)+ 完整[技术文档](https://www.youlai.tech/docs/admin/) |
|
||||
|
||||
## 📸 系统预览
|
||||
## 系统预览
|
||||
|
||||
**PC 端**
|
||||
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td><img alt="系统预览1" width="400" src="https://www.youlai.tech/storage/blog/2026/04/12/admin_preview.jpeg"></td>
|
||||
<td><img alt="系统预览2" width="400" src="https://www.youlai.tech/storage/blog/2026/04/12/441_1x_shots_so.jpeg"></td>
|
||||
<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>
|
||||
|
||||
**移动端**
|
||||
|
||||
## 🚀 快速开始
|
||||
<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+ / 5.7+ |
|
||||
| Redis | 6.0+ |
|
||||
**环境要求**:JDK 17+ · MySQL 8.0+ · Redis 6.0+
|
||||
|
||||
### 本地启动
|
||||
|
||||
```bash
|
||||
# 1. 克隆项目
|
||||
git clone https://gitee.com/youlaiorg/youlai-boot.git
|
||||
|
||||
# 2. 导入数据库脚本 sql/mysql/youlai_admin.sql
|
||||
|
||||
# 3. 修改 application-dev.yml 配置 MySQL 和 Redis 连接信息
|
||||
# 💡 默认已配置线上只读数据源,可直接启动体验
|
||||
|
||||
# 4. 运行 YouLaiBootApplication.java,访问 http://localhost:8000/doc.html
|
||||
```
|
||||
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 部署
|
||||
**Docker 部署**:`cd deploy/docker`,然后 `docker-compose up -d`
|
||||
|
||||
```bash
|
||||
cd docker && docker-compose up -d
|
||||
```
|
||||
详细指南:[部署文档](https://www.youlai.tech/docs/server/spring-boot/deploy) · [开发规范](https://www.youlai.tech/docs/server/spring-boot/dev-standards)
|
||||
|
||||
详细指南:[部署文档](https://www.youlai.tech/docs/admin/backends/java/deploy) · [开发规范](https://www.youlai.tech/docs/admin/backends/java/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/
|
||||
├── docker/ # Docker 部署编排
|
||||
├── deploy/
|
||||
│ └── docker/ # Docker 部署编排
|
||||
├── docs/ # 项目文档与图片资源
|
||||
├── sql/ # 数据库初始化脚本
|
||||
├── src/main/java/com/youlai/boot/
|
||||
│ ├── YouLaiBootApplication # 启动类
|
||||
│ ├── YouLaiBootApplication.java # 启动类
|
||||
│ ├── auth/ # 认证授权(登录/登出/令牌)
|
||||
│ ├── codegen/ # 代码生成器
|
||||
│ ├── common/ # 公共模块(常量/枚举/统一响应)
|
||||
│ ├── file/ # 文件服务(MinIO/本地存储)
|
||||
│ ├── framework/ # 技术基座
|
||||
│ │ ├── apidoc/ # OpenAPI/Swagger
|
||||
│ │ ├── cache/ # Redis/Caffeine 缓存
|
||||
│ ├── file/ # 文件服务(MinIO/本地/OSS)
|
||||
│ ├── framework/ # 技术框架层
|
||||
│ │ ├── apidoc/ # OpenAPI / Knife4j
|
||||
│ │ ├── cache/ # Redis / Caffeine 缓存
|
||||
│ │ ├── captcha/ # 图形验证码
|
||||
│ │ ├── integration/ # 短信/邮件/微信
|
||||
│ │ ├── integration/ # 短信 / 邮件 / 微信
|
||||
│ │ ├── job/ # XXL-Job 定时任务
|
||||
│ │ ├── mybatis/ # MyBatis Plus 配置
|
||||
│ │ ├── security/ # 安全过滤器/Token机制
|
||||
│ │ └── web/ # 全局异常/跨域/限流
|
||||
│ │ ├── mybatis/ # MyBatis-Plus 配置
|
||||
│ │ ├── security/ # Security / JWT / Token
|
||||
│ │ └── web/ # 全局异常 / 跨域 / 限流
|
||||
│ ├── message/ # SSE 消息推送
|
||||
│ └── system/ # 业务模块(用户/角色/菜单/部门)
|
||||
└── pom.xml # Maven 依赖
|
||||
│ └── system/ # 系统业务(用户/角色/菜单/部门)
|
||||
└── pom.xml # Maven 依赖管理
|
||||
```
|
||||
|
||||
## 🌐 相关生态
|
||||
## 生态矩阵
|
||||
|
||||
| 项目 | 技术栈 | 定位 |
|
||||
|------|--------|------|
|
||||
| [**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-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-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# 后端** |
|
||||
**前端**
|
||||
|
||||
| 项目 | 技术栈 | 说明 |
|
||||
|:-----|:-------|:-----|
|
||||
| [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 规范** 和 **数据库结构**,前端可无缝切换。
|
||||
|
||||
## 📘 文档资源
|
||||
## 文档资源
|
||||
|
||||
| 资源 | 地址 |
|
||||
|------|------|
|
||||
| **📖 完整文档站** | [docs.youlai.tech](https://www.youlai.tech/docs/admin/) |
|
||||
| **🖥️ 在线预览(前端)** | [vue.youlai.tech](https://vue.youlai.tech) |
|
||||
| **📱 在线预览(移动端)** | [app.youlai.tech](https://app.youlai.tech) |
|
||||
| **🔗 接口文档** | 启动后访问 [http://localhost:8000/doc.html](http://localhost:8000/doc.html) |
|
||||
|:-----|:-----|
|
||||
| 📖 完整文档站 | [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、PR 和 Star!详见 [贡献指南](https://www.youlai.tech/docs/admin/faq/help)。
|
||||
欢迎提交 Issue 和 Pull Request!详见 [贡献指南](https://www.youlai.tech/faq/help)。
|
||||
|
||||
[](https://github.com/haoxianrui/youlai-boot/graphs/contributors)
|
||||
|
||||
## 📄 开源协议
|
||||
## 开源协议
|
||||
|
||||
本项目基于 [Apache License 2.0](LICENSE) 开源,可免费用于商业项目。
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="./docs/images/qr/wechat-offical.png" height="180" alt="公众号「有来技术」"><br>
|
||||
<sub>公众号「有来技术」</sub>
|
||||
</td>
|
||||
<td> </td>
|
||||
<td align="center">
|
||||
<img src="./docs/images/qr/wechat-mp.jpg" height="180" alt="小程序「有来技术」"><br>
|
||||
<sub>小程序「有来技术」</sub>
|
||||
</td>
|
||||
<td> </td>
|
||||
<td align="center">
|
||||
<img src="./docs/images/qr/wechat-personal.png" height="180" alt="添加作者微信"><br>
|
||||
<sub>添加作者微信</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
**关注「有来技术」,获取最新动态与技术分享**
|
||||
|
||||
<br>
|
||||
|
||||
<img src="https://foruda.gitee.com/images/1737108820762592766/3390ed0d_716974.png" width="220">
|
||||
|
||||
<br>
|
||||
|
||||
*微信搜索「有来技术」或扫码关注*
|
||||
|
||||
</div>
|
||||
<p align="center"><em>技术交流 · 问题反馈 · 商务合作</em></p>
|
||||
|
||||
@@ -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
BIN
docs/images/architecture/architecture.png
Normal file
|
After Width: | Height: | Size: 748 KiB |
BIN
docs/images/logo/logo.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
docs/images/preview/app-01.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
docs/images/preview/app-02.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/images/preview/app-03.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/images/preview/app-04.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/images/preview/pc-01.png
Normal file
|
After Width: | Height: | Size: 487 KiB |
BIN
docs/images/preview/pc-02.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
docs/images/preview/pc-03.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
docs/images/preview/pc-04.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
docs/images/preview/pc-05.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
docs/images/preview/pc-06.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
docs/images/qr/wechat-mp.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
docs/images/qr/wechat-offical.png
Normal file
|
After Width: | Height: | Size: 4.1 MiB |
BIN
docs/images/qr/wechat-personal.png
Normal file
|
After Width: | Height: | Size: 306 KiB |
22
pom.xml
@@ -12,7 +12,7 @@
|
||||
<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>
|
||||
|
||||
@@ -117,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>
|
||||
@@ -283,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>
|
||||
|
||||
@@ -1,524 +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', '18888888888', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (3, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345679', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (4, 'dept_manager', '部门主管', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345680', 1, 'manager@youlaitech.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (5, 'dept_member', '部门成员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345681', 1, 'member@youlaitech.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (6, 'employee', '普通员工', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 2, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345682', 1, 'employee@youlaitech.com', now(), NULL, now(), NULL, 0);
|
||||
INSERT INTO `sys_user` VALUES (7, 'custom_user', '自定义权限用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345683', 1, 'custom@youlaitech.com', now(), NULL, now(), NULL, 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_user_role
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_user_role`;
|
||||
CREATE TABLE `sys_user_role` (
|
||||
`user_id` bigint NOT NULL COMMENT '用户ID',
|
||||
`role_id` bigint NOT NULL COMMENT '角色ID',
|
||||
PRIMARY KEY (`user_id`, `role_id`) USING BTREE
|
||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '用户角色关联表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Records of sys_user_role
|
||||
-- ----------------------------
|
||||
INSERT INTO `sys_user_role` VALUES (1, 1);
|
||||
INSERT INTO `sys_user_role` VALUES (2, 2);
|
||||
INSERT INTO `sys_user_role` VALUES (3, 3);
|
||||
INSERT IGNORE INTO `sys_user_role` VALUES (4, 4);
|
||||
INSERT IGNORE INTO `sys_user_role` VALUES (5, 5);
|
||||
INSERT IGNORE INTO `sys_user_role` VALUES (6, 6);
|
||||
INSERT IGNORE INTO `sys_user_role` VALUES (7, 7);
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for sys_log
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_log`;
|
||||
CREATE TABLE `sys_log` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`module` TINYINT NOT NULL COMMENT '模块,数字枚举,参考 LogModule 枚举',
|
||||
`action_type` TINYINT NOT NULL COMMENT '操作类型,数字枚举,参考 ActionType 枚举',
|
||||
`title` VARCHAR(100) NOT NULL COMMENT '前端显示标题',
|
||||
`content` TEXT COMMENT '自定义日志内容',
|
||||
`operator_id` BIGINT COMMENT '操作人ID',
|
||||
`operator_name` VARCHAR(50) COMMENT '操作人名称',
|
||||
`request_uri` VARCHAR(255) COMMENT '请求路径',
|
||||
`request_method` VARCHAR(10) COMMENT '请求方法',
|
||||
`ip` VARCHAR(45) COMMENT 'IP地址',
|
||||
`province` VARCHAR(100) COMMENT '省份',
|
||||
`city` VARCHAR(100) COMMENT '城市',
|
||||
`device` VARCHAR(100) COMMENT '设备',
|
||||
`os` VARCHAR(100) COMMENT '操作系统',
|
||||
`browser` VARCHAR(100) COMMENT '浏览器',
|
||||
`status` TINYINT DEFAULT 1 COMMENT '0失败 1成功',
|
||||
`error_msg` VARCHAR(255) COMMENT '错误信息',
|
||||
`execution_time` INT COMMENT '执行时间(ms)',
|
||||
`create_time` DATETIME COMMENT '操作时间',
|
||||
PRIMARY KEY (`id`) USING BTREE,
|
||||
KEY `idx_module_action_time` (`module`, `action_type`, `create_time`),
|
||||
KEY `idx_operator_time` (`operator_id`, `create_time`),
|
||||
KEY `idx_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for gen_table
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `gen_table`;
|
||||
CREATE TABLE `gen_table` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`table_name` varchar(100) NOT NULL COMMENT '表名',
|
||||
`module_name` varchar(100) COMMENT '模块名',
|
||||
`package_name` varchar(255) NOT NULL COMMENT '包名',
|
||||
`business_name` varchar(100) NOT NULL COMMENT '业务名',
|
||||
`entity_name` varchar(100) NOT NULL COMMENT '实体类名',
|
||||
`author` varchar(50) NOT NULL COMMENT '作者',
|
||||
`parent_menu_id` bigint COMMENT '上级菜单ID,对应sys_menu的id ',
|
||||
`remove_table_prefix` varchar(20) COMMENT '要移除的表前缀,如: sys_',
|
||||
`page_type` varchar(20) COMMENT '页面类型(classic|curd)',
|
||||
`create_time` datetime COMMENT '创建时间',
|
||||
`update_time` datetime COMMENT '更新时间',
|
||||
`is_deleted` tinyint(4) DEFAULT 0 COMMENT '是否删除',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tablename` (`table_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成配置表';
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for gen_table_column
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `gen_table_column`;
|
||||
CREATE TABLE `gen_table_column` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`table_id` bigint NOT NULL COMMENT '关联的表配置ID',
|
||||
`column_name` varchar(100) ,
|
||||
`column_type` varchar(50) ,
|
||||
`column_length` int ,
|
||||
`field_name` varchar(100) NOT NULL COMMENT '字段名称',
|
||||
`field_type` varchar(100) COMMENT '字段类型',
|
||||
`field_sort` int COMMENT '字段排序',
|
||||
`field_comment` varchar(255) COMMENT '字段描述',
|
||||
`max_length` int ,
|
||||
`is_required` tinyint(1) COMMENT '是否必填',
|
||||
`is_show_in_list` tinyint(1) DEFAULT '0' COMMENT '是否在列表显示',
|
||||
`is_show_in_form` tinyint(1) DEFAULT '0' COMMENT '是否在表单显示',
|
||||
`is_show_in_query` tinyint(1) DEFAULT '0' COMMENT '是否在查询条件显示',
|
||||
`query_type` tinyint COMMENT '查询方式',
|
||||
`form_type` tinyint COMMENT '表单类型',
|
||||
`dict_type` varchar(50) COMMENT '字典类型',
|
||||
`create_time` datetime COMMENT '创建时间',
|
||||
`update_time` datetime COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_table_id` (`table_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成字段配置表';
|
||||
|
||||
-- ----------------------------
|
||||
-- 系统配置表
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_config`;
|
||||
CREATE TABLE `sys_config` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`config_name` varchar(50) NOT NULL COMMENT '配置名称',
|
||||
`config_key` varchar(50) NOT NULL COMMENT '配置key',
|
||||
`config_value` varchar(100) NOT NULL COMMENT '配置值',
|
||||
`remark` varchar(255) COMMENT '备注',
|
||||
`create_time` datetime COMMENT '创建时间',
|
||||
`create_by` bigint COMMENT '创建人ID',
|
||||
`update_time` datetime COMMENT '更新时间',
|
||||
`update_by` bigint COMMENT '更新人ID',
|
||||
`is_deleted` tinyint(4) DEFAULT '0' NOT NULL COMMENT '逻辑删除标识(0-未删除 1-已删除)',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB COMMENT='系统配置表';
|
||||
|
||||
INSERT INTO `sys_config` VALUES (1, '系统限流QPS', 'IP_QPS_THRESHOLD_LIMIT', '10', '单个IP请求的最大每秒查询数(QPS)阈值Key', now(), 1, NULL, NULL, 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- 通知公告表
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_notice`;
|
||||
CREATE TABLE `sys_notice` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT,
|
||||
`title` varchar(50) COMMENT '通知标题',
|
||||
`content` text COMMENT '通知内容',
|
||||
`type` tinyint NOT NULL COMMENT '通知类型(关联字典编码:notice_type)',
|
||||
`level` varchar(5) NOT NULL COMMENT '通知等级(字典code:notice_level)',
|
||||
`target_type` tinyint NOT NULL COMMENT '目标类型(1: 全体, 2: 指定)',
|
||||
`target_user_ids` varchar(255) COMMENT '目标人ID集合(多个使用英文逗号,分割)',
|
||||
`publisher_id` bigint COMMENT '发布人ID',
|
||||
`publish_status` tinyint DEFAULT '0' COMMENT '发布状态(0: 未发布, 1: 已发布, -1: 已撤回)',
|
||||
`publish_time` datetime COMMENT '发布时间',
|
||||
`revoke_time` datetime COMMENT '撤回时间',
|
||||
`create_by` bigint NOT NULL COMMENT '创建人ID',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
`update_by` bigint COMMENT '更新人ID',
|
||||
`update_time` datetime COMMENT '更新时间',
|
||||
`is_deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除(0: 未删除, 1: 已删除)',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统通知公告表';
|
||||
|
||||
INSERT INTO `sys_notice` VALUES (1, 'v3.0.0 版本发布 - 多租户功能上线', '<p>🎉 新版本发布,主要更新内容:</p><p>1. 新增多租户功能,支持租户隔离和数据管理</p><p>2. 优化系统性能,提升响应速度</p><p>3. 完善权限管理,增强安全性</p><p>4. 修复已知问题,提升系统稳定性</p>', 1, 'H', 1, NULL, 1, 1, '2024-12-15 10:00:00', NULL, 1, '2024-12-15 10:00:00', 1, '2024-12-15 10:00:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (2, '系统维护通知 - 2024年12月20日', '<p>⏰ 系统维护通知</p><p>系统将于 <strong>2024年12月20日(本周五)凌晨 2:00-4:00</strong> 进行例行维护升级。</p><p>维护期间系统将暂停服务,请提前做好数据备份工作。</p><p>给您带来的不便,敬请谅解!</p>', 2, 'H', 1, NULL, 1, 1, '2024-12-18 14:30:00', NULL, 1, '2024-12-18 14:30:00', 1, '2024-12-18 14:30:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (3, '安全提醒 - 防范钓鱼邮件', '<p>⚠️ 安全提醒</p><p>近期发现有不法分子通过钓鱼邮件进行网络攻击,请大家提高警惕:</p><p>1. 不要点击来源不明的邮件链接</p><p>2. 不要下载可疑附件</p><p>3. 遇到可疑邮件请及时联系IT部门</p><p>4. 定期修改密码,使用强密码策略</p>', 3, 'H', 1, NULL, 1, 1, '2024-12-10 09:00:00', NULL, 1, '2024-12-10 09:00:00', 1, '2024-12-10 09:00:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (4, '元旦假期安排通知', '<p>📅 元旦假期安排</p><p>根据国家法定节假日安排,公司元旦假期时间为:</p><p><strong>2024年12月30日(周一)至 2025年1月1日(周三)</strong>,共3天。</p><p>2024年12月29日(周日)正常上班。</p><p>祝大家元旦快乐,假期愉快!</p>', 4, 'M', 1, NULL, 1, 1, '2024-12-25 16:00:00', NULL, 1, '2024-12-25 16:00:00', 1, '2024-12-25 16:00:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (5, '新产品发布会邀请', '<p>🎊 新产品发布会邀请</p><p>公司将于 <strong>2025年1月15日下午14:00</strong> 在总部会议室举办新产品发布会。</p><p>届时将展示最新研发的产品和技术成果,欢迎全体员工参加。</p><p>请各部门提前安排好工作,准时参加。</p>', 5, 'M', 1, NULL, 1, 1, '2024-12-28 11:00:00', NULL, 1, '2024-12-28 11:00:00', 1, '2024-12-28 11:00:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (6, 'v2.16.1 版本更新', '<p>✨ 版本更新</p><p>v2.16.1 版本已发布,主要修复内容:</p><p>1. 修复 WebSocket 重复连接导致的后台线程阻塞问题</p><p>2. 优化通知公告功能,提升用户体验</p><p>3. 修复部分已知bug</p><p>建议尽快更新到最新版本。</p>', 1, 'M', 1, NULL, 1, 1, '2024-12-05 15:30:00', NULL, 1, '2024-12-05 15:30:00', 1, '2024-12-05 15:30:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (7, '年终总结会议通知', '<p>📋 年终总结会议通知</p><p>各部门年终总结会议将于 <strong>2024年12月30日上午9:00</strong> 召开。</p><p>请各部门负责人提前准备好年度工作总结和下年度工作计划。</p><p>会议地点:总部大会议室</p>', 5, 'M', 2, '1,2', 1, 1, '2024-12-22 10:00:00', NULL, 1, '2024-12-22 10:00:00', 1, '2024-12-22 10:00:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (8, '系统功能优化完成', '<p>✅ 系统功能优化</p><p>已完成以下功能优化:</p><p>1. 优化用户管理界面,提升操作体验</p><p>2. 增强数据导出功能,支持更多格式</p><p>3. 优化搜索功能,提升查询效率</p><p>4. 修复部分界面显示问题</p>', 1, 'L', 1, NULL, 1, 1, '2024-12-12 14:20:00', NULL, 1, '2024-12-12 14:20:00', 1, '2024-12-12 14:20:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (9, '员工培训计划', '<p>📚 员工培训计划</p><p>为提升员工专业技能,公司将于 <strong>2025年1月8日-10日</strong> 组织技术培训。</p><p>培训内容:</p><p>1. 新技术框架应用</p><p>2. 代码规范与最佳实践</p><p>3. 系统架构设计</p><p>请各部门合理安排工作,确保培训顺利进行。</p>', 5, 'M', 1, NULL, 1, 1, '2024-12-20 09:30:00', NULL, 1, '2024-12-20 09:30:00', 1, '2024-12-20 09:30:00', 0);
|
||||
INSERT INTO `sys_notice` VALUES (10, '数据备份提醒', '<p>💾 数据备份提醒</p><p>请各部门注意定期备份重要数据,建议每周至少备份一次。</p><p>备份方式:</p><p>1. 使用系统自带备份功能</p><p>2. 手动导出重要数据</p><p>3. 联系IT部门协助备份</p><p>数据安全,人人有责!</p>', 3, 'L', 1, NULL, 1, 1, '2024-12-08 08:00:00', NULL, 1, '2024-12-08 08:00:00', 1, '2024-12-08 08:00:00', 0);
|
||||
|
||||
-- ----------------------------
|
||||
-- 用户通知公告表
|
||||
-- ----------------------------
|
||||
DROP TABLE IF EXISTS `sys_user_notice`;
|
||||
CREATE TABLE `sys_user_notice` (
|
||||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
|
||||
`notice_id` bigint NOT NULL COMMENT '公共通知id',
|
||||
`user_id` bigint NOT NULL COMMENT '用户id',
|
||||
`is_read` bigint DEFAULT '0' COMMENT '读取状态(0: 未读, 1: 已读)',
|
||||
`read_time` datetime COMMENT '阅读时间',
|
||||
`create_time` datetime NOT NULL COMMENT '创建时间',
|
||||
`update_time` datetime COMMENT '更新时间',
|
||||
`is_deleted` tinyint DEFAULT '0' COMMENT '逻辑删除(0: 未删除, 1: 已删除)',
|
||||
PRIMARY KEY (`id`) USING BTREE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户通知公告关联表';
|
||||
|
||||
INSERT INTO `sys_user_notice` VALUES (1, 1, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (2, 2, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (3, 3, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (4, 4, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (5, 5, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (6, 6, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (7, 7, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (8, 8, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (9, 9, 2, 1, NULL, now(), now(), 0);
|
||||
INSERT INTO `sys_user_notice` VALUES (10, 10, 2, 1, NULL, now(), now(), 0);
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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/auth/app")
|
||||
@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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
84
src/main/java/com/youlai/boot/app/mapper/AppUserMapper.java
Normal 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);
|
||||
|
||||
}
|
||||
65
src/main/java/com/youlai/boot/app/model/entity/AppUser.java
Normal 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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
198
src/main/java/com/youlai/boot/app/service/AppUserService.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
@@ -95,6 +95,7 @@ public class LogAspect {
|
||||
// 解析 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);
|
||||
@@ -121,8 +122,8 @@ public class LogAspect {
|
||||
logEntity.setActionType(actionType);
|
||||
logEntity.setTitle(title);
|
||||
logEntity.setContent(content);
|
||||
logEntity.setOperatorId(userId);
|
||||
logEntity.setOperatorName(username);
|
||||
logEntity.setOperatorId(userId != null ? userId : 99);
|
||||
logEntity.setOperatorName(username != null ? username : sn);
|
||||
logEntity.setRequestUri(request.getRequestURI());
|
||||
logEntity.setRequestMethod(request.getMethod());
|
||||
logEntity.setIp(ip);
|
||||
|
||||
46
src/main/java/com/youlai/boot/common/config/FilePath.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,15 @@ public enum ActionTypeEnum implements IBaseEnum<Integer> {
|
||||
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
|
||||
|
||||
@@ -27,6 +27,8 @@ public enum LogModuleEnum implements IBaseEnum<Integer> {
|
||||
NOTICE(9, "通知公告"),
|
||||
LOG(10, "日志管理"),
|
||||
CODEGEN(11, "代码生成"),
|
||||
DEVICE(15, "设备SN管理"),
|
||||
MOBILE(16, "移动设备管理"),
|
||||
OTHER(99, "其他");
|
||||
|
||||
@EnumValue
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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模式进行体验");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
138
src/main/java/com/youlai/boot/common/util/HashUtils.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
package com.youlai.boot.device.controller;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.youlai.boot.common.annotation.Log;
|
||||
import com.youlai.boot.common.config.FilePath;
|
||||
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.util.HashUtils;
|
||||
import com.youlai.boot.device.model.entity.SnDeviceHardware;
|
||||
import com.youlai.boot.device.model.req.ApkInstallInfoReq;
|
||||
import com.youlai.boot.device.model.req.SnHardwareInfoReq;
|
||||
import com.youlai.boot.device.model.req.SnLocationReq;
|
||||
import com.youlai.boot.device.model.entity.SnDeveloper;
|
||||
import com.youlai.boot.device.model.entity.SnLocation;
|
||||
import com.youlai.boot.device.model.entity.SnScreenshot;
|
||||
import com.youlai.boot.device.model.vo.DeveloperOptionsVO;
|
||||
import com.youlai.boot.device.service.*;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 设备控制层
|
||||
* @author TTSTD
|
||||
* @since 2026/04/05
|
||||
*/
|
||||
@Tag(name = "16.移动设备管理")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/sn")
|
||||
@RequiredArgsConstructor
|
||||
public class MobileController {
|
||||
private static final String DEVICE_SECRET_PREFIX = "device:secret:";
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final DeviceService deviceService;
|
||||
private final ScreenshotService screenshotService;
|
||||
private final LocationService locationService;
|
||||
private final DeveloperService developerService;
|
||||
private final ApkInstallService apkInstallService;
|
||||
private final HardwareService hardwareService;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MobileController.class);
|
||||
|
||||
|
||||
@Operation(summary = "注册设备")
|
||||
@PostMapping("/register")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.REGISTER)
|
||||
public Map<String, Object> registerDevice(@RequestParam() String sn) {
|
||||
|
||||
// 生成设备密钥
|
||||
String deviceSecret = IdUtil.fastSimpleUUID();
|
||||
|
||||
// 存储到Redis(可根据需要设置过期时间)
|
||||
redisTemplate.opsForValue().set(DEVICE_SECRET_PREFIX + sn, deviceSecret, 365, TimeUnit.DAYS);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("deviceId", sn);
|
||||
result.put("deviceSecret", deviceSecret);
|
||||
result.put("message", "请妥善保管设备密钥,用于生成API签名");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Operation(summary = "上传设备硬件信息")
|
||||
@PostMapping("/update_hardware_info")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
|
||||
public Result<Void> updateHardwareInfo(
|
||||
@RequestHeader(value = "X-Device-SN") String sn,
|
||||
@RequestBody SnHardwareInfoReq hardwareInfoReq
|
||||
) {
|
||||
try {
|
||||
if (sn == null || sn.trim().isEmpty()) {
|
||||
return Result.failed("设备序列号不能为空");
|
||||
}
|
||||
|
||||
if (hardwareInfoReq == null) {
|
||||
return Result.failed("硬件信息不能为空");
|
||||
}
|
||||
|
||||
SnDeviceHardware hardware = new SnDeviceHardware();
|
||||
BeanUtils.copyProperties(hardwareInfoReq, hardware);
|
||||
hardware.setSerialno(sn);
|
||||
hardware.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
SnDeviceHardware existingHardware = hardwareService.lambdaQuery()
|
||||
.eq(SnDeviceHardware::getSerialno, sn)
|
||||
.one();
|
||||
|
||||
boolean saved;
|
||||
if (existingHardware != null) {
|
||||
hardware.setId(existingHardware.getId());
|
||||
saved = hardwareService.updateById(hardware);
|
||||
} else {
|
||||
hardware.setCreateTime(LocalDateTime.now());
|
||||
saved = hardwareService.save(hardware);
|
||||
}
|
||||
|
||||
if (!saved) {
|
||||
return Result.failed("保存硬件信息失败");
|
||||
}
|
||||
|
||||
logger.info("硬件信息上传成功, sn: {}", sn);
|
||||
return Result.success();
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("updateHardwareInfo error, sn: {}", sn, e);
|
||||
return Result.failed("上传硬件信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "上传设备截图")
|
||||
@PostMapping("/upload_screenshot")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.SCREENSHOT)
|
||||
public Result<Void> uploadScreenshot(
|
||||
@RequestPart(value = "file") MultipartFile file,
|
||||
@RequestHeader(value = "X-Device-SN") String sn
|
||||
) {
|
||||
try {
|
||||
if (file.isEmpty()) {
|
||||
return Result.failed("上传文件不能为空");
|
||||
}
|
||||
|
||||
if (sn == null || sn.trim().isEmpty()) {
|
||||
return Result.failed("设备序列号不能为空");
|
||||
}
|
||||
|
||||
String screenshotPath = FilePath.getScreenshotPath();
|
||||
logger.info("uploadScreenshot, screenshotPath: {}", screenshotPath);
|
||||
|
||||
File fileDir = new File(screenshotPath);
|
||||
if (!fileDir.exists()) {
|
||||
boolean created = fileDir.mkdirs();
|
||||
if (!created) {
|
||||
logger.error("创建目录失败: {}", screenshotPath);
|
||||
return Result.failed("创建目录失败");
|
||||
}
|
||||
}
|
||||
|
||||
String originName = file.getOriginalFilename();
|
||||
if (originName == null || originName.isEmpty()) {
|
||||
return Result.failed("文件名无效");
|
||||
}
|
||||
|
||||
String fileExtension = FilenameUtils.getExtension(originName);
|
||||
String md5 = HashUtils.calculateMultipartFileMd5(file);
|
||||
String sha1 = HashUtils.calculateMultipartFileSha1(file);
|
||||
String sha256 = HashUtils.calculateMultipartFileSha256(file);
|
||||
String fileName = sn + "_" + System.currentTimeMillis() + "_" + md5 + "." + fileExtension;
|
||||
File destFile = new File(fileDir, fileName);
|
||||
|
||||
file.transferTo(destFile);
|
||||
|
||||
SnScreenshot screenshotInfo = new SnScreenshot();
|
||||
screenshotInfo.setSn(sn);
|
||||
screenshotInfo.setFileName(fileName);
|
||||
screenshotInfo.setFilePath(screenshotPath + fileName);
|
||||
screenshotInfo.setFileSize(file.getSize());
|
||||
screenshotInfo.setFileMd5(md5);
|
||||
screenshotInfo.setFileSha1(sha1);
|
||||
screenshotInfo.setFileSha256(sha256);
|
||||
screenshotInfo.setUploadTime(LocalDateTime.now());
|
||||
|
||||
screenshotService.save(screenshotInfo);
|
||||
|
||||
logger.info("截图上传成功, sn: {}, fileName: {}", sn, fileName);
|
||||
return Result.success();
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("uploadScreenshot error, sn: {}", sn, e);
|
||||
return Result.failed("上传截图失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "上传设备定位信息")
|
||||
@PostMapping("/upload_location")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.LOCATE)
|
||||
public Result<Void> uploadLocation(
|
||||
@RequestHeader(value = "X-Device-SN") String sn,
|
||||
@RequestBody SnLocationReq locationReq
|
||||
) {
|
||||
try {
|
||||
if (sn == null || sn.trim().isEmpty()) {
|
||||
return Result.failed("设备序列号不能为空");
|
||||
}
|
||||
|
||||
if (locationReq == null) {
|
||||
return Result.failed("定位信息不能为空");
|
||||
}
|
||||
|
||||
SnLocation location = new SnLocation();
|
||||
BeanUtils.copyProperties(locationReq, location);
|
||||
location.setSn(sn);
|
||||
|
||||
SnLocation existingLocation = locationService.lambdaQuery()
|
||||
.eq(SnLocation::getSn, sn)
|
||||
.one();
|
||||
|
||||
boolean saved;
|
||||
if (existingLocation != null) {
|
||||
location.setId(existingLocation.getId());
|
||||
saved = locationService.updateById(location);
|
||||
} else {
|
||||
saved = locationService.save(location);
|
||||
}
|
||||
|
||||
if (!saved) {
|
||||
return Result.failed("保存定位信息失败");
|
||||
}
|
||||
|
||||
logger.info("定位信息上传成功, sn: {}, location: {}", sn, locationReq);
|
||||
return Result.success();
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("uploadLocation error, sn: {}", sn, e);
|
||||
return Result.failed("上传定位信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取开发者选项开关")
|
||||
@GetMapping("/get_developer_options")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
|
||||
public Result<DeveloperOptionsVO> getDeveloperOptions(@RequestHeader(value = "X-Device-SN") String sn) {
|
||||
try {
|
||||
if (sn == null || sn.trim().isEmpty()) {
|
||||
return Result.failed("设备序列号不能为空");
|
||||
}
|
||||
|
||||
// 查询该设备的开发者选项配置
|
||||
SnDeveloper developerConfig = developerService.lambdaQuery()
|
||||
.eq(SnDeveloper::getSn, sn)
|
||||
.one();
|
||||
|
||||
DeveloperOptionsVO vo = new DeveloperOptionsVO();
|
||||
if (developerConfig != null) {
|
||||
vo.setDeveloperOptions(developerConfig.getDeveloperOptions());
|
||||
} else {
|
||||
vo.setDeveloperOptions(0);
|
||||
}
|
||||
// 返回开发者选项开关状态
|
||||
return Result.success(vo);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("getDeveloperOptions error, sn: {}", sn, e);
|
||||
return Result.failed("获取开发者选项失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "上传设备已安装应用列表")
|
||||
@PostMapping("/upload_install_apks")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
|
||||
public Result<Void> uploadInstallApks(
|
||||
@RequestHeader(value = "X-Device-SN") String sn,
|
||||
@RequestBody List<ApkInstallInfoReq> apkInfos
|
||||
) {
|
||||
try {
|
||||
if (sn == null || sn.trim().isEmpty()) {
|
||||
return Result.failed("设备序列号不能为空");
|
||||
}
|
||||
|
||||
if (apkInfos == null || apkInfos.isEmpty()) {
|
||||
return Result.failed("应用列表不能为空");
|
||||
}
|
||||
|
||||
boolean saved = apkInstallService.saveOrUpdateDeviceApkInfo(sn, apkInfos);
|
||||
|
||||
if (!saved) {
|
||||
return Result.failed("保存应用信息失败");
|
||||
}
|
||||
|
||||
logger.info("应用列表上传成功, sn: {}, count: {}", sn, apkInfos.size());
|
||||
return Result.success();
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("uploadInstallApks error, sn: {}", sn, e);
|
||||
return Result.failed("上传应用列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取设备已安装应用列表")
|
||||
@GetMapping("/get_install_apks")
|
||||
@Log(module = LogModuleEnum.MOBILE, value = ActionTypeEnum.DEVELOPER)
|
||||
public Result<List<ApkInstallInfoReq>> getInstallApks(@RequestHeader(value = "X-Device-SN") String sn) {
|
||||
try {
|
||||
if (sn == null || sn.trim().isEmpty()) {
|
||||
return Result.failed("设备序列号不能为空");
|
||||
}
|
||||
|
||||
List<ApkInstallInfoReq> apkInfos = apkInstallService.getDeviceApkInfo(sn);
|
||||
|
||||
logger.info("获取应用列表成功, sn: {}, count: {}", sn, apkInfos.size());
|
||||
return Result.success(apkInfos);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("getInstallApks error, sn: {}", sn, e);
|
||||
return Result.failed("获取应用列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.youlai.boot.device.converter;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.device.model.entity.SnContact;
|
||||
import com.youlai.boot.device.model.form.ContactForm;
|
||||
import com.youlai.boot.device.model.vo.ContactVO;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.Named;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface ContactConverter {
|
||||
|
||||
Page<ContactVO> toPageVo(Page<SnContact> page);
|
||||
|
||||
SnContact toEntity(ContactForm contactForm);
|
||||
|
||||
ContactForm toForm(SnContact entity);
|
||||
|
||||
@Mapping(target = "createTime", source = "createTime", qualifiedByName = "localDateTimeToLong")
|
||||
@Mapping(target = "updateTime", source = "updateTime", qualifiedByName = "localDateTimeToLong")
|
||||
ContactVO toVo(SnContact entity);
|
||||
|
||||
List<ContactVO> toVoList(List<SnContact> list);
|
||||
|
||||
@Named("localDateTimeToLong")
|
||||
default Long localDateTimeToLong(LocalDateTime dateTime) {
|
||||
if (dateTime == null) {
|
||||
return null;
|
||||
}
|
||||
return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.youlai.boot.device.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.youlai.boot.device.model.entity.SnContact;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ContactMapper extends BaseMapper<SnContact> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.youlai.boot.device.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.youlai.boot.device.model.entity.SnDeveloper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 设备开发者选项Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface DeveloperMapper extends BaseMapper<SnDeveloper> {
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.youlai.boot.device.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.boot.common.annotation.DataPermission;
|
||||
import com.youlai.boot.device.model.entity.SnDeviceInfo;
|
||||
import com.youlai.boot.device.model.query.DeviceQuery;
|
||||
import com.youlai.boot.device.model.vo.DevicePageVO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@Mapper
|
||||
public interface DeviceMapper extends BaseMapper<SnDeviceInfo> {
|
||||
|
||||
/**
|
||||
* 获取用户分页列表
|
||||
*
|
||||
* @param page 分页参数
|
||||
* @param queryParams 查询参数
|
||||
* @return 用户分页列表
|
||||
*/
|
||||
@DataPermission(deptAlias = "u", userAlias = "u")
|
||||
Page<DevicePageVO> getSnPage(Page<DevicePageVO> page, @Param("queryParams") DeviceQuery queryParams);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.youlai.boot.device.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.youlai.boot.device.model.entity.SnDeviceHardware;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface HardwareMapper extends BaseMapper<SnDeviceHardware> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.youlai.boot.device.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.youlai.boot.device.model.entity.SnLocation;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 设备定位Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface LocationMapper extends BaseMapper<SnLocation> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.youlai.boot.device.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.youlai.boot.device.model.entity.SnScreenshot;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ScreenshotMapper extends BaseMapper<SnScreenshot> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.youlai.boot.device.model.document;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.mongodb.core.index.Indexed;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Document(collection = "apk_install_info")
|
||||
public class ApkInstallDocument {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Field("sn")
|
||||
@Indexed(unique = true)
|
||||
private String sn;
|
||||
|
||||
@Field("apk_list")
|
||||
private List<ApkInfo> apkList;
|
||||
|
||||
@Field("create_time")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Field("update_time")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Data
|
||||
public static class ApkInfo {
|
||||
@Field("package_name")
|
||||
private String packageName;
|
||||
|
||||
@Field("app_name")
|
||||
private String appName;
|
||||
|
||||
@Field("version_name")
|
||||
private String versionName;
|
||||
|
||||
@Field("version_code")
|
||||
private Long versionCode;
|
||||
|
||||
@Field("install_time")
|
||||
private LocalDateTime installTime;
|
||||
|
||||
@Field("last_update_time")
|
||||
private LocalDateTime lastUpdateTime;
|
||||
|
||||
@Field("apk_size")
|
||||
private Long apkSize;
|
||||
|
||||
@Field("data_size")
|
||||
private Long dataSize;
|
||||
|
||||
@Field("cache_size")
|
||||
private Long cacheSize;
|
||||
|
||||
@Field("md5")
|
||||
private String md5;
|
||||
|
||||
@Field("system_app")
|
||||
private boolean systemApp;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.youlai.boot.device.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 联系人实体
|
||||
*/
|
||||
@TableName("sys_sn_contact")
|
||||
@Getter
|
||||
@Setter
|
||||
public class SnContact extends BaseEntity {
|
||||
|
||||
@TableField("name")
|
||||
private String name;
|
||||
|
||||
@TableField("nick_name")
|
||||
private String nickName;
|
||||
|
||||
@TableField("phone_number")
|
||||
private String phoneNumber;
|
||||
|
||||
private String avatar;
|
||||
|
||||
private int position;
|
||||
|
||||
@TableField("emergency")
|
||||
private boolean emergency;
|
||||
|
||||
@TableField("show_desktop")
|
||||
private boolean showDesktop;
|
||||
|
||||
@TableField("bind_phone")
|
||||
private String bindPhone;
|
||||
|
||||
@TableField("bind_sn")
|
||||
private String bindSn;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.youlai.boot.device.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 设备截图实体
|
||||
*/
|
||||
@TableName("sys_sn_developer")
|
||||
@Getter
|
||||
@Setter
|
||||
public class SnDeveloper extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 设备序列号
|
||||
*/
|
||||
private String sn;
|
||||
|
||||
/**
|
||||
* 开发者选项开关(0关闭,1开启)
|
||||
*/
|
||||
private int developerOptions;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.youlai.boot.device.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 设备实体
|
||||
*/
|
||||
@TableName("sys_device")
|
||||
@Getter
|
||||
@Setter
|
||||
public class SnDevice extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 性别((1-男 2-女 0-保密)
|
||||
*/
|
||||
private Integer gender;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 用户头像
|
||||
*/
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 联系方式
|
||||
*/
|
||||
private String mobile;
|
||||
|
||||
/**
|
||||
* 状态((1-正常 0-禁用)
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 创建人 ID
|
||||
*/
|
||||
private Long createBy;
|
||||
|
||||
/**
|
||||
* 更新人 ID
|
||||
*/
|
||||
private Long updateBy;
|
||||
|
||||
/**
|
||||
* 是否删除(0-否 1-是)
|
||||
*/
|
||||
private Integer isDeleted;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.youlai.boot.device.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* 用户实体
|
||||
*/
|
||||
@TableName("sys_sn_hardware")
|
||||
@Getter
|
||||
@Setter
|
||||
public class SnDeviceHardware extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 设备序列号
|
||||
*/
|
||||
private String serialno;
|
||||
private String snImei;
|
||||
private String snImsi;
|
||||
private String snWlanMac;
|
||||
private String snDeviceMac;
|
||||
private String snBluetoothMac;
|
||||
private String snModel;
|
||||
private String snBrand;
|
||||
private String snBoard;
|
||||
private String snAndroidVersion;
|
||||
private int snAndroidApi;
|
||||
private String snBuildId;
|
||||
private String snBuildDisplayId;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.youlai.boot.device.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户实体
|
||||
*/
|
||||
@TableName("sys_sn")
|
||||
@Getter
|
||||
@Setter
|
||||
public class SnDeviceInfo extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 设备序列号
|
||||
*/
|
||||
private String serialno;
|
||||
|
||||
private String snModel;
|
||||
|
||||
private String snName;
|
||||
|
||||
private String snMobile;
|
||||
|
||||
private String pushId;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private LocalDateTime activateTime;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.youlai.boot.device.model.entity;
|
||||
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 设备定位实体
|
||||
*/
|
||||
@TableName("sys_sn_location")
|
||||
@Getter
|
||||
@Setter
|
||||
public class SnLocation extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 设备序列号
|
||||
*/
|
||||
private String sn;
|
||||
|
||||
/**
|
||||
*国家
|
||||
*/
|
||||
private String country;
|
||||
|
||||
/**
|
||||
*国家代码
|
||||
*/
|
||||
private String countryCode;
|
||||
|
||||
/**
|
||||
*省
|
||||
*/
|
||||
private String province;
|
||||
|
||||
/**
|
||||
*市
|
||||
*/
|
||||
private String city;
|
||||
|
||||
/**
|
||||
*城市代码
|
||||
*/
|
||||
private String cityCode;
|
||||
|
||||
/**
|
||||
*区
|
||||
*/
|
||||
private String district;
|
||||
|
||||
/**
|
||||
*街道
|
||||
*/
|
||||
private String street;
|
||||
|
||||
/**
|
||||
*门牌号
|
||||
*/
|
||||
private String streetNumber;
|
||||
|
||||
/**
|
||||
*详细地址
|
||||
*/
|
||||
private String address;
|
||||
|
||||
/**
|
||||
*区域编码
|
||||
*/
|
||||
private String adCode;
|
||||
|
||||
/**
|
||||
*镇
|
||||
*/
|
||||
private String town;
|
||||
|
||||
/**
|
||||
*镇级行政区划编码
|
||||
*/
|
||||
private String townCode;
|
||||
|
||||
/**
|
||||
*位置描述
|
||||
*/
|
||||
private String locationDescribe;
|
||||
|
||||
/**
|
||||
*经度
|
||||
*/
|
||||
private String longitude;
|
||||
|
||||
/**
|
||||
*纬度
|
||||
*/
|
||||
private String latitude;
|
||||
|
||||
/**
|
||||
*失败原因
|
||||
*/
|
||||
private String mapError;
|
||||
|
||||
/**
|
||||
*上次定位成功时间
|
||||
*/
|
||||
private LocalDateTime lastSuccessfulTime;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.youlai.boot.device.model.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.youlai.boot.common.base.BaseEntity;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 设备截图实体
|
||||
*/
|
||||
@TableName("sys_sn_screenshot")
|
||||
@Getter
|
||||
@Setter
|
||||
public class SnScreenshot extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 设备序列号
|
||||
*/
|
||||
private String sn;
|
||||
|
||||
/**
|
||||
* 文件名称
|
||||
*/
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件路径
|
||||
*/
|
||||
private String filePath;
|
||||
|
||||
/**
|
||||
* 文件大小(字节)
|
||||
*/
|
||||
private Long fileSize;
|
||||
|
||||
/**
|
||||
* 文件MD5值
|
||||
*/
|
||||
private String fileMd5;
|
||||
/**
|
||||
* 文件Sha1值
|
||||
*/
|
||||
|
||||
private String fileSha1;
|
||||
|
||||
/**
|
||||
* 文件Sha256值
|
||||
*/
|
||||
private String fileSha256;
|
||||
|
||||
/**
|
||||
* 上传时间戳
|
||||
*/
|
||||
private LocalDateTime uploadTime;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.youlai.boot.device.model.form;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@Schema(description = "联系人表单")
|
||||
public class ContactForm implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "主键ID")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "姓名不能为空")
|
||||
@Schema(description = "姓名")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "昵称")
|
||||
private String nickName;
|
||||
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
@Schema(description = "手机号")
|
||||
private String phoneNumber;
|
||||
|
||||
@Schema(description = "头像")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "排序")
|
||||
private int position;
|
||||
|
||||
@Schema(description = "是否紧急联系人")
|
||||
private boolean emergency;
|
||||
|
||||
@Schema(description = "是否显示")
|
||||
private boolean showDesktop;
|
||||
|
||||
@Schema(description = "绑定手机号")
|
||||
private String bindPhone;
|
||||
|
||||
@Schema(description = "绑定设备SN")
|
||||
private String bindSn;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.youlai.boot.device.model.form;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 设备开发者选项表单
|
||||
*
|
||||
* @author TTSTD
|
||||
* @since 2026-04-27
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "设备开发者选项Form")
|
||||
public class DeveloperForm implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "主键ID")
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "设备序列号不能为空")
|
||||
@Schema(description = "设备序列号")
|
||||
private String sn;
|
||||
|
||||
@NotNull(message = "开发者选项开关不能为空")
|
||||
@Schema(description = "开发者选项开关(0关闭,1开启)")
|
||||
private Integer developerOptions;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.youlai.boot.device.model.query;
|
||||
|
||||
import com.youlai.boot.common.base.BaseQuery;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Schema(description = "联系人查询")
|
||||
public class ContactQuery extends BaseQuery {
|
||||
|
||||
// @Schema(description = "关键字(姓名/昵称/手机号)")
|
||||
// private String keywords;
|
||||
//
|
||||
// @Schema(description = "是否紧急联系人")
|
||||
// private Boolean isEmergency;
|
||||
//
|
||||
// @Schema(description = "是否显示")
|
||||
// private Boolean show;
|
||||
|
||||
@Schema(description = "绑定设备SN")
|
||||
private String bindSn;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.youlai.boot.device.model.query;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.youlai.boot.common.base.BaseQuery;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description = "设备SN对象")
|
||||
public class DeviceQuery extends BaseQuery {
|
||||
|
||||
@Schema(description = "关键字(SN/IMEI/MAC)")
|
||||
private String keywords;
|
||||
|
||||
@Schema(description = "用户状态")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "部门ID")
|
||||
private Long deptId;
|
||||
|
||||
@Schema(description = "角色ID")
|
||||
private List<Long> roleIds;
|
||||
|
||||
@Schema(description = "创建时间范围")
|
||||
private List<String> createTime;
|
||||
|
||||
@JsonIgnore
|
||||
@Schema(hidden = true)
|
||||
private Boolean isRoot;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.youlai.boot.device.model.req;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Schema(description = "APK安装信息")
|
||||
public class ApkInstallInfoReq {
|
||||
@JsonProperty("package_name")
|
||||
@Schema(description = "包名")
|
||||
private String packageName;
|
||||
|
||||
@JsonProperty("app_name")
|
||||
@Schema(description = "应用名称")
|
||||
private String appName;
|
||||
|
||||
@JsonProperty("version_name")
|
||||
@Schema(description = "版本名称")
|
||||
private String versionName;
|
||||
|
||||
@JsonProperty("version_code")
|
||||
@Schema(description = "版本号")
|
||||
private Long versionCode;
|
||||
|
||||
@JsonProperty("install_time")
|
||||
@Schema(description = "安装时间")
|
||||
private Date installTime;
|
||||
|
||||
@JsonProperty("last_update_time")
|
||||
@Schema(description = "最后更新时间")
|
||||
private Date lastUpdateTime;
|
||||
|
||||
@JsonProperty("apk_size")
|
||||
@Schema(description = "Apk文件大小")
|
||||
private long apkSize;
|
||||
|
||||
@JsonProperty("data_size")
|
||||
@Schema(description = "App数据占用空间")
|
||||
private long dataSize;
|
||||
|
||||
@JsonProperty("cache_size")
|
||||
@Schema(description = "App缓存占用空间")
|
||||
private long cacheSize;
|
||||
|
||||
@JsonProperty("md5")
|
||||
@Schema(description = "apk MD5")
|
||||
private String md5;
|
||||
|
||||
@JsonProperty("system_app")
|
||||
@Schema(description = "是否为系统应用")
|
||||
private boolean systemApp;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.youlai.boot.device.model.req;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "设备硬件信息")
|
||||
@Data
|
||||
public class SnHardwareInfoReq {
|
||||
|
||||
@Schema(description="设备ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description="SN")
|
||||
private String serialno;
|
||||
|
||||
@Schema(description="WLAN MAC地址")
|
||||
@JsonProperty("sn_wlan_mac")
|
||||
private String snWlanMac;
|
||||
|
||||
@Schema(description="设备MAC地址")
|
||||
@JsonProperty("sn_device_mac")
|
||||
private String snDeviceMac;
|
||||
|
||||
@Schema(description="蓝牙MAC地址")
|
||||
@JsonProperty("sn_bluetooth_mac")
|
||||
private String snBluetoothMac;
|
||||
|
||||
@Schema(description="设备IMEI")
|
||||
@JsonProperty("sn_imei")
|
||||
private String snImei;
|
||||
|
||||
@Schema(description="设备型号")
|
||||
@JsonProperty("sn_model")
|
||||
private String snModel;
|
||||
|
||||
@Schema(description="设备品牌")
|
||||
@JsonProperty("sn_brand")
|
||||
private String snBrand;
|
||||
|
||||
@Schema(description="设备主板")
|
||||
@JsonProperty("sn_board")
|
||||
private String snBoard;
|
||||
|
||||
@Schema(description="设备Android版本")
|
||||
@JsonProperty("sn_android_version")
|
||||
private String snAndroidVersion;
|
||||
|
||||
@Schema(description="设备Android API")
|
||||
@JsonProperty("sn_android_api")
|
||||
private int snAndroidApi;
|
||||
|
||||
@Schema(description="设备构建ID")
|
||||
@JsonProperty("sn_build_id")
|
||||
private String snBuildId;
|
||||
|
||||
@Schema(description="设备显示ID")
|
||||
@JsonProperty("sn_build_display_id")
|
||||
private String snBuildDisplayId;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.youlai.boot.device.model.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 设备定位信息请求DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "设备定位信息")
|
||||
public class SnLocationReq {
|
||||
|
||||
@Schema(description = "设备序列号")
|
||||
private String sn;
|
||||
|
||||
@Schema(description = "国家")
|
||||
private String country;
|
||||
|
||||
@Schema(description = "国家代码")
|
||||
private String countryCode;
|
||||
|
||||
@Schema(description = "省")
|
||||
private String province;
|
||||
|
||||
@Schema(description = "市")
|
||||
private String city;
|
||||
|
||||
@Schema(description = "城市代码")
|
||||
private String cityCode;
|
||||
|
||||
@Schema(description = "区")
|
||||
private String district;
|
||||
|
||||
@Schema(description = "街道")
|
||||
private String street;
|
||||
|
||||
@Schema(description = "门牌号")
|
||||
private String streetNumber;
|
||||
|
||||
@Schema(description = "详细地址")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "区域编码")
|
||||
private String adCode;
|
||||
|
||||
@Schema(description = "镇")
|
||||
private String town;
|
||||
|
||||
@Schema(description = "镇级行政区划编码")
|
||||
private String townCode;
|
||||
|
||||
@Schema(description = "位置描述")
|
||||
private String locationDescribe;
|
||||
|
||||
@Schema(description = "经度")
|
||||
private String longitude;
|
||||
|
||||
@Schema(description = "纬度")
|
||||
private String latitude;
|
||||
|
||||
@Schema(description = "失败原因")
|
||||
private String mapError;
|
||||
|
||||
@Schema(description = "上次定位成功时间")
|
||||
private LocalDateTime lastSuccessfulTime;
|
||||
}
|
||||
53
src/main/java/com/youlai/boot/device/model/vo/ContactVO.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.youlai.boot.device.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Schema(description = "联系人视图对象")
|
||||
public class ContactVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "主键ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "姓名")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "昵称")
|
||||
private String nickName;
|
||||
|
||||
@Schema(description = "手机号")
|
||||
private String phoneNumber;
|
||||
|
||||
private String avatar;
|
||||
|
||||
private int position;
|
||||
|
||||
@Schema(description = "是否紧急联系人")
|
||||
private boolean emergency;
|
||||
|
||||
@Schema(description = "是否显示")
|
||||
private boolean showDesktop;
|
||||
|
||||
@Schema(description = "绑定手机号")
|
||||
private String bindPhone;
|
||||
|
||||
@Schema(description = "绑定设备SN")
|
||||
private String bindSn;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Long updateTime;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.youlai.boot.device.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Schema(description = "开发者选项响应对象")
|
||||
@Data
|
||||
public class DeveloperOptionsVO {
|
||||
|
||||
@Schema(description = "开发者选项开关(0关闭,1开启)")
|
||||
private Integer developerOptions;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.youlai.boot.device.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 设备硬件对象
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/1/15 9:41
|
||||
*/
|
||||
@Schema(description ="设备硬件对象")
|
||||
@Data
|
||||
public class DeviceHardwareVO {
|
||||
|
||||
@Schema(description="设备ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description="SN")
|
||||
private String serialno;
|
||||
|
||||
@Schema(description="WLAN MAC地址")
|
||||
private String snWlanMac;
|
||||
|
||||
@Schema(description="设备MAC地址")
|
||||
private String snDeviceMac;
|
||||
|
||||
@Schema(description="蓝牙MAC地址")
|
||||
private String snBluetoothMac;
|
||||
|
||||
@Schema(description="设备IMEI")
|
||||
private String snImei;
|
||||
|
||||
@Schema(description="设备型号")
|
||||
private String snModel;
|
||||
|
||||
@Schema(description="设备品牌")
|
||||
private String snBrand;
|
||||
|
||||
@Schema(description="设备主板")
|
||||
private String snBoard;
|
||||
|
||||
@Schema(description="设备Android版本")
|
||||
private String snAndroidVersion;
|
||||
|
||||
@Schema(description="设备Android API")
|
||||
private int snAndroidApi;
|
||||
|
||||
@Schema(description="设备构建ID")
|
||||
private String snBuildId;
|
||||
|
||||
@Schema(description="设备显示ID")
|
||||
private String snBuildDisplayId;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.youlai.boot.device.model.vo;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 设备分页视图对象
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/1/15 9:41
|
||||
*/
|
||||
@Schema(description ="设备分页对象")
|
||||
@Data
|
||||
public class DevicePageVO {
|
||||
|
||||
@Schema(description="设备ID")
|
||||
private Long id;
|
||||
|
||||
@Schema(description="SN")
|
||||
private String serialno;
|
||||
|
||||
@Schema(description="设备型号")
|
||||
private String snModel;
|
||||
|
||||
@Schema(description="设备名")
|
||||
private String snName;
|
||||
|
||||
@Schema(description="设备绑定手机")
|
||||
private String snMobile;
|
||||
|
||||
@Schema(description="推送ID")
|
||||
private String pushId;
|
||||
|
||||
@Schema(description="设备状态(1:启用;0:禁用)")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description="是否删除(1:删除;0:未删除)")
|
||||
private Integer isDelete;
|
||||
|
||||
@Schema(description="激活时间")
|
||||
@JsonFormat(pattern = "yyyy/MM/dd HH:mm")
|
||||
private LocalDateTime activateTime;
|
||||
|
||||
@Schema(description="创建时间")
|
||||
@JsonFormat(pattern = "yyyy/MM/dd HH:mm")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.youlai.boot.device.repository;
|
||||
|
||||
import com.youlai.boot.device.model.document.ApkInstallDocument;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface ApkInstallRepository extends MongoRepository<ApkInstallDocument, String> {
|
||||
|
||||
Optional<ApkInstallDocument> findBySn(String sn);
|
||||
|
||||
void deleteBySn(String sn);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.youlai.boot.device.service;
|
||||
|
||||
import com.youlai.boot.device.model.req.ApkInstallInfoReq;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ApkInstallService {
|
||||
|
||||
boolean saveOrUpdateDeviceApkInfo(String sn, List<ApkInstallInfoReq> apkInfos);
|
||||
|
||||
List<ApkInstallInfoReq> getDeviceApkInfo(String sn);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.youlai.boot.device.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.device.model.entity.SnContact;
|
||||
import com.youlai.boot.device.model.form.ContactForm;
|
||||
import com.youlai.boot.device.model.query.ContactQuery;
|
||||
import com.youlai.boot.device.model.vo.ContactVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ContactService extends IService<SnContact> {
|
||||
|
||||
IPage<ContactVO> getContactPage(ContactQuery queryParams);
|
||||
|
||||
List<ContactVO> getAllContacts(String sn);
|
||||
|
||||
Long saveContact(String sn, ContactForm contactForm);
|
||||
|
||||
ContactForm getContactForm(Long id, String sn);
|
||||
|
||||
boolean updateContact(Long id, String sn, ContactForm contactForm);
|
||||
|
||||
boolean deleteContact(Long id, String sn);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.youlai.boot.device.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.device.model.entity.SnDeveloper;
|
||||
|
||||
/**
|
||||
* 设备开发者选项服务接口
|
||||
*/
|
||||
public interface DeveloperService extends IService<SnDeveloper> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.youlai.boot.device.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.device.model.entity.SnDeviceInfo;
|
||||
import com.youlai.boot.device.model.query.DeviceQuery;
|
||||
import com.youlai.boot.device.model.vo.DeviceHardwareVO;
|
||||
import com.youlai.boot.device.model.vo.DevicePageVO;
|
||||
|
||||
|
||||
public interface DeviceService extends IService<SnDeviceInfo> {
|
||||
|
||||
/**
|
||||
* 用户分页列表
|
||||
*
|
||||
* @return {@link IPage<DevicePageVO>} 用户分页列表
|
||||
*/
|
||||
IPage<DevicePageVO> getSnPage(DeviceQuery queryParams);
|
||||
|
||||
/**
|
||||
* 获取设备详情
|
||||
*
|
||||
* @param sn 设备序列号
|
||||
* @return 设备详情信息
|
||||
*/
|
||||
DevicePageVO getSnBindInfo(String sn);
|
||||
/**
|
||||
* 获取设备硬件信息
|
||||
*
|
||||
* @param sn 设备序列号
|
||||
* @return 设备硬件信息
|
||||
*/
|
||||
DeviceHardwareVO getSnHardwareInfo(String sn);
|
||||
|
||||
/**
|
||||
* 添加设备
|
||||
* @param device 设备信息
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean addSn(SnDeviceInfo device);
|
||||
|
||||
/**
|
||||
* 删除设备
|
||||
* @param sn 设备序列号
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteSn(String sn);
|
||||
|
||||
boolean deviceRefresh(String sn);
|
||||
/**
|
||||
* 截图
|
||||
* @param sn 序列号
|
||||
* @return 是否推送成功
|
||||
*/
|
||||
boolean screenSnapshot(String sn);
|
||||
|
||||
boolean deviceReboot(String sn);
|
||||
boolean deviceShutdown(String sn);
|
||||
boolean deviceLocate(String sn);
|
||||
|
||||
/**
|
||||
* 恢复出厂设置
|
||||
* @param sn 序列号
|
||||
* @return 是否推送成功
|
||||
*/
|
||||
boolean restore(String sn);
|
||||
|
||||
boolean setDeviceDeveloper(String sn);
|
||||
|
||||
/**
|
||||
* 新增开发者选项配置
|
||||
*
|
||||
* @param sn 设备序列号
|
||||
* @param developerOptions 开发者选项开关(0关闭,1开启)
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean addDeveloperConfig(String sn, Integer developerOptions);
|
||||
|
||||
/**
|
||||
* 删除开发者选项配置
|
||||
*
|
||||
* @param sn 设备序列号
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean deleteDeveloperConfig(String sn);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.youlai.boot.device.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.device.model.entity.SnDeviceHardware;
|
||||
|
||||
public interface HardwareService extends IService<SnDeviceHardware> {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.youlai.boot.device.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.device.model.entity.SnLocation;
|
||||
|
||||
/**
|
||||
* 设备定位服务接口
|
||||
*/
|
||||
public interface LocationService extends IService<SnLocation> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.youlai.boot.device.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.youlai.boot.device.model.entity.SnScreenshot;
|
||||
|
||||
public interface ScreenshotService extends IService<SnScreenshot> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.youlai.boot.device.service;
|
||||
|
||||
public interface StorageService {
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.youlai.boot.device.service.impl;
|
||||
|
||||
import com.youlai.boot.device.model.document.ApkInstallDocument;
|
||||
import com.youlai.boot.device.model.req.ApkInstallInfoReq;
|
||||
import com.youlai.boot.device.repository.ApkInstallRepository;
|
||||
import com.youlai.boot.device.service.ApkInstallService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ApkInstallServiceImpl implements ApkInstallService {
|
||||
|
||||
private final ApkInstallRepository apkInstallRepository;
|
||||
|
||||
@Override
|
||||
public boolean saveOrUpdateDeviceApkInfo(String sn, List<ApkInstallInfoReq> apkInfos) {
|
||||
if (apkInfos == null || apkInfos.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Optional<ApkInstallDocument> existingOpt = apkInstallRepository.findBySn(sn);
|
||||
ApkInstallDocument document;
|
||||
|
||||
if (existingOpt.isPresent()) {
|
||||
document = existingOpt.get();
|
||||
} else {
|
||||
document = new ApkInstallDocument();
|
||||
document.setSn(sn);
|
||||
document.setCreateTime(LocalDateTime.now());
|
||||
}
|
||||
|
||||
List<ApkInstallDocument.ApkInfo> apkInfoList = apkInfos.stream().map(req -> {
|
||||
ApkInstallDocument.ApkInfo apkInfo = new ApkInstallDocument.ApkInfo();
|
||||
BeanUtils.copyProperties(req, apkInfo);
|
||||
|
||||
if (req.getInstallTime() != null) {
|
||||
apkInfo.setInstallTime(LocalDateTime.ofInstant(
|
||||
req.getInstallTime().toInstant(), ZoneId.systemDefault()));
|
||||
}
|
||||
|
||||
if (req.getLastUpdateTime() != null) {
|
||||
apkInfo.setLastUpdateTime(LocalDateTime.ofInstant(
|
||||
req.getLastUpdateTime().toInstant(), ZoneId.systemDefault()));
|
||||
}
|
||||
|
||||
return apkInfo;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
document.setApkList(apkInfoList);
|
||||
document.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
apkInstallRepository.save(document);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ApkInstallInfoReq> getDeviceApkInfo(String sn) {
|
||||
Optional<ApkInstallDocument> documentOpt = apkInstallRepository.findBySn(sn);
|
||||
|
||||
if (!documentOpt.isPresent() || documentOpt.get().getApkList() == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
ApkInstallDocument document = documentOpt.get();
|
||||
List<ApkInstallDocument.ApkInfo> apkInfoList = document.getApkList();
|
||||
|
||||
return apkInfoList.stream().map(apkInfo -> {
|
||||
ApkInstallInfoReq req = new ApkInstallInfoReq();
|
||||
BeanUtils.copyProperties(apkInfo, req);
|
||||
|
||||
if (apkInfo.getInstallTime() != null) {
|
||||
req.setInstallTime(Date.from(apkInfo.getInstallTime().atZone(ZoneId.systemDefault()).toInstant()));
|
||||
}
|
||||
|
||||
if (apkInfo.getLastUpdateTime() != null) {
|
||||
req.setLastUpdateTime(Date.from(apkInfo.getLastUpdateTime().atZone(ZoneId.systemDefault()).toInstant()));
|
||||
}
|
||||
|
||||
return req;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.youlai.boot.device.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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.device.converter.ContactConverter;
|
||||
import com.youlai.boot.device.mapper.ContactMapper;
|
||||
import com.youlai.boot.device.model.entity.SnContact;
|
||||
import com.youlai.boot.device.model.form.ContactForm;
|
||||
import com.youlai.boot.device.model.query.ContactQuery;
|
||||
import com.youlai.boot.device.model.vo.ContactVO;
|
||||
import com.youlai.boot.device.service.ContactService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ContactServiceImpl extends ServiceImpl<ContactMapper, SnContact> implements ContactService {
|
||||
|
||||
private final ContactConverter contactConverter;
|
||||
|
||||
@Override
|
||||
public IPage<ContactVO> getContactPage(ContactQuery queryParams) {
|
||||
int pageNum = queryParams.getPageNum();
|
||||
int pageSize = queryParams.getPageSize();
|
||||
Page<SnContact> page = new Page<>(pageNum, pageSize);
|
||||
|
||||
// String keywords = queryParams.getKeywords();
|
||||
// Boolean isEmergency = queryParams.getIsEmergency();
|
||||
// Boolean show = queryParams.getShow();
|
||||
String bindSn = queryParams.getBindSn();
|
||||
|
||||
LambdaQueryWrapper<SnContact> wrapper = new LambdaQueryWrapper<SnContact>()
|
||||
// .and(StringUtils.isNotBlank(keywords),
|
||||
// w -> w.like(SnContact::getName, keywords)
|
||||
// .or()
|
||||
// .like(SnContact::getNick_name, keywords)
|
||||
// .or()
|
||||
// .like(SnContact::getPhone_number, keywords)
|
||||
// )
|
||||
// .eq(isEmergency != null, SnContact::is_emergency, isEmergency)
|
||||
// .eq(show != null, SnContact::is_show, show)
|
||||
.eq(StringUtils.isNotBlank(bindSn), SnContact::getBindSn, bindSn)
|
||||
.orderByDesc(SnContact::getCreateTime);
|
||||
|
||||
Page<SnContact> contactPage = this.page(page, wrapper);
|
||||
return contactConverter.toPageVo(contactPage);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<ContactVO> getAllContacts(String sn) {
|
||||
Assert.isTrue(sn != null && !sn.trim().isEmpty(), "设备序列号不能为空");
|
||||
|
||||
LambdaQueryWrapper<SnContact> wrapper = new LambdaQueryWrapper<SnContact>()
|
||||
.eq(SnContact::getBindSn, sn)
|
||||
.orderByDesc(SnContact::getUpdateTime);
|
||||
|
||||
List<SnContact> contacts = this.list(wrapper);
|
||||
return contactConverter.toVoList(contacts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long saveContact(String sn, ContactForm contactForm) {
|
||||
Assert.isTrue(sn != null && !sn.trim().isEmpty(), "设备序列号不能为空");
|
||||
|
||||
// 检查是否已存在相同的电话号码和设备SN组合
|
||||
LambdaQueryWrapper<SnContact> wrapper = new LambdaQueryWrapper<SnContact>()
|
||||
.eq(SnContact::getPhoneNumber, contactForm.getPhoneNumber())
|
||||
.eq(SnContact::getBindSn, sn);
|
||||
|
||||
long count = this.count(wrapper);
|
||||
if (count > 0) {
|
||||
// 如果存在重复,返回 null 表示失败
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
SnContact contact = contactConverter.toEntity(contactForm);
|
||||
if (contact.getId() < 0) {
|
||||
contact.setId(null);
|
||||
}
|
||||
contact.setBindSn(sn);
|
||||
|
||||
if (contact.getCreateTime() == null) {
|
||||
contact.setCreateTime(LocalDateTime.now());
|
||||
}
|
||||
if (contact.getUpdateTime() == null) {
|
||||
contact.setUpdateTime(LocalDateTime.now());
|
||||
}
|
||||
|
||||
if (contact.getPosition() == 0) {
|
||||
LambdaQueryWrapper<SnContact> positionWrapper = new LambdaQueryWrapper<SnContact>()
|
||||
.eq(SnContact::getBindSn, sn)
|
||||
.orderByDesc(SnContact::getPosition)
|
||||
.last("LIMIT 1");
|
||||
SnContact lastContact = this.getOne(positionWrapper);
|
||||
int newPosition = (lastContact != null) ? lastContact.getPosition() + 1 : 1;
|
||||
contact.setPosition(newPosition);
|
||||
}
|
||||
|
||||
// contact.setCreateBy(SecurityUtils.getUserId());
|
||||
if (this.save(contact)) {
|
||||
return contact.getId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContactForm getContactForm(Long id, String sn) {
|
||||
Assert.isTrue(sn != null && !sn.trim().isEmpty(), "设备序列号不能为空");
|
||||
|
||||
SnContact contact = this.getById(id);
|
||||
Assert.isTrue(contact != null, "联系人不存在");
|
||||
Assert.isTrue(sn.equals(contact.getBindSn()), "无权访问该联系人数据");
|
||||
|
||||
return contactConverter.toForm(contact);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateContact(Long id, String sn, ContactForm contactForm) {
|
||||
Assert.isTrue(sn != null && !sn.trim().isEmpty(), "设备序列号不能为空");
|
||||
|
||||
SnContact existingContact = this.getById(id);
|
||||
Assert.isTrue(existingContact != null, "联系人不存在");
|
||||
Assert.isTrue(sn.equals(existingContact.getBindSn()), "无权修改该联系人数据");
|
||||
|
||||
SnContact contact = contactConverter.toEntity(contactForm);
|
||||
contact.setId(id);
|
||||
contact.setBindSn(sn);
|
||||
// contact.setUpdateBy(SecurityUtils.getUserId());
|
||||
return this.updateById(contact);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteContact(Long id, String sn) {
|
||||
Assert.isTrue(sn != null && !sn.trim().isEmpty(), "设备序列号不能为空");
|
||||
|
||||
SnContact contact = this.getById(id);
|
||||
Assert.isTrue(contact != null, "联系人不存在");
|
||||
Assert.isTrue(sn.equals(contact.getBindSn()), "无权删除该联系人数据");
|
||||
|
||||
return this.removeById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.youlai.boot.device.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.device.mapper.DeveloperMapper;
|
||||
import com.youlai.boot.device.model.entity.SnDeveloper;
|
||||
import com.youlai.boot.device.service.DeveloperService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 设备开发者选项服务实现类
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DeveloperServiceImpl extends ServiceImpl<DeveloperMapper, SnDeveloper> implements DeveloperService {
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package com.youlai.boot.device.service.impl;
|
||||
|
||||
import cn.jiguang.sdk.api.PushApi;
|
||||
import cn.jiguang.sdk.bean.push.PushSendParam;
|
||||
import cn.jiguang.sdk.bean.push.PushSendResult;
|
||||
import cn.jiguang.sdk.bean.push.message.custom.CustomMessage;
|
||||
import cn.jiguang.sdk.exception.ApiErrorException;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
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.framework.security.util.SecurityUtils;
|
||||
import com.youlai.boot.device.mapper.DeviceMapper;
|
||||
import com.youlai.boot.device.model.entity.SnDeviceInfo;
|
||||
import com.youlai.boot.device.model.entity.SnDeveloper;
|
||||
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.DeveloperService;
|
||||
import com.youlai.boot.device.service.DeviceService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 设备控制实现类
|
||||
* @author TTSTD
|
||||
* @since 2026/04/06
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, SnDeviceInfo> implements DeviceService {
|
||||
PushApi pushApi = new PushApi.Builder()
|
||||
.setAppKey("d779178d9900d4fb5d633678")
|
||||
.setMasterSecret("be0e197d30fec7bec118a70d")
|
||||
.build();
|
||||
|
||||
private final DeveloperService developerService;
|
||||
|
||||
@Override
|
||||
public IPage<DevicePageVO> getSnPage(DeviceQuery queryParams) {
|
||||
// 参数构建
|
||||
int pageNum = queryParams.getPageNum();
|
||||
int pageSize = queryParams.getPageSize();
|
||||
Page<DevicePageVO> page = new Page<>(pageNum, pageSize);
|
||||
|
||||
boolean isRoot = SecurityUtils.isRoot();
|
||||
queryParams.setIsRoot(isRoot);
|
||||
|
||||
// 查询数据
|
||||
return this.baseMapper.getSnPage(page, queryParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DevicePageVO getSnBindInfo(String sn) {
|
||||
// 根据SN查询设备详情
|
||||
LambdaQueryWrapper<SnDeviceInfo> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(SnDeviceInfo::getSerialno, sn);
|
||||
|
||||
SnDeviceInfo device = this.getOne(wrapper);
|
||||
if (device == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 这里可以扩展查询更多详细信息,如部门、角色等
|
||||
DevicePageVO devicePageVO = new DevicePageVO();
|
||||
BeanUtils.copyProperties(device, devicePageVO);
|
||||
|
||||
return devicePageVO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceHardwareVO getSnHardwareInfo(String sn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addSn(SnDeviceInfo device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteSn(String sn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private PushSendParam getSinglePushSendParam(String sn, String type, String title, String content) {
|
||||
PushSendParam pushSendParam = new PushSendParam();
|
||||
pushSendParam.setPlatform("android");
|
||||
Map<String, Set<String>> aaudience = new HashMap<>();
|
||||
Set<String> aliasSet = new HashSet<>();
|
||||
aliasSet.add(sn);
|
||||
aaudience.put("alias", aliasSet);
|
||||
pushSendParam.setAudience(aaudience);
|
||||
CustomMessage customMessage = new CustomMessage();
|
||||
customMessage.setTitle(title);
|
||||
customMessage.setContentType(type);
|
||||
customMessage.setContent(content);
|
||||
customMessage.setExtras(new HashMap<>());
|
||||
pushSendParam.setCustom(customMessage);
|
||||
|
||||
return pushSendParam;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deviceRefresh(String sn) {
|
||||
PushSendParam pushSendParam = getSinglePushSendParam(sn, "1", "deviceRefresh", "refresh");
|
||||
try {
|
||||
PushSendResult result = pushApi.send(pushSendParam);
|
||||
log.info("send success:{}", result);
|
||||
return true;
|
||||
} catch (ApiErrorException e) {
|
||||
// 错误信息
|
||||
int httpStatus = e.getStats(); // HTTP状态码
|
||||
int errorCode = e.getApiError().getError().getCode(); // 错误码
|
||||
String errorMessage = e.getApiError().getError().getMessage(); // 错误信息
|
||||
log.error("send error, httpStatus:{} code:{}, message:{}", httpStatus, errorCode, errorMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean screenSnapshot(String sn) {
|
||||
PushSendParam pushSendParam = getSinglePushSendParam(sn, "2", "screenSnapshot", "screenshot");
|
||||
try {
|
||||
PushSendResult result = pushApi.send(pushSendParam);
|
||||
log.info("send success:{}", result);
|
||||
return true;
|
||||
} catch (ApiErrorException e) {
|
||||
// 错误信息
|
||||
int httpStatus = e.getStats(); // HTTP状态码
|
||||
int errorCode = e.getApiError().getError().getCode(); // 错误码
|
||||
String errorMessage = e.getApiError().getError().getMessage(); // 错误信息
|
||||
log.error("send error, httpStatus:{} code:{}, message:{}", httpStatus, errorCode, errorMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deviceReboot(String sn) {
|
||||
PushSendParam pushSendParam = getSinglePushSendParam(sn, "3", "deviceReboot", "reboot");
|
||||
try {
|
||||
PushSendResult result = pushApi.send(pushSendParam);
|
||||
log.info("send success:{}", result);
|
||||
return true;
|
||||
} catch (ApiErrorException e) {
|
||||
// 错误信息
|
||||
int httpStatus = e.getStats(); // HTTP状态码
|
||||
int errorCode = e.getApiError().getError().getCode(); // 错误码
|
||||
String errorMessage = e.getApiError().getError().getMessage(); // 错误信息
|
||||
log.error("send error, httpStatus:{} code:{}, message:{}", httpStatus, errorCode, errorMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deviceShutdown(String sn) {
|
||||
PushSendParam pushSendParam = getSinglePushSendParam(sn, "4", "deviceShutdown", "shutdown");
|
||||
try {
|
||||
PushSendResult result = pushApi.send(pushSendParam);
|
||||
log.info("send success:{}", result);
|
||||
return true;
|
||||
} catch (ApiErrorException e) {
|
||||
// 错误信息
|
||||
int httpStatus = e.getStats(); // HTTP状态码
|
||||
int errorCode = e.getApiError().getError().getCode(); // 错误码
|
||||
String errorMessage = e.getApiError().getError().getMessage(); // 错误信息
|
||||
log.error("send error, httpStatus:{} code:{}, message:{}", httpStatus, errorCode, errorMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deviceLocate(String sn) {
|
||||
PushSendParam pushSendParam = getSinglePushSendParam(sn, "5", "deviceLocate", "locate");
|
||||
try {
|
||||
PushSendResult result = pushApi.send(pushSendParam);
|
||||
log.info("send success:{}", result);
|
||||
return true;
|
||||
} catch (ApiErrorException e) {
|
||||
// 错误信息
|
||||
int httpStatus = e.getStats(); // HTTP状态码
|
||||
int errorCode = e.getApiError().getError().getCode(); // 错误码
|
||||
String errorMessage = e.getApiError().getError().getMessage(); // 错误信息
|
||||
log.error("send error, httpStatus:{} code:{}, message:{}", httpStatus, errorCode, errorMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean restore(String sn) {
|
||||
PushSendParam pushSendParam = getSinglePushSendParam(sn, "6", "deviceRestore", "restore");
|
||||
try {
|
||||
PushSendResult result = pushApi.send(pushSendParam);
|
||||
log.info("send success:{}", result);
|
||||
return true;
|
||||
} catch (ApiErrorException e) {
|
||||
// 错误信息
|
||||
int httpStatus = e.getStats(); // HTTP状态码
|
||||
int errorCode = e.getApiError().getError().getCode(); // 错误码
|
||||
String errorMessage = e.getApiError().getError().getMessage(); // 错误信息
|
||||
log.error("send error, httpStatus:{} code:{}, message:{}", httpStatus, errorCode, errorMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setDeviceDeveloper(String sn) {
|
||||
PushSendParam pushSendParam = getSinglePushSendParam(sn, "7", "deviceDeveloper", "developer");
|
||||
try {
|
||||
PushSendResult result = pushApi.send(pushSendParam);
|
||||
log.info("send success:{}", result);
|
||||
return true;
|
||||
} catch (ApiErrorException e) {
|
||||
// 错误信息
|
||||
int httpStatus = e.getStats(); // HTTP状态码
|
||||
int errorCode = e.getApiError().getError().getCode(); // 错误码
|
||||
String errorMessage = e.getApiError().getError().getMessage(); // 错误信息
|
||||
log.error("send error, httpStatus:{} code:{}, message:{}", httpStatus, errorCode, errorMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addDeveloperConfig(String sn, Integer developerOptions) {
|
||||
try {
|
||||
// 检查是否已存在该设备的配置
|
||||
SnDeveloper existingConfig = developerService.lambdaQuery()
|
||||
.eq(SnDeveloper::getSn, sn)
|
||||
.one();
|
||||
|
||||
SnDeveloper developerConfig = new SnDeveloper();
|
||||
developerConfig.setSn(sn);
|
||||
developerConfig.setDeveloperOptions(developerOptions);
|
||||
|
||||
boolean result;
|
||||
if (existingConfig != null) {
|
||||
// 如果已存在,则更新
|
||||
developerConfig.setId(existingConfig.getId());
|
||||
result = developerService.updateById(developerConfig);
|
||||
log.info("更新开发者选项配置成功, sn: {}, developerOptions: {}", sn, developerOptions);
|
||||
} else {
|
||||
// 如果不存在,则新增
|
||||
result = developerService.save(developerConfig);
|
||||
log.info("新增开发者选项配置成功, sn: {}, developerOptions: {}", sn, developerOptions);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("新增开发者选项配置失败, sn: {}", sn, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteDeveloperConfig(String sn) {
|
||||
try {
|
||||
boolean result = developerService.lambdaUpdate()
|
||||
.eq(SnDeveloper::getSn, sn)
|
||||
.remove();
|
||||
|
||||
if (result) {
|
||||
log.info("删除开发者选项配置成功, sn: {}", sn);
|
||||
} else {
|
||||
log.warn("删除开发者选项配置失败,记录不存在, sn: {}", sn);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("删除开发者选项配置失败, sn: {}", sn, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.youlai.boot.device.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.device.mapper.HardwareMapper;
|
||||
import com.youlai.boot.device.model.entity.SnDeviceHardware;
|
||||
import com.youlai.boot.device.service.HardwareService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class HardwareServiceImpl extends ServiceImpl<HardwareMapper, SnDeviceHardware> implements HardwareService {
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.youlai.boot.device.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.device.mapper.LocationMapper;
|
||||
import com.youlai.boot.device.model.entity.SnLocation;
|
||||
import com.youlai.boot.device.service.LocationService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 设备定位服务实现类
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LocationServiceImpl extends ServiceImpl<LocationMapper, SnLocation> implements LocationService {
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.youlai.boot.device.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.device.mapper.ScreenshotMapper;
|
||||
import com.youlai.boot.device.model.entity.SnScreenshot;
|
||||
import com.youlai.boot.device.service.ScreenshotService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ScreenshotServiceImpl extends ServiceImpl<ScreenshotMapper, SnScreenshot> implements ScreenshotService {
|
||||
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
package com.youlai.boot.framework.cache;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.JacksonJsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import tools.jackson.databind.cfg.DateTimeFeature;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
/**
|
||||
* Redis 配置
|
||||
@@ -38,13 +37,11 @@ public class RedisConfig {
|
||||
redisTemplate.setHashKeySerializer(RedisSerializer.string());
|
||||
|
||||
// Value 使用自定义 JSON 序列化(不写入类型信息,避免 HashSet 等集合被序列化成带 @class 的结构)
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
// 禁用类型信息写入,避免集合类型名被当成元素
|
||||
objectMapper.disableDefaultTyping();
|
||||
JsonMapper jsonMapper = JsonMapper.builder()
|
||||
.disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
.build();
|
||||
|
||||
Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
|
||||
JacksonJsonRedisSerializer<Object> jsonSerializer = new JacksonJsonRedisSerializer<>(jsonMapper, Object.class);
|
||||
|
||||
redisTemplate.setValueSerializer(jsonSerializer);
|
||||
redisTemplate.setHashValueSerializer(jsonSerializer);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.youlai.boot.framework.integration.sms.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "sms.tencent")
|
||||
@Data
|
||||
public class TencentSmsProperties {
|
||||
|
||||
private String secretId;
|
||||
|
||||
private String secretKey;
|
||||
|
||||
private String regionId;
|
||||
|
||||
private String sdkAppId;
|
||||
|
||||
private String signName;
|
||||
|
||||
private Map<String, String> templates;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.youlai.boot.framework.integration.sms.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.aliyuncs.CommonRequest;
|
||||
import com.aliyuncs.CommonResponse;
|
||||
@@ -12,6 +13,7 @@ import com.youlai.boot.framework.integration.sms.config.AliyunSmsProperties;
|
||||
import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum;
|
||||
import com.youlai.boot.framework.integration.sms.service.SmsService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -24,6 +26,7 @@ import java.util.Map;
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AliyunSmsService implements SmsService {
|
||||
|
||||
private final AliyunSmsProperties aliyunSmsProperties;
|
||||
@@ -68,9 +71,23 @@ public class AliyunSmsService implements SmsService {
|
||||
|
||||
try {
|
||||
CommonResponse response = client.getCommonResponse(request);
|
||||
return response.getHttpResponse().isSuccess();
|
||||
String data = response.getData();
|
||||
log.info("阿里云短信响应: {}", data);
|
||||
|
||||
// 解析响应判断是否真正发送成功
|
||||
JSONObject jsonObject = JSONUtil.parseObj(data);
|
||||
String code = jsonObject.getStr("Code");
|
||||
String message = jsonObject.getStr("Message");
|
||||
|
||||
boolean success = "OK".equals(code);
|
||||
if (!success) {
|
||||
log.error("阿里云短信发送失败,手机号: {}, Code: {}, Message: {}", mobile, code, message);
|
||||
} else {
|
||||
log.info("阿里云短信发送成功,手机号: {}", mobile);
|
||||
}
|
||||
return success;
|
||||
} catch (ClientException e) {
|
||||
e.printStackTrace();
|
||||
log.error("阿里云短信发送异常,手机号: {}, 错误信息: {}", mobile, e.getMessage(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.youlai.boot.framework.integration.sms.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.tencentcloudapi.common.Credential;
|
||||
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
|
||||
import com.tencentcloudapi.common.profile.ClientProfile;
|
||||
import com.tencentcloudapi.common.profile.HttpProfile;
|
||||
import com.tencentcloudapi.sms.v20210111.SmsClient;
|
||||
import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest;
|
||||
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
|
||||
import com.youlai.boot.framework.integration.sms.config.TencentSmsProperties;
|
||||
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.context.annotation.Primary;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Primary
|
||||
public class TencentSmsService implements SmsService {
|
||||
|
||||
private final TencentSmsProperties tencentSmsProperties;
|
||||
|
||||
@Override
|
||||
public boolean sendSms(String mobile, SmsTypeEnum smsType, Map<String, String> templateParams) {
|
||||
try {
|
||||
String templateCode = tencentSmsProperties.getTemplates().get(smsType.getValue());
|
||||
|
||||
Credential cred = new Credential(
|
||||
tencentSmsProperties.getSecretId(),
|
||||
tencentSmsProperties.getSecretKey()
|
||||
);
|
||||
|
||||
HttpProfile httpProfile = new HttpProfile();
|
||||
httpProfile.setEndpoint("sms.tencentcloudapi.com");
|
||||
|
||||
ClientProfile clientProfile = new ClientProfile();
|
||||
clientProfile.setHttpProfile(httpProfile);
|
||||
|
||||
SmsClient client = new SmsClient(cred, tencentSmsProperties.getRegionId(), clientProfile);
|
||||
|
||||
SendSmsRequest req = new SendSmsRequest();
|
||||
req.setSmsSdkAppId(tencentSmsProperties.getSdkAppId());
|
||||
req.setSignName(tencentSmsProperties.getSignName());
|
||||
req.setTemplateId(templateCode);
|
||||
|
||||
String[] phoneNumberSet = {"+86" + mobile};
|
||||
req.setPhoneNumberSet(phoneNumberSet);
|
||||
|
||||
String[] templateParamSet = templateParams.values().toArray(new String[0]);
|
||||
req.setTemplateParamSet(templateParamSet);
|
||||
|
||||
SendSmsResponse resp = client.SendSms(req);
|
||||
|
||||
log.info("腾讯云短信发送响应: {}", JSONUtil.toJsonStr(resp));
|
||||
|
||||
return "Ok".equals(resp.getSendStatusSet()[0].getCode());
|
||||
|
||||
} catch (TencentCloudSDKException e) {
|
||||
log.error("腾讯云短信发送失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,10 @@ import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionIntercepto
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.youlai.boot.framework.mybatis.handler.MyMetaObjectHandler;
|
||||
import com.youlai.boot.framework.mybatis.interceptor.MyDataPermissionHandler;
|
||||
import org.apache.ibatis.mapping.DatabaseIdProvider;
|
||||
import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* mybatis-plus 配置类
|
||||
*
|
||||
@@ -26,9 +21,6 @@ import java.util.Properties;
|
||||
@EnableTransactionManagement
|
||||
public class MybatisConfig {
|
||||
|
||||
@Value("${app.db-type:mysql}")
|
||||
private String dbType;
|
||||
|
||||
/**
|
||||
* 分页插件和数据权限插件
|
||||
*/
|
||||
@@ -39,13 +31,8 @@ public class MybatisConfig {
|
||||
// 数据权限
|
||||
interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler()));
|
||||
|
||||
// 分页插件,根据配置动态选择数据库类型
|
||||
DbType mpDbType = DbType.MYSQL;
|
||||
String type = dbType == null ? "mysql" : dbType.toLowerCase();
|
||||
if ("postgres".equals(type) || "postgresql".equals(type)) {
|
||||
mpDbType = DbType.POSTGRE_SQL;
|
||||
}
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(mpDbType));
|
||||
// 分页插件
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
|
||||
return interceptor;
|
||||
}
|
||||
@@ -60,16 +47,4 @@ public class MybatisConfig {
|
||||
return globalConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库类型自动识别
|
||||
*/
|
||||
@Bean
|
||||
public DatabaseIdProvider databaseIdProvider() {
|
||||
DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("MySQL", "mysql");
|
||||
databaseIdProvider.setProperties(properties);
|
||||
return databaseIdProvider;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.youlai.boot.framework.security.config;
|
||||
import cn.binarywang.wx.miniapp.api.WxMaService;
|
||||
import com.youlai.boot.framework.captcha.service.CaptchaService;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import com.youlai.boot.framework.security.filter.MobileApiSignatureFilter;
|
||||
import com.youlai.boot.framework.web.filter.RateLimiterFilter;
|
||||
import com.youlai.boot.framework.security.filter.CaptchaValidationFilter;
|
||||
import com.youlai.boot.framework.security.filter.TokenAuthenticationFilter;
|
||||
@@ -67,6 +68,10 @@ public class SecurityConfig {
|
||||
if (ArrayUtil.isNotEmpty(ignoreUrls)) {
|
||||
requestMatcherRegistry.requestMatchers(ignoreUrls).permitAll();
|
||||
}
|
||||
|
||||
// 移动设备专用接口路径(需要设备签名验证,但不需要用户登录)
|
||||
requestMatcherRegistry.requestMatchers("/api/v1/sn/**").permitAll();
|
||||
requestMatcherRegistry.requestMatchers("/api/v1/auth/app/**").permitAll();
|
||||
// 其他所有请求需登录后访问
|
||||
requestMatcherRegistry.anyRequest().authenticated();
|
||||
}
|
||||
@@ -90,6 +95,8 @@ public class SecurityConfig {
|
||||
.addFilterBefore(new RateLimiterFilter(redisTemplate, configService), UsernamePasswordAuthenticationFilter.class)
|
||||
// 验证码校验过滤器
|
||||
.addFilterBefore(new CaptchaValidationFilter(captchaService), UsernamePasswordAuthenticationFilter.class)
|
||||
// 移动设备API签名验证过滤器(仅对 /api/v1/sn/** 路径生效)
|
||||
.addFilterBefore(new MobileApiSignatureFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class)
|
||||
// 验证和解析过滤器
|
||||
.addFilterBefore(new TokenAuthenticationFilter(tokenManager), UsernamePasswordAuthenticationFilter.class)
|
||||
.build();
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
package com.youlai.boot.framework.security.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.common.result.ResponseWriter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* 移动设备API签名验证过滤器
|
||||
* <p>
|
||||
* 用于验证 /api/v1/sn/* 路径下的移动设备请求
|
||||
* 验证规则:
|
||||
* 1. 检查必需的设备标识(deviceId)
|
||||
* 2. 检查时间戳(timestamp),防止重放攻击
|
||||
* 3. 验证签名(sign),确保请求未被篡改
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2026/4/21
|
||||
*/
|
||||
public class MobileApiSignatureFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final String HEADER_DEVICE_ID = "X-Device-SN";
|
||||
private static final String HEADER_NONCE = "X-Nonce";
|
||||
private static final String HEADER_TIMESTAMP = "X-Timestamp";
|
||||
private static final String HEADER_SIGN = "X-Sign";
|
||||
|
||||
|
||||
/**
|
||||
* 签名有效期(毫秒),默认2分钟
|
||||
*/
|
||||
private static final long SIGN_VALID_DURATION = 2 * 60 * 1000L;
|
||||
|
||||
/**
|
||||
* 设备密钥前缀
|
||||
*/
|
||||
private static final String DEVICE_SECRET_PREFIX = "device:secret:";
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
public MobileApiSignatureFilter(RedisTemplate<String, Object> redisTemplate) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
return !requestURI.startsWith("/api/v1/sn/");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
// 1. 获取设备标识
|
||||
String deviceId = request.getHeader(HEADER_DEVICE_ID);
|
||||
if (StrUtil.isBlank(deviceId)) {
|
||||
ResponseWriter.writeSuccess(response, ResultCode.MOBILE_DEVICE_ID_REQUIRED);
|
||||
return;
|
||||
}
|
||||
// 获取随机数
|
||||
String nonce = request.getHeader(HEADER_NONCE);
|
||||
if (StrUtil.isBlank(nonce)) {
|
||||
ResponseWriter.writeSuccess(response, ResultCode.MOBILE_NONCE_REQUIRED);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取时间戳
|
||||
String timestampStr = request.getHeader(HEADER_TIMESTAMP);
|
||||
if (StrUtil.isBlank(timestampStr)) {
|
||||
ResponseWriter.writeSuccess(response, ResultCode.MOBILE_TIMESTAMP_REQUIRED);
|
||||
return;
|
||||
}
|
||||
|
||||
long timestamp;
|
||||
try {
|
||||
timestamp = Long.parseLong(timestampStr);
|
||||
} catch (NumberFormatException e) {
|
||||
ResponseWriter.writeSuccess(response, ResultCode.MOBILE_TIMESTAMP_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 验证时间戳是否在有效期内
|
||||
long currentTime = System.currentTimeMillis();
|
||||
if (Math.abs(currentTime - timestamp) > SIGN_VALID_DURATION) {
|
||||
ResponseWriter.writeSuccess(response, ResultCode.MOBILE_TIMESTAMP_EXPIRED);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 获取签名
|
||||
String sign = request.getHeader(HEADER_SIGN);
|
||||
if (StrUtil.isBlank(sign)) {
|
||||
ResponseWriter.writeSuccess(response, ResultCode.MOBILE_SIGN_REQUIRED);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 获取设备密钥(从Redis或数据库)
|
||||
// String deviceSecret = getDeviceSecret(deviceId);
|
||||
// if (StrUtil.isBlank(deviceSecret)) {
|
||||
// ResponseWriter.writeSuccess(response, ResultCode.MOBILE_DEVICE_NOT_REGISTERED);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// 6. 验证签名
|
||||
String expectedSign = generateSign(request);
|
||||
logger.info("Expected sign: " + expectedSign);
|
||||
if (!sign.equals(expectedSign)) {
|
||||
ResponseWriter.writeSuccess(response, ResultCode.MOBILE_SIGN_INVALID);
|
||||
return;
|
||||
}
|
||||
|
||||
// 7. 将设备信息存入请求属性,供后续使用
|
||||
request.setAttribute("deviceId", deviceId);
|
||||
request.setAttribute("deviceAuthenticated", true);
|
||||
|
||||
// 8. 继续过滤器链
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备密钥
|
||||
*/
|
||||
private String getDeviceSecret(String deviceId) {
|
||||
Object secret = redisTemplate.opsForValue().get(DEVICE_SECRET_PREFIX + deviceId);
|
||||
return secret != null ? secret.toString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
* <p>
|
||||
* 签名算法:MD5(sorted_params + timestamp + deviceSecret)
|
||||
*/
|
||||
private String generateSign(HttpServletRequest request, String deviceSecret) {
|
||||
// 1. 收集所有请求参数
|
||||
TreeMap<String, String> params = new TreeMap<>();
|
||||
request.getParameterMap().forEach((key, values) -> {
|
||||
if (values != null && values.length > 0) {
|
||||
params.put(key, values[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 按字母顺序拼接参数
|
||||
StringBuilder sb = new StringBuilder();
|
||||
params.forEach((key, value) -> {
|
||||
sb.append(key).append("=").append(value).append("&");
|
||||
});
|
||||
|
||||
// 3. 添加时间戳和设备密钥
|
||||
String timestamp = request.getHeader(HEADER_TIMESTAMP);
|
||||
sb.append("timestamp=").append(timestamp)
|
||||
.append("&secret=").append(deviceSecret);
|
||||
|
||||
// 4. 生成MD5签名
|
||||
return DigestUtil.md5Hex(sb.toString(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private String generateSign(HttpServletRequest request) {
|
||||
// 1. 收集所有请求参数
|
||||
SortedMap<String, String> params = new TreeMap<>();
|
||||
params.put(HEADER_DEVICE_ID, request.getHeader(HEADER_DEVICE_ID));
|
||||
params.put(HEADER_NONCE, request.getHeader(HEADER_NONCE));
|
||||
params.put(HEADER_TIMESTAMP, request.getHeader(HEADER_TIMESTAMP));
|
||||
|
||||
// 2. 按字母顺序拼接参数
|
||||
StringBuilder sb = new StringBuilder();
|
||||
params.forEach((key, value) -> {
|
||||
sb.append(key).append("=").append(value).append("&");
|
||||
});
|
||||
sb.setLength(sb.length() - 1);
|
||||
|
||||
// 3. 生成MD5签名
|
||||
return DigestUtil.sha256Hex(sb.toString());
|
||||
}
|
||||
}
|
||||
@@ -190,13 +190,11 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(BadSqlGrammarException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public <T> Result<T> handleBadSqlGrammarException(BadSqlGrammarException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
String errorMsg = e.getMessage();
|
||||
if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) {
|
||||
log.error("SQL执行异常, message={}", e.getMessage(), e);
|
||||
if (StrUtil.isNotBlank(e.getMessage()) && e.getMessage().contains("denied to user")) {
|
||||
return Result.failed(ResultCode.DATABASE_ACCESS_DENIED);
|
||||
} else {
|
||||
return Result.failed(e.getMessage());
|
||||
}
|
||||
return Result.failed(ResultCode.DATABASE_EXECUTION_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.youlai.boot.framework.web.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import tools.jackson.databind.PropertyNamingStrategies;
|
||||
import tools.jackson.databind.cfg.DateTimeFeature;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
import tools.jackson.databind.module.SimpleModule;
|
||||
@@ -41,6 +42,7 @@ public class JacksonConfig {
|
||||
.addSerializer(Long.class, ToStringSerializer.instance)
|
||||
.addSerializer(BigInteger.class, ToStringSerializer.instance)
|
||||
)
|
||||
// .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@ import org.springframework.context.event.ContextClosedEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -198,6 +200,28 @@ public class SseSessionRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 心跳检测:每30秒向所有连接发送ping事件,及时清理已断开的僵尸连接
|
||||
*/
|
||||
@Scheduled(fixedRate = 30000)
|
||||
public void heartbeat() {
|
||||
if (emitterUserMap.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<SseEmitter> failedEmitters = new ArrayList<>();
|
||||
for (SseEmitter emitter : emitterUserMap.keySet()) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("ping").data("heartbeat"));
|
||||
} catch (Exception e) {
|
||||
failedEmitters.add(emitter);
|
||||
}
|
||||
}
|
||||
if (!failedEmitters.isEmpty()) {
|
||||
log.debug("心跳检测清理{}个失效SSE连接", failedEmitters.size());
|
||||
failedEmitters.forEach(this::removeEmitter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 容器关闭时主动断开所有 SSE 连接,避免阻塞应用停止
|
||||
*/
|
||||
|
||||
@@ -2,19 +2,23 @@ server:
|
||||
port: 8000
|
||||
|
||||
spring:
|
||||
threads:
|
||||
virtual:
|
||||
enabled: true # 开启虚拟线程
|
||||
|
||||
datasource:
|
||||
type: com.alibaba.druid.pool.DruidDataSource
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
|
||||
url: jdbc:mysql://www.youlai.tech:3306/youlai_admin?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
|
||||
username: youlai
|
||||
password: 123456
|
||||
url: jdbc:mysql://175.178.213.60:33306/youlai_admin?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
|
||||
username: root
|
||||
password: fanhuitong
|
||||
|
||||
data:
|
||||
redis:
|
||||
database: 12
|
||||
host: www.youlai.tech
|
||||
port: 6379
|
||||
password: 123456
|
||||
host: 175.178.213.60
|
||||
port: 26379
|
||||
password: fanhuitong
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
pool:
|
||||
@@ -50,6 +54,14 @@ spring:
|
||||
enable: true
|
||||
# 邮件发送者
|
||||
from: youlaitech@163.com
|
||||
mongodb:
|
||||
host: 175.178.213.60
|
||||
port: 27027
|
||||
database: device_apks
|
||||
username: fht
|
||||
password: fanhuitong
|
||||
authentication-database: admin
|
||||
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:/mapper/**/*.xml
|
||||
global-config:
|
||||
@@ -86,6 +98,7 @@ security:
|
||||
- /api/v1/auth/refresh-token # 刷新令牌接口
|
||||
- /api/v1/wxma/auth/** # 微信小程序认证接口(静默登录/手机号快捷登录/绑定手机号)
|
||||
- /api/v1/logs/** # 日志接口(访问日志列表)
|
||||
- /api/v1/sn/** # 移动设备专用接口(通过设备签名验证)
|
||||
# 非安全端点路径,完全绕过 Spring Security 的过滤器
|
||||
unsecured-urls:
|
||||
- ${springdoc.swagger-ui.path}
|
||||
@@ -94,6 +107,7 @@ security:
|
||||
- /v3/api-docs/**
|
||||
- /webjars/**
|
||||
- /favicon.ico
|
||||
- /error
|
||||
|
||||
# 文件存储配置
|
||||
oss:
|
||||
@@ -129,18 +143,29 @@ oss:
|
||||
sms:
|
||||
# 阿里云短信
|
||||
aliyun:
|
||||
accessKeyId: LTAI5tSMgfxxxxxxdiBJLyR
|
||||
accessKeySecret: SoOWRqpjtS7xxxxxxZ2PZiMTJOVC
|
||||
accessKeyId: LTAI5t6DdbXsfbyE91bscHEc
|
||||
accessKeySecret: s37PIUqflWiQT4FSNiwCSC30Bc5ojf
|
||||
domain: dysmsapi.aliyuncs.com
|
||||
regionId: cn-shanghai
|
||||
signName: 有来技术
|
||||
regionId: cn-shenzhen
|
||||
signName: 深圳市壹键通讯科技
|
||||
templates:
|
||||
# 注册短信验证码模板
|
||||
register: SMS_22xxx771
|
||||
register: SMS_506225577
|
||||
# 登录短信验证码模板
|
||||
login: SMS_22xxx772
|
||||
login: SMS_506225577
|
||||
# 修改手机号短信验证码模板
|
||||
change-mobile: SMS_22xxx773
|
||||
change-mobile: SMS_506225577
|
||||
|
||||
tencent:
|
||||
secretId: AKIDJXDqJk2963sUuAE7oIsQtAD4jANNBmCG
|
||||
secretKey: znQq58i8NTQAhR2Qi1KRO9i5HG2jDWcX
|
||||
regionId: ap-guangzhou
|
||||
sdkAppId: "1401023068"
|
||||
signName: 深圳市壹键通讯科技
|
||||
templates:
|
||||
register: "2510826"
|
||||
login: "2496464"
|
||||
change-mobile: "1234569"
|
||||
|
||||
# springdoc 配置文档: https://springdoc.org/properties.html
|
||||
springdoc:
|
||||
|
||||
@@ -5,15 +5,15 @@ spring:
|
||||
datasource:
|
||||
type: com.alibaba.druid.pool.DruidDataSource
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
|
||||
url: jdbc:mysql://www.youlai.tech:3306/youlai_admin?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
|
||||
username: youlai
|
||||
password: 123456
|
||||
url: jdbc:mysql://175.178.213.60:33306/youlai_admin?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoReconnect=true&allowMultiQueries=true
|
||||
username: root
|
||||
password: fanhuitong
|
||||
data:
|
||||
redis:
|
||||
database: 11
|
||||
host: www.youlai.tech
|
||||
port: 6379
|
||||
password: 123456
|
||||
host: 175.178.213.60
|
||||
port: 26379
|
||||
password: fanhuitong
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
pool:
|
||||
@@ -49,6 +49,13 @@ spring:
|
||||
enable: true
|
||||
# 邮件发送者
|
||||
from: youlaitech@163.com
|
||||
mongodb:
|
||||
host: 175.178.213.60
|
||||
port: 27027
|
||||
database: device_apks
|
||||
username: fht
|
||||
password: fanhuitong
|
||||
authentication-database: admin
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:/mapper/**/*.xml
|
||||
global-config:
|
||||
@@ -85,6 +92,7 @@ security:
|
||||
- /api/v1/auth/refresh-token # 刷新令牌接口
|
||||
- /api/v1/wxma/auth/** # 微信小程序认证接口(静默登录/手机号快捷登录/绑定手机号)
|
||||
- /api/v1/logs/** # 日志接口(访问日志列表)
|
||||
- /api/v1/sn/** # 移动设备专用接口(通过设备签名验证)
|
||||
# 非安全端点路径,完全绕过 Spring Security 的过滤器
|
||||
unsecured-urls:
|
||||
- ${springdoc.swagger-ui.path}
|
||||
@@ -93,6 +101,7 @@ security:
|
||||
- /v3/api-docs/**
|
||||
- /webjars/**
|
||||
- /favicon.ico
|
||||
- /error
|
||||
|
||||
# 文件存储配置
|
||||
oss:
|
||||
|
||||