commit
efed257f35
330 changed files with 29065 additions and 0 deletions
@ -0,0 +1,35 @@ |
|||
# EditorConfig is awesome: https://EditorConfig.org |
|||
|
|||
# top-most EditorConfig file |
|||
root = true |
|||
|
|||
# Unix-style newlines with a newline ending every file |
|||
[*] |
|||
charset = utf-8 |
|||
end_of_line = lf |
|||
insert_final_newline = true |
|||
trim_trailing_whitespace = true |
|||
|
|||
# Java files |
|||
[*.java] |
|||
indent_style = space |
|||
indent_size = 4 |
|||
|
|||
# XML files |
|||
[*.xml] |
|||
indent_style = space |
|||
indent_size = 4 |
|||
|
|||
# YAML files |
|||
[*.{yml,yaml}] |
|||
indent_style = space |
|||
indent_size = 2 |
|||
|
|||
# Properties files |
|||
[*.properties] |
|||
indent_style = space |
|||
indent_size = 4 |
|||
|
|||
# Markdown files |
|||
[*.md] |
|||
trim_trailing_whitespace = false |
|||
@ -0,0 +1,14 @@ |
|||
# Auto detect text files and perform LF normalization |
|||
* text=auto eol=lf |
|||
|
|||
# Java files |
|||
*.java text eol=lf |
|||
*.xml text eol=lf |
|||
|
|||
# Config files |
|||
*.yml text eol=lf |
|||
*.yaml text eol=lf |
|||
*.properties text eol=lf |
|||
|
|||
# Shell scripts |
|||
*.sh text eol=lf |
|||
@ -0,0 +1,19 @@ |
|||
# Created by .ignore support plugin (hsz.mobi) |
|||
### Example sysUserDetails template template |
|||
### Example sysUserDetails template |
|||
|
|||
# IntelliJ project files |
|||
.idea |
|||
*.iml |
|||
out |
|||
gen |
|||
target |
|||
*.log |
|||
logs |
|||
.history |
|||
|
|||
|
|||
docker/*/data/ |
|||
docker/minio/config |
|||
docker/xxljob/logs |
|||
application-youlai.yml |
|||
@ -0,0 +1,23 @@ |
|||
# 基础镜像 |
|||
FROM openjdk:17 |
|||
|
|||
# 维护者信息 |
|||
LABEL maintainer="youlai <youlaitech@163.com>" |
|||
|
|||
# 设置时区(Debian直接使用环境变量) |
|||
ENV TZ=Asia/Shanghai |
|||
|
|||
# 在运行时自动挂载 /tmp 目录为匿名卷 |
|||
VOLUME /tmp |
|||
|
|||
# 添加应用 |
|||
ADD target/youlai-boot.jar app.jar |
|||
|
|||
# 启动命令 |
|||
CMD java \ |
|||
-Xms512m -Xmx512m \ |
|||
-Djava.security.egd=file:/dev/./urandom \ |
|||
-jar /app.jar |
|||
|
|||
# 暴露端口 |
|||
EXPOSE 8000 |
|||
@ -0,0 +1,201 @@ |
|||
Apache License |
|||
Version 2.0, January 2004 |
|||
http://www.apache.org/licenses/ |
|||
|
|||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
|||
|
|||
1. Definitions. |
|||
|
|||
"License" shall mean the terms and conditions for use, reproduction, |
|||
and distribution as defined by Sections 1 through 9 of this document. |
|||
|
|||
"Licensor" shall mean the copyright owner or entity authorized by |
|||
the copyright owner that is granting the License. |
|||
|
|||
"Legal Entity" shall mean the union of the acting entity and all |
|||
other entities that control, are controlled by, or are under common |
|||
control with that entity. For the purposes of this definition, |
|||
"control" means (i) the power, direct or indirect, to cause the |
|||
direction or management of such entity, whether by contract or |
|||
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
|||
outstanding shares, or (iii) beneficial ownership of such entity. |
|||
|
|||
"You" (or "Your") shall mean an individual or Legal Entity |
|||
exercising permissions granted by this License. |
|||
|
|||
"Source" form shall mean the preferred form for making modifications, |
|||
including but not limited to software source code, documentation |
|||
source, and configuration files. |
|||
|
|||
"Object" form shall mean any form resulting from mechanical |
|||
transformation or translation of a Source form, including but |
|||
not limited to compiled object code, generated documentation, |
|||
and conversions to other media types. |
|||
|
|||
"Work" shall mean the work of authorship, whether in Source or |
|||
Object form, made available under the License, as indicated by a |
|||
copyright notice that is included in or attached to the work |
|||
(an example is provided in the Appendix below). |
|||
|
|||
"Derivative Works" shall mean any work, whether in Source or Object |
|||
form, that is based on (or derived from) the Work and for which the |
|||
editorial revisions, annotations, elaborations, or other modifications |
|||
represent, as a whole, an original work of authorship. For the purposes |
|||
of this License, Derivative Works shall not include works that remain |
|||
separable from, or merely link (or bind by name) to the interfaces of, |
|||
the Work and Derivative Works thereof. |
|||
|
|||
"Contribution" shall mean any work of authorship, including |
|||
the original version of the Work and any modifications or additions |
|||
to that Work or Derivative Works thereof, that is intentionally |
|||
submitted to Licensor for inclusion in the Work by the copyright owner |
|||
or by an individual or Legal Entity authorized to submit on behalf of |
|||
the copyright owner. For the purposes of this definition, "submitted" |
|||
means any form of electronic, verbal, or written communication sent |
|||
to the Licensor or its representatives, including but not limited to |
|||
communication on electronic mailing lists, source code control systems, |
|||
and issue tracking systems that are managed by, or on behalf of, the |
|||
Licensor for the purpose of discussing and improving the Work, but |
|||
excluding communication that is conspicuously marked or otherwise |
|||
designated in writing by the copyright owner as "Not a Contribution." |
|||
|
|||
"Contributor" shall mean Licensor and any individual or Legal Entity |
|||
on behalf of whom a Contribution has been received by Licensor and |
|||
subsequently incorporated within the Work. |
|||
|
|||
2. Grant of Copyright License. Subject to the terms and conditions of |
|||
this License, each Contributor hereby grants to You a perpetual, |
|||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
|||
copyright license to reproduce, prepare Derivative Works of, |
|||
publicly display, publicly perform, sublicense, and distribute the |
|||
Work and such Derivative Works in Source or Object form. |
|||
|
|||
3. Grant of Patent License. Subject to the terms and conditions of |
|||
this License, each Contributor hereby grants to You a perpetual, |
|||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
|||
(except as stated in this section) patent license to make, have made, |
|||
use, offer to sell, sell, import, and otherwise transfer the Work, |
|||
where such license applies only to those patent claims licensable |
|||
by such Contributor that are necessarily infringed by their |
|||
Contribution(s) alone or by combination of their Contribution(s) |
|||
with the Work to which such Contribution(s) was submitted. If You |
|||
institute patent litigation against any entity (including a |
|||
cross-claim or counterclaim in a lawsuit) alleging that the Work |
|||
or a Contribution incorporated within the Work constitutes direct |
|||
or contributory patent infringement, then any patent licenses |
|||
granted to You under this License for that Work shall terminate |
|||
as of the date such litigation is filed. |
|||
|
|||
4. Redistribution. You may reproduce and distribute copies of the |
|||
Work or Derivative Works thereof in any medium, with or without |
|||
modifications, and in Source or Object form, provided that You |
|||
meet the following conditions: |
|||
|
|||
(a) You must give any other recipients of the Work or |
|||
Derivative Works a copy of this License; and |
|||
|
|||
(b) You must cause any modified files to carry prominent notices |
|||
stating that You changed the files; and |
|||
|
|||
(c) You must retain, in the Source form of any Derivative Works |
|||
that You distribute, all copyright, patent, trademark, and |
|||
attribution notices from the Source form of the Work, |
|||
excluding those notices that do not pertain to any part of |
|||
the Derivative Works; and |
|||
|
|||
(d) If the Work includes a "NOTICE" text file as part of its |
|||
distribution, then any Derivative Works that You distribute must |
|||
include a readable copy of the attribution notices contained |
|||
within such NOTICE file, excluding those notices that do not |
|||
pertain to any part of the Derivative Works, in at least one |
|||
of the following places: within a NOTICE text file distributed |
|||
as part of the Derivative Works; within the Source form or |
|||
documentation, if provided along with the Derivative Works; or, |
|||
within a display generated by the Derivative Works, if and |
|||
wherever such third-party notices normally appear. The contents |
|||
of the NOTICE file are for informational purposes only and |
|||
do not modify the License. You may add Your own attribution |
|||
notices within Derivative Works that You distribute, alongside |
|||
or as an addendum to the NOTICE text from the Work, provided |
|||
that such additional attribution notices cannot be construed |
|||
as modifying the License. |
|||
|
|||
You may add Your own copyright statement to Your modifications and |
|||
may provide additional or different license terms and conditions |
|||
for use, reproduction, or distribution of Your modifications, or |
|||
for any such Derivative Works as a whole, provided Your use, |
|||
reproduction, and distribution of the Work otherwise complies with |
|||
the conditions stated in this License. |
|||
|
|||
5. Submission of Contributions. Unless You explicitly state otherwise, |
|||
any Contribution intentionally submitted for inclusion in the Work |
|||
by You to the Licensor shall be under the terms and conditions of |
|||
this License, without any additional terms or conditions. |
|||
Notwithstanding the above, nothing herein shall supersede or modify |
|||
the terms of any separate license agreement you may have executed |
|||
with Licensor regarding such Contributions. |
|||
|
|||
6. Trademarks. This License does not grant permission to use the trade |
|||
names, trademarks, service marks, or product names of the Licensor, |
|||
except as required for reasonable and customary use in describing the |
|||
origin of the Work and reproducing the content of the NOTICE file. |
|||
|
|||
7. Disclaimer of Warranty. Unless required by applicable law or |
|||
agreed to in writing, Licensor provides the Work (and each |
|||
Contributor provides its Contributions) on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
|||
implied, including, without limitation, any warranties or conditions |
|||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
|||
PARTICULAR PURPOSE. You are solely responsible for determining the |
|||
appropriateness of using or redistributing the Work and assume any |
|||
risks associated with Your exercise of permissions under this License. |
|||
|
|||
8. Limitation of Liability. In no event and under no legal theory, |
|||
whether in tort (including negligence), contract, or otherwise, |
|||
unless required by applicable law (such as deliberate and grossly |
|||
negligent acts) or agreed to in writing, shall any Contributor be |
|||
liable to You for damages, including any direct, indirect, special, |
|||
incidental, or consequential damages of any character arising as a |
|||
result of this License or out of the use or inability to use the |
|||
Work (including but not limited to damages for loss of goodwill, |
|||
work stoppage, computer failure or malfunction, or any and all |
|||
other commercial damages or losses), even if such Contributor |
|||
has been advised of the possibility of such damages. |
|||
|
|||
9. Accepting Warranty or Additional Liability. While redistributing |
|||
the Work or Derivative Works thereof, You may choose to offer, |
|||
and charge a fee for, acceptance of support, warranty, indemnity, |
|||
or other liability obligations and/or rights consistent with this |
|||
License. However, in accepting such obligations, You may act only |
|||
on Your own behalf and on Your sole responsibility, not on behalf |
|||
of any other Contributor, and only if You agree to indemnify, |
|||
defend, and hold each Contributor harmless for any liability |
|||
incurred by, or claims asserted against, such Contributor by reason |
|||
of your accepting any such warranty or additional liability. |
|||
|
|||
END OF TERMS AND CONDITIONS |
|||
|
|||
APPENDIX: How to apply the Apache License to your work. |
|||
|
|||
To apply the Apache License to your work, attach the following |
|||
boilerplate notice, with the fields enclosed by brackets "[]" |
|||
replaced with your own identifying information. (Don't include |
|||
the brackets!) The text should be enclosed in the appropriate |
|||
comment syntax for the file format. We also recommend that a |
|||
file or class name and description of purpose be included on the |
|||
same "printed page" as the copyright notice for easier |
|||
identification within third-party archives. |
|||
|
|||
Copyright 2023-present 有来开源 |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
|||
@ -0,0 +1,195 @@ |
|||
<p align="center"> |
|||
<img alt="youlai-boot" width="120" src="https://foruda.gitee.com/images/1733417239320800627/3c5290fe_716974.png"> |
|||
</p> |
|||
|
|||
<h1 align="center">youlai-boot</h1> |
|||
|
|||
<p align="center"> |
|||
<strong>Spring Boot 4 企业级权限管理系统后端</strong> |
|||
</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/) | |
|||
|
|||
## 🏗️ 技术栈 |
|||
|
|||
``` |
|||
┌─────────────────────────────────────────────┐ |
|||
│ youlai-boot │ |
|||
│ │ |
|||
│ ┌───────────┐ ┌──────────┐ ┌───────────┐ │ |
|||
│ │ Spring │ │ MyBatis │ │ Redis │ │ |
|||
│ │ Security │ │ Plus │ │ + JWT │ │ |
|||
│ └─────┬─────┘ └────┬─────┘ └─────┬─────┘ │ |
|||
│ │ │ │ │ |
|||
│ ┌─────▼──────────────▼─────────────▼───┐ │ |
|||
│ │ Spring Boot 4 │ │ |
|||
│ │ JDK 17 (LTS) │ │ |
|||
│ └───────────────────────────────────────┘ │ |
|||
│ │ |
|||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ |
|||
│ │ Swagger │ │ XXL-JOB │ │ MinIO │ │ |
|||
│ │ (接口文档) │ │(定时任务) │ │(文件存储) │ │ |
|||
│ └──────────┘ └──────────┘ └──────────┘ │ |
|||
│ │ |
|||
│ MySQL 8.0 · Redis 7 · Docker Compose │ |
|||
└─────────────────────────────────────────────┘ |
|||
``` |
|||
|
|||
<!-- 截图占位:建议替换为系统实际运行截图(登录页 + 首页 + 权限管理) --> |
|||
<p align="center"> |
|||
<!-- TODO: 替换为系统截图 --> |
|||
<img alt="系统截图" width="800" src="https://via.placeholder.com/800x450/e8f0fe/2563eb?text=System+Screenshot+Placeholder"> |
|||
</p> |
|||
<p align="center"><em>↑ 系统运行效果(待补充实际截图)</em></p> |
|||
|
|||
--- |
|||
|
|||
## 🚀 快速开始 |
|||
|
|||
### 环境要求 |
|||
|
|||
| 组件 | 版本 | |
|||
|------|------| |
|||
| JDK | 17+ | |
|||
| MySQL | 8.0+ / 5.7+ | |
|||
| 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 |
|||
``` |
|||
|
|||
默认账号:`admin` / `123456` |
|||
|
|||
### Docker 部署 |
|||
|
|||
```bash |
|||
cd docker && docker-compose up -d |
|||
``` |
|||
|
|||
详细指南:[部署文档](https://www.youlai.tech/docs/admin/backends/java/deploy) · [开发规范](https://www.youlai.tech/docs/admin/backends/java/dev-standards) |
|||
|
|||
## 📁 目录结构 |
|||
|
|||
``` |
|||
youlai-boot/ |
|||
├── docker/ # Docker 部署编排 |
|||
├── sql/ # 数据库初始化脚本 |
|||
├── src/main/java/com/youlai/boot/ |
|||
│ ├── YouLaiBootApplication # 启动类 |
|||
│ ├── auth/ # 认证授权(登录/登出/令牌) |
|||
│ ├── codegen/ # 代码生成器 |
|||
│ ├── common/ # 公共模块(常量/枚举/统一响应) |
|||
│ ├── file/ # 文件服务(MinIO/本地存储) |
|||
│ ├── framework/ # 技术基座 |
|||
│ │ ├── apidoc/ # OpenAPI/Swagger |
|||
│ │ ├── cache/ # Redis/Caffeine 缓存 |
|||
│ │ ├── captcha/ # 图形验证码 |
|||
│ │ ├── integration/ # 短信/邮件/微信 |
|||
│ │ ├── job/ # XXL-Job 定时任务 |
|||
│ │ ├── mybatis/ # MyBatis Plus 配置 |
|||
│ │ ├── security/ # 安全过滤器/Token机制 |
|||
│ │ └── web/ # 全局异常/跨域/限流 |
|||
│ ├── message/ # SSE 消息推送 |
|||
│ └── 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# 后端** | |
|||
|
|||
> 六种后端共享同一套 **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) | |
|||
| **🔗 接口文档** | 启动后访问 `/doc.html` | |
|||
|
|||
## 📊 项目统计 |
|||
|
|||
 |
|||
|
|||
## 🤝 参与贡献 |
|||
|
|||
欢迎 Issue、PR 和 Star!详见 [贡献指南](https://www.youlai.tech/docs/admin/faq/help)。 |
|||
|
|||
[](https://github.com/haoxianrui/youlai-boot/graphs/contributors) |
|||
|
|||
## 📄 开源协议 |
|||
|
|||
本项目基于 [Apache License 2.0](LICENSE) 开源,可免费用于商业项目。 |
|||
|
|||
--- |
|||
|
|||
<div align="center"> |
|||
|
|||
**关注「有来技术」,获取最新动态与技术分享** |
|||
|
|||
<br> |
|||
|
|||
<img src="https://foruda.gitee.com/images/1737108820762592766/3390ed0d_716974.png" width="220"> |
|||
|
|||
<br> |
|||
|
|||
*微信搜索「有来技术」或扫码关注* |
|||
|
|||
</div> |
|||
@ -0,0 +1,69 @@ |
|||
# 创建一个名为 "youlai-boot" 的桥接网络,在同一个网络中的容器可以通过容器名互相访问 |
|||
networks: |
|||
youlai-boot: |
|||
driver: bridge |
|||
|
|||
services: |
|||
mysql: |
|||
image: mysql:8.0.29 |
|||
container_name: mysql |
|||
restart: unless-stopped # 重启策略:除非手动停止容器,否则自动重启 |
|||
environment: |
|||
- TZ=Asia/Shanghai |
|||
- LANG= en_US.UTF-8 |
|||
- MYSQL_ROOT_PASSWORD=123456 #设置 root 用户的密码 |
|||
volumes: |
|||
- ./mysql/conf/my.cnf:/etc/my.cnf # 挂载 my.cnf 文件到容器的指定路径 |
|||
- ./mysql/data:/var/lib/mysql # 持久化 MySQL 数据 |
|||
- ../sql/mysql:/docker-entrypoint-initdb.d # 初始化 SQL 脚本目录 |
|||
ports: |
|||
- 3306:3306 |
|||
networks: |
|||
- youlai-boot # 加入 "youlai-boot" 网络 |
|||
|
|||
redis: |
|||
image: redis:7.2.3 |
|||
container_name: redis |
|||
restart: unless-stopped |
|||
command: redis-server /etc/redis/redis.conf --requirepass 123456 --appendonly no # 启动 Redis 服务并添加密码为:123456,默认不开启 Redis AOF 方式持久化配置 |
|||
environment: |
|||
- TZ=Asia/Shanghai |
|||
volumes: |
|||
- ./redis/data:/data |
|||
- ./redis/config/redis.conf:/etc/redis/redis.conf |
|||
ports: |
|||
- 6379:6379 |
|||
networks: |
|||
- youlai-boot |
|||
|
|||
minio: |
|||
image: minio/minio:RELEASE.2024-07-16T23-46-41Z |
|||
container_name: minio |
|||
restart: unless-stopped |
|||
command: server /data --console-address ":9001" |
|||
ports: |
|||
- 9000:9000 |
|||
- 9001:9001 |
|||
environment: |
|||
- TZ=Asia/Shanghai |
|||
- LANG=en_US.UTF-8 |
|||
- MINIO_ROOT_USER=minioadmin |
|||
- MINIO_ROOT_PASSWORD=minioadmin |
|||
volumes: |
|||
- ./minio/data:/data |
|||
- ./minio/config:/root/.minio |
|||
networks: |
|||
- youlai-boot |
|||
|
|||
xxl-job-admin: |
|||
image: xuxueli/xxl-job-admin:2.4.0 |
|||
container_name: xxl-job-admin |
|||
restart: unless-stopped |
|||
environment: |
|||
PARAMS: '--spring.datasource.url=jdbc:mysql://mysql:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai --spring.datasource.username=root --spring.datasource.password=123456 --spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver' |
|||
volumes: |
|||
- ./xxljob/logs:/data/applogs |
|||
ports: |
|||
- 8080:8080 |
|||
networks: |
|||
- youlai-boot |
|||
@ -0,0 +1,20 @@ |
|||
|
|||
|
|||
[mysqld] |
|||
# 字符集与排序规则 |
|||
character-set-server = utf8mb4 # 服务端默认字符集 |
|||
collation-server = utf8mb4_0900_ai_ci # 服务端默认排序规则 |
|||
|
|||
# 网络与路径 |
|||
datadir = /var/lib/mysql # 数据文件存放的目录 |
|||
bind-address = 0.0.0.0 # 允许远程连接,默认 127.0.0.1 只允许本地连接 |
|||
port = 3306 # 显式指定端口(默认3306可不写) |
|||
|
|||
# 客户端字符集同步(避免乱码) |
|||
init_connect = 'SET NAMES utf8mb4' # 连接初始化时设置字符集 |
|||
|
|||
[client] |
|||
default-character-set = utf8mb4 # 客户端默认字符集 |
|||
|
|||
[mysql] |
|||
default-character-set = utf8mb4 # MySQL 命令行工具字符集 |
|||
File diff suppressed because it is too large
@ -0,0 +1,16 @@ |
|||
|
|||
# Docker Compose 安装中间件 MySQL、Redis、Minio、Xxl-Job |
|||
|
|||
## 安装 |
|||
|
|||
```bash |
|||
docker-compose -f ./docker-compose.yml -p youlai-boot up -d |
|||
``` |
|||
|
|||
- p youlai-boot 指定命名空间,避免与其他容器冲突,这里方便管理,统一管理和卸载 |
|||
|
|||
## 卸载 |
|||
```bash |
|||
docker-compose -f ./docker-compose.yml -p youlai-boot down |
|||
``` |
|||
|
|||
@ -0,0 +1,319 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<groupId>com.youlai</groupId> |
|||
<artifactId>stray-animals</artifactId> |
|||
<version>4.3.0</version> |
|||
<description>基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。</description> |
|||
|
|||
<parent> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-parent</artifactId> |
|||
<version>4.0.1</version> <!-- lookup parent from repository --> |
|||
<relativePath/> |
|||
</parent> |
|||
|
|||
<properties> |
|||
<maven.compiler.source>17</maven.compiler.source> |
|||
<maven.compiler.target>17</maven.compiler.target> |
|||
|
|||
<hutool.version>5.8.41</hutool.version> |
|||
|
|||
<mysql-connector-j.version>9.1.0</mysql-connector-j.version> |
|||
<druid.version>1.2.24</druid.version> |
|||
<!-- Spring Boot 4.x 必须使用更新的版本 --> |
|||
<mybatis-plus.version>3.5.15</mybatis-plus.version> |
|||
<dynamic-datasource.version>4.3.1</dynamic-datasource.version> |
|||
|
|||
<knife4j.version>4.5.0</knife4j.version> |
|||
|
|||
<mapstruct.version>1.6.3</mapstruct.version> |
|||
<lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version> |
|||
|
|||
<xxl-job.version>3.2.0</xxl-job.version> |
|||
|
|||
<fastexcel.version>1.3.0</fastexcel.version> |
|||
|
|||
<!-- 对象存储 --> |
|||
<minio.version>8.5.10</minio.version> |
|||
<okhttp3.version>4.8.1</okhttp3.version> |
|||
|
|||
<aliyun-sdk-oss.version>3.16.3</aliyun-sdk-oss.version> |
|||
|
|||
<!-- redisson 分布式锁 --> |
|||
<redisson.version>4.1.0</redisson.version> |
|||
|
|||
<!-- 自动代码生成 --> |
|||
<mybatis-plus-generator.version>3.5.6</mybatis-plus-generator.version> |
|||
<velocity.version>2.3</velocity.version> |
|||
|
|||
<!-- IP 地区转换 --> |
|||
<ip2region.version>2.7.0</ip2region.version> |
|||
|
|||
<!-- 阿里云短信 --> |
|||
<aliyun.java.sdk.core.version>4.7.6</aliyun.java.sdk.core.version> |
|||
<aliyun.java.sdk.dysmsapi.version>2.2.1</aliyun.java.sdk.dysmsapi.version> |
|||
|
|||
<caffeine.version>2.9.3</caffeine.version> |
|||
|
|||
<!-- 阿里 TransmittableThreadLocal (支持异步场景的ThreadLocal传递) --> |
|||
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version> |
|||
|
|||
<weixin-java-miniapp.version>4.8.1.B</weixin-java-miniapp.version> |
|||
|
|||
|
|||
</properties> |
|||
|
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
<!--编译测试环境,不打包在lib--> |
|||
<scope>provided</scope> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>cn.hutool</groupId> |
|||
<artifactId>hutool-all</artifactId> |
|||
<version>${hutool.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- TransmittableThreadLocal: 支持异步场景的租户上下文传递 --> |
|||
<dependency> |
|||
<groupId>com.alibaba</groupId> |
|||
<artifactId>transmittable-thread-local</artifactId> |
|||
<version>${transmittable-thread-local.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- 允许使用Lombok的Java Bean类中使用MapStruct注解 (Lombok 1.18.20+) --> |
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok-mapstruct-binding</artifactId> |
|||
<version>${lombok-mapstruct-binding.version}</version> |
|||
<scope>provided</scope> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-web</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-test</artifactId> |
|||
<scope>test</scope> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-security</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-data-redis</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-cache</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Spring Boot 4.x 已改名 --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-aspectj</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-validation</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-mail</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.mysql</groupId> |
|||
<artifactId>mysql-connector-j</artifactId> |
|||
<version>${mysql-connector-j.version}</version> |
|||
<scope>runtime</scope> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.alibaba</groupId> |
|||
<artifactId>druid-spring-boot-starter</artifactId> |
|||
<version>${druid.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- Spring Boot 4.x 必须使用boot4版本 --> |
|||
<dependency> |
|||
<groupId>com.baomidou</groupId> |
|||
<artifactId>mybatis-plus-spring-boot4-starter</artifactId> |
|||
<version>${mybatis-plus.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.baomidou</groupId> |
|||
<artifactId>mybatis-plus-jsqlparser</artifactId> |
|||
<version>${mybatis-plus.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- knife4j 接口文档 --> |
|||
<dependency> |
|||
<groupId>com.github.xiaoymin</groupId> |
|||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId> |
|||
<version>${knife4j.version}</version> |
|||
<exclusions> |
|||
<exclusion> |
|||
<groupId>org.springdoc</groupId> |
|||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> |
|||
</exclusion> |
|||
</exclusions> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.springdoc</groupId> |
|||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> |
|||
<version>2.8.9</version> |
|||
</dependency> |
|||
|
|||
<!-- MapStruct 对象映射 --> |
|||
<dependency> |
|||
<groupId>org.mapstruct</groupId> |
|||
<artifactId>mapstruct</artifactId> |
|||
<version>${mapstruct.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.mapstruct</groupId> |
|||
<artifactId>mapstruct-processor</artifactId> |
|||
<version>${mapstruct.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- xxl-job 定时任务 --> |
|||
<dependency> |
|||
<groupId>com.xuxueli</groupId> |
|||
<artifactId>xxl-job-core</artifactId> |
|||
<version>${xxl-job.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- Excel 工具(EasyExcel-PLus ) --> |
|||
<dependency> |
|||
<groupId>cn.idev.excel</groupId> |
|||
<artifactId>fastexcel</artifactId> |
|||
<version>${fastexcel.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- MinIO 对象存储 --> |
|||
<dependency> |
|||
<groupId>io.minio</groupId> |
|||
<artifactId>minio</artifactId> |
|||
<version>${minio.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- 阿里云 OSS 对象存储 --> |
|||
<dependency> |
|||
<groupId>com.aliyun.oss</groupId> |
|||
<artifactId>aliyun-sdk-oss</artifactId> |
|||
<version>${aliyun-sdk-oss.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- redisson 分布式锁 --> |
|||
<dependency> |
|||
<groupId>org.redisson</groupId> |
|||
<artifactId>redisson-spring-boot-starter</artifactId> |
|||
<version>${redisson.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- mybatis-plus 代码生成器 --> |
|||
<dependency> |
|||
<groupId>com.baomidou</groupId> |
|||
<artifactId>mybatis-plus-generator</artifactId> |
|||
<version>${mybatis-plus-generator.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- velocity 模板引擎(代码生成) --> |
|||
<dependency> |
|||
<groupId>org.apache.velocity</groupId> |
|||
<artifactId>velocity-engine-core</artifactId> |
|||
<version>${velocity.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- IP 转省市区 --> |
|||
<dependency> |
|||
<groupId>org.lionsoul</groupId> |
|||
<artifactId>ip2region</artifactId> |
|||
<version>${ip2region.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.aliyun</groupId> |
|||
<artifactId>aliyun-java-sdk-core</artifactId> |
|||
<version>${aliyun.java.sdk.core.version}</version> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>com.aliyun</groupId> |
|||
<artifactId>aliyun-java-sdk-dysmsapi</artifactId> |
|||
<version>${aliyun.java.sdk.dysmsapi.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- 本地缓存 --> |
|||
<dependency> |
|||
<groupId>com.github.ben-manes.caffeine</groupId> |
|||
<artifactId>caffeine</artifactId> |
|||
<version>${caffeine.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- 微信小程序登录 --> |
|||
<dependency> |
|||
<groupId>com.github.binarywang</groupId> |
|||
<artifactId>weixin-java-miniapp</artifactId> |
|||
<version>${weixin-java-miniapp.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- 动态多数据源 --> |
|||
<!--<dependency> |
|||
<groupId>com.baomidou</groupId> |
|||
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId> |
|||
<version>${dynamic-datasource.version}</version> |
|||
</dependency>--> |
|||
|
|||
</dependencies> |
|||
|
|||
<build> |
|||
<finalName>${project.artifactId}</finalName> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-maven-plugin</artifactId> |
|||
<configuration> |
|||
<excludes> |
|||
<exclude> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
</exclude> |
|||
</excludes> |
|||
</configuration> |
|||
</plugin> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-jar-plugin</artifactId> |
|||
<version>3.3.0</version> |
|||
<configuration> |
|||
<excludes> |
|||
<!-- 只排除application相关的配置文件 --> |
|||
<exclude>**/application.yml</exclude> |
|||
<exclude>**/application-dev.yml</exclude> |
|||
<exclude>**/application-prod.yml</exclude> |
|||
</excludes> |
|||
</configuration> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
|
|||
</project> |
|||
@ -0,0 +1,586 @@ |
|||
|
|||
# YouLai_Admin 数据库(MySQL 5.7 ~ MySQL 8.x) |
|||
# Copyright (c) 2021-present, youlai.tech |
|||
|
|||
|
|||
-- ---------------------------- |
|||
-- 1. 创建数据库 |
|||
-- ---------------------------- |
|||
CREATE DATABASE IF NOT EXISTS youlai_admin CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; |
|||
|
|||
|
|||
-- ---------------------------- |
|||
-- 2. 创建表 && 数据初始化 |
|||
-- ---------------------------- |
|||
USE youlai_admin; |
|||
|
|||
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` json NULL COMMENT '路由参数', |
|||
PRIMARY KEY (`id`) USING BTREE |
|||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统菜单表'; |
|||
|
|||
-- ---------------------------- |
|||
-- Records of sys_menu |
|||
-- ---------------------------- |
|||
-- 顶级目录(1-9):系统/代码生成/文档/接口文档/组件/演示/多级/路由 |
|||
INSERT INTO `sys_menu` VALUES (1, 0, '0', '系统管理', 'C', '', '/system', 'Layout', NULL, NULL, NULL, 1, 1, 'system', '/system/user', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2, 0, '0', '代码生成', 'C', '', '/codegen', 'Layout', NULL, NULL, NULL, 1, 2, 'code', '/codegen/index', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (4, 0, '0', '平台文档', 'C', '', '/doc', 'Layout', NULL, NULL, NULL, 1, 4, 'document', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (5, 0, '0', '接口文档', 'C', '', '/api', 'Layout', NULL, NULL, NULL, 1, 5, 'api', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (6, 0, '0', '组件封装', 'C', '', '/component', 'Layout', NULL, NULL, NULL, 1, 6, 'menu', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (7, 0, '0', '功能演示', 'C', '', '/function', 'Layout', NULL, NULL, NULL, 1, 7, 'menu', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (8, 0, '0', '多级菜单', 'C', NULL, '/multi-level', 'Layout', NULL, 1, NULL, 1, 8, 'cascader', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (9, 0, '0', '路由参数', 'C', '', '/route-param', 'Layout', NULL, NULL, NULL, 1, 9, 'el-icon-ElementPlus', '', 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 (2601, 260, '0,1,260', '日志查询', 'B', NULL, '', NULL, 'sys:log:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); |
|||
|
|||
INSERT INTO `sys_menu` VALUES (270, 1, '0,1', '系统配置', 'M', 'Config', 'config', 'system/config/index', NULL, 0, 1, 1, 8, 'setting', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2701, 270, '0,1,270', '系统配置查询', 'B', NULL, '', NULL, 'sys:config:list', 0, 1, 1, 1, '', NULL, now(), now(), NULL); |
|||
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); |
|||
|
|||
-- 组件封装 |
|||
INSERT INTO `sys_menu` VALUES (701, 6, '0,6', '富文本编辑器', 'M', 'WangEditor', 'wang-editor', 'demo/wang-editor', NULL, NULL, 1, 1, 2, '', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (702, 6, '0,6', '图片上传', 'M', 'Upload', 'upload', 'demo/upload', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (703, 6, '0,6', '图标选择器', 'M', 'IconSelect', 'icon-select', 'demo/icon-select', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (704, 6, '0,6', '字典组件', 'M', 'DictDemo', 'dict-demo', 'demo/dictionary', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (705, 6, '0,6', '增删改查', 'M', 'Curd', 'curd', 'demo/curd/index', NULL, NULL, 1, 1, 0, '', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (706, 6, '0,6', '列表选择器', 'M', 'TableSelect', 'table-select', 'demo/table-select/index', NULL, NULL, 1, 1, 1, '', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (707, 6, '0,6', '拖拽组件', 'M', 'Drag', 'drag', 'demo/drag', NULL, NULL, NULL, 1, 5, '', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (708, 6, '0,6', '滚动文本', 'M', 'TextScroll', 'text-scroll', 'demo/text-scroll', NULL, NULL, NULL, 1, 6, '', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (709, 6, '0,6', '自适应表格操作列', 'M', 'AutoOperationColumn', 'operation-column', 'demo/auto-operation-column', NULL, NULL, 1, 1, 1, '', '', now(), now(), NULL); |
|||
|
|||
-- 功能演示 |
|||
INSERT INTO `sys_menu` VALUES (801, 7, '0,7', 'Icons', 'M', 'IconDemo', 'icon-demo', 'demo/icons', NULL, NULL, 1, 1, 2, 'el-icon-Notification', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (802, 7, '0,7', '字典实时同步', 'M', 'DictSync', 'dict-sync', 'demo/dict-sync', NULL, NULL, NULL, 1, 3, '', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (803, 7, '0,7', 'VxeTable', 'M', 'VxeTable', 'vxe-table', 'demo/vxe-table/index', NULL, NULL, 1, 1, 4, 'el-icon-MagicStick', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (804, 7, '0,7', 'CURD单文件', 'M', 'CurdSingle', 'curd-single', 'demo/curd-single', NULL, NULL, 1, 1, 5, 'el-icon-Reading', '', now(), now(), NULL); |
|||
|
|||
-- 多级菜单示例 |
|||
INSERT INTO `sys_menu` VALUES (910, 8, '0,8', '菜单一级', 'C', NULL, 'multi-level1', 'Layout', NULL, 1, NULL, 1, 1, '', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (911, 910, '0,8,910', '菜单二级', 'C', NULL, 'multi-level2', 'Layout', NULL, 0, NULL, 1, 1, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (912, 911, '0,8,910,911', '菜单三级-1', 'M', NULL, 'multi-level3-1', 'demo/multi-level/children/children/level3-1', NULL, 0, 1, 1, 1, '', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (913, 911, '0,8,910,911', '菜单三级-2', 'M', NULL, 'multi-level3-2', 'demo/multi-level/children/children/level3-2', NULL, 0, 1, 1, 2, '', '', now(), now(), NULL); |
|||
|
|||
-- 路由参数 |
|||
INSERT INTO `sys_menu` VALUES (1001, 9, '0,9', '参数(type=1)', 'M', 'RouteParamType1', 'route-param-type1', 'demo/route-param', NULL, 0, 1, 1, 1, 'el-icon-Star', NULL, now(), now(), '{\"type\": \"1\"}'); |
|||
INSERT INTO `sys_menu` VALUES (1002, 9, '0,9', '参数(type=2)', 'M', 'RouteParamType2', 'route-param-type2', 'demo/route-param', NULL, 0, 1, 1, 2, 'el-icon-StarFilled', NULL, now(), now(), '{\"type\": \"2\"}'); |
|||
|
|||
-- ---------------------------- |
|||
-- 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), (2, 6), (2, 7), (2, 8), (2, 9); |
|||
-- 系统管理 |
|||
INSERT INTO `sys_role_menu` VALUES (2, 210), (2, 2101), (2, 2102), (2, 2103), (2, 2104), (2, 2105), (2, 2106), (2, 2107); |
|||
INSERT INTO `sys_role_menu` VALUES (2, 220), (2, 2201), (2, 2202), (2, 2203), (2, 2204), (2, 2205); |
|||
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); |
|||
-- 组件封装 |
|||
INSERT INTO `sys_role_menu` VALUES (2, 701), (2, 702), (2, 703), (2, 704), (2, 705), (2, 706), (2, 707), (2, 708), (2, 709); |
|||
-- 功能演示 / 多级菜单 |
|||
INSERT INTO `sys_role_menu` VALUES (2, 801), (2, 802), (2, 803), (2, 804), (2, 910), (2, 911), (2, 912), (2, 913); |
|||
-- 路由参数 |
|||
INSERT INTO `sys_role_menu` VALUES (2, 1001), (2, 1002); |
|||
|
|||
-- ---------------------------- |
|||
-- 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 IGNORE INTO `sys_user_role` VALUES (1, 1); |
|||
INSERT IGNORE INTO `sys_user_role` VALUES (2, 2); |
|||
INSERT IGNORE INTO `sys_user_role` VALUES (3, 3); |
|||
INSERT IGNORE INTO `sys_user_role` VALUES (4, 4); |
|||
INSERT IGNORE INTO `sys_user_role` VALUES (5, 5); |
|||
INSERT IGNORE INTO `sys_user_role` VALUES (6, 6); |
|||
INSERT IGNORE INTO `sys_user_role` VALUES (7, 7); |
|||
|
|||
|
|||
-- ---------------------------- |
|||
-- 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` tinyint 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); |
|||
|
|||
-- ---------------------------- |
|||
-- Table structure for sys_user_social |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `sys_user_social`; |
|||
CREATE TABLE `sys_user_social` ( |
|||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', |
|||
`user_id` bigint NOT NULL COMMENT '用户ID', |
|||
`platform` varchar(20) NOT NULL COMMENT '平台类型(WECHAT_MINI/WECHAT_MP/ALIPAY/QQ/APPLE)', |
|||
`openid` varchar(64) NOT NULL COMMENT '平台openid', |
|||
`unionid` varchar(64) DEFAULT NULL COMMENT '微信unionid', |
|||
`nickname` varchar(64) DEFAULT NULL COMMENT '第三方昵称', |
|||
`avatar` varchar(255) DEFAULT NULL COMMENT '第三方头像URL', |
|||
`session_key` varchar(128) DEFAULT NULL COMMENT '微信session_key', |
|||
`verified` tinyint(1) DEFAULT 1 COMMENT '是否已验证(1-已验证 0-未验证)', |
|||
`create_time` datetime DEFAULT NULL COMMENT '绑定时间', |
|||
`update_time` datetime DEFAULT NULL COMMENT '更新时间', |
|||
PRIMARY KEY (`id`), |
|||
UNIQUE KEY `uk_platform_openid` (`platform`, `openid`), |
|||
KEY `idx_user_id` (`user_id`), |
|||
KEY `idx_unionid` (`unionid`) |
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户第三方账号绑定表'; |
|||
@ -0,0 +1,524 @@ |
|||
# YouLai_Admin 数据库(MySQL 5.7 ~ MySQL 8.x) |
|||
# Copyright (c) 2021-present, youlai.tech |
|||
|
|||
|
|||
-- ---------------------------- |
|||
-- 1. 创建数据库 |
|||
-- ---------------------------- |
|||
CREATE DATABASE IF NOT EXISTS youlai_admin_template CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; |
|||
|
|||
|
|||
-- ---------------------------- |
|||
-- 2. 创建表 && 数据初始化 |
|||
-- ---------------------------- |
|||
USE youlai_admin_template; |
|||
|
|||
SET NAMES utf8mb4; # 设置字符集 |
|||
SET FOREIGN_KEY_CHECKS = 0; # 关闭外键检查,加快导入速度 |
|||
|
|||
-- ---------------------------- |
|||
-- Table structure for sys_dept |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `sys_dept`; |
|||
CREATE TABLE `sys_dept` ( |
|||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', |
|||
`name` varchar(100) NOT NULL COMMENT '部门名称', |
|||
`code` varchar(100) NOT NULL COMMENT '部门编号', |
|||
`parent_id` bigint DEFAULT 0 COMMENT '父节点id', |
|||
`tree_path` varchar(255) NOT NULL COMMENT '父节点id路径', |
|||
`sort` smallint DEFAULT 0 COMMENT '显示顺序', |
|||
`status` tinyint DEFAULT 1 COMMENT '状态(1-正常 0-禁用)', |
|||
`create_by` bigint NULL COMMENT '创建人ID', |
|||
`create_time` datetime NULL COMMENT '创建时间', |
|||
`update_by` bigint NULL COMMENT '修改人ID', |
|||
`update_time` datetime NULL COMMENT '更新时间', |
|||
`is_deleted` tinyint DEFAULT 0 COMMENT '逻辑删除标识(1-已删除 0-未删除)', |
|||
PRIMARY KEY (`id`) USING BTREE, |
|||
UNIQUE INDEX `uk_code`(`code` ASC) USING BTREE COMMENT '部门编号唯一索引' |
|||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '部门管理表'; |
|||
|
|||
-- ---------------------------- |
|||
-- Records of sys_dept |
|||
-- ---------------------------- |
|||
INSERT INTO `sys_dept` VALUES (1, '有来技术', 'YOULAI', 0, '0', 1, 1, 1, NULL, 1, now(), 0); |
|||
INSERT INTO `sys_dept` VALUES (2, '研发部门', 'RD001', 1, '0,1', 1, 1, 2, NULL, 2, now(), 0); |
|||
INSERT INTO `sys_dept` VALUES (3, '测试部门', 'QA001', 1, '0,1', 1, 1, 2, NULL, 2, now(), 0); |
|||
|
|||
-- ---------------------------- |
|||
-- Table structure for sys_dict |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `sys_dict`; |
|||
CREATE TABLE `sys_dict` ( |
|||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键 ', |
|||
`dict_code` varchar(50) COMMENT '类型编码', |
|||
`name` varchar(50) COMMENT '类型名称', |
|||
`status` tinyint(1) DEFAULT '0' COMMENT '状态(0:正常;1:禁用)', |
|||
`remark` varchar(255) COMMENT '备注', |
|||
`create_time` datetime COMMENT '创建时间', |
|||
`create_by` bigint COMMENT '创建人ID', |
|||
`update_time` datetime COMMENT '更新时间', |
|||
`update_by` bigint COMMENT '修改人ID', |
|||
`is_deleted` tinyint DEFAULT '0' COMMENT '是否删除(1-删除,0-未删除)', |
|||
PRIMARY KEY (`id`) USING BTREE, |
|||
KEY `idx_dict_code` (`dict_code`) |
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据字典类型表'; |
|||
-- ---------------------------- |
|||
-- Records of sys_dict |
|||
-- ---------------------------- |
|||
INSERT INTO `sys_dict` VALUES (1, 'gender', '性别', 1, NULL, now() , 1,now(), 1,0); |
|||
INSERT INTO `sys_dict` VALUES (2, 'notice_type', '通知类型', 1, NULL, now(), 1,now(), 1,0); |
|||
INSERT INTO `sys_dict` VALUES (3, 'notice_level', '通知级别', 1, NULL, now(), 1,now(), 1,0); |
|||
|
|||
|
|||
-- ---------------------------- |
|||
-- Table structure for sys_dict_item |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `sys_dict_item`; |
|||
CREATE TABLE `sys_dict_item` ( |
|||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', |
|||
`dict_code` varchar(50) COMMENT '关联字典编码,与sys_dict表中的dict_code对应', |
|||
`value` varchar(50) COMMENT '字典项值', |
|||
`label` varchar(100) COMMENT '字典项标签', |
|||
`tag_type` varchar(50) COMMENT '标签类型,用于前端样式展示(如success、warning等)', |
|||
`status` tinyint DEFAULT '0' COMMENT '状态(1-正常,0-禁用)', |
|||
`sort` int DEFAULT '0' COMMENT '排序', |
|||
`remark` varchar(255) COMMENT '备注', |
|||
`create_time` datetime COMMENT '创建时间', |
|||
`create_by` bigint COMMENT '创建人ID', |
|||
`update_time` datetime COMMENT '更新时间', |
|||
`update_by` bigint COMMENT '修改人ID', |
|||
PRIMARY KEY (`id`) USING BTREE |
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据字典项表'; |
|||
|
|||
-- ---------------------------- |
|||
-- Records of sys_dict_item |
|||
-- ---------------------------- |
|||
INSERT INTO `sys_dict_item` VALUES (1, 'gender', '1', '男', 'primary', 1, 1, NULL, now(), 1,now(),1); |
|||
INSERT INTO `sys_dict_item` VALUES (2, 'gender', '2', '女', 'danger', 1, 2, NULL, now(), 1,now(),1); |
|||
INSERT INTO `sys_dict_item` VALUES (3, 'gender', '0', '保密', 'info', 1, 3, NULL, now(), 1,now(),1); |
|||
INSERT INTO `sys_dict_item` VALUES (4, 'notice_type', '1', '系统升级', 'success', 1, 1, '', now(), 1,now(),1); |
|||
INSERT INTO `sys_dict_item` VALUES (5, 'notice_type', '2', '系统维护', 'primary', 1, 2, '', now(), 1,now(),1); |
|||
INSERT INTO `sys_dict_item` VALUES (6, 'notice_type', '3', '安全警告', 'danger', 1, 3, '', now(), 1,now(),1); |
|||
INSERT INTO `sys_dict_item` VALUES (7, 'notice_type', '4', '假期通知', 'success', 1, 4, '', now(), 1,now(),1); |
|||
INSERT INTO `sys_dict_item` VALUES (8, 'notice_type', '5', '公司新闻', 'primary', 1, 5, '', now(), 1,now(),1); |
|||
INSERT INTO `sys_dict_item` VALUES (9, 'notice_type', '99', '其他', 'info', 1, 99, '', now(), 1,now(),1); |
|||
INSERT INTO `sys_dict_item` VALUES (10, 'notice_level', 'L', '低', 'info', 1, 1, '', now(), 1,now(),1); |
|||
INSERT INTO `sys_dict_item` VALUES (11, 'notice_level', 'M', '中', 'warning', 1, 2, '', now(), 1,now(),1); |
|||
INSERT INTO `sys_dict_item` VALUES (12, 'notice_level', 'H', '高', 'danger', 1, 3, '', now(), 1,now(),1); |
|||
|
|||
-- ---------------------------- |
|||
-- Table structure for sys_menu |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `sys_menu`; |
|||
CREATE TABLE `sys_menu` ( |
|||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', |
|||
`parent_id` bigint NOT NULL COMMENT '父菜单ID', |
|||
`tree_path` varchar(255) COMMENT '父节点ID路径', |
|||
`name` varchar(64) NOT NULL COMMENT '菜单名称', |
|||
`type` char(1) NOT NULL COMMENT '菜单类型(C-目录 M-菜单 B-按钮)', |
|||
`route_name` varchar(255) COMMENT '路由名称(Vue Router 中用于命名路由)', |
|||
`route_path` varchar(128) COMMENT '路由路径(Vue Router 中定义的 URL 路径)', |
|||
`component` varchar(128) COMMENT '组件路径(组件页面完整路径,相对于 src/views/,缺省后缀 .vue)', |
|||
`perm` varchar(128) COMMENT '【按钮】权限标识', |
|||
`always_show` tinyint DEFAULT 0 COMMENT '【目录】只有一个子路由是否始终显示(1-是 0-否)', |
|||
`keep_alive` tinyint DEFAULT 0 COMMENT '【菜单】是否开启页面缓存(1-是 0-否)', |
|||
`visible` tinyint(1) DEFAULT 1 COMMENT '显示状态(1-显示 0-隐藏)', |
|||
`sort` int DEFAULT 0 COMMENT '排序', |
|||
`icon` varchar(64) COMMENT '菜单图标', |
|||
`redirect` varchar(128) COMMENT '跳转路径', |
|||
`create_time` datetime NULL COMMENT '创建时间', |
|||
`update_time` datetime NULL COMMENT '更新时间', |
|||
`params` varchar(255) NULL COMMENT '路由参数', |
|||
PRIMARY KEY (`id`) USING BTREE |
|||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统菜单表'; |
|||
|
|||
-- ---------------------------- |
|||
-- Records of sys_menu |
|||
-- ---------------------------- |
|||
-- 顶级目录:系统管理/代码生成/平台文档/接口文档 |
|||
INSERT INTO `sys_menu` VALUES (1, 0, '0', '系统管理', 'C', '', '/system', 'Layout', NULL, NULL, NULL, 1, 1, 'system', '/system/user', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2, 0, '0', '代码生成', 'C', '', '/codegen', 'Layout', NULL, NULL, NULL, 1, 2, 'code', '/codegen/index', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (4, 0, '0', '平台文档', 'C', '', '/doc', 'Layout', NULL, NULL, NULL, 1, 4, 'document', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (5, 0, '0', '接口文档', 'C', '', '/api', 'Layout', NULL, NULL, NULL, 1, 5, 'api', '', now(), now(), NULL); |
|||
|
|||
-- 系统管理 |
|||
INSERT INTO `sys_menu` VALUES (210, 1, '0,1', '用户管理', 'M', 'User', 'user', 'system/user/index', NULL, NULL, 1, 1, 1, 'el-icon-User', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2101, 210, '0,1,210', '用户查询', 'B', NULL, '', NULL, 'sys:user:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2102, 210, '0,1,210', '用户新增', 'B', NULL, '', NULL, 'sys:user:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2103, 210, '0,1,210', '用户编辑', 'B', NULL, '', NULL, 'sys:user:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2104, 210, '0,1,210', '用户删除', 'B', NULL, '', NULL, 'sys:user:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2105, 210, '0,1,210', '重置密码', 'B', NULL, '', NULL, 'sys:user:reset-password', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2106, 210, '0,1,210', '用户导入', 'B', NULL, '', NULL, 'sys:user:import', NULL, NULL, 1, 6, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2107, 210, '0,1,210', '用户导出', 'B', NULL, '', NULL, 'sys:user:export', NULL, NULL, 1, 7, '', NULL, now(), now(), NULL); |
|||
|
|||
INSERT INTO `sys_menu` VALUES (220, 1, '0,1', '角色管理', 'M', 'Role', 'role', 'system/role/index', NULL, NULL, 1, 1, 2, 'role', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2201, 220, '0,1,220', '角色查询', 'B', NULL, '', NULL, 'sys:role:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2202, 220, '0,1,220', '角色新增', 'B', NULL, '', NULL, 'sys:role:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2203, 220, '0,1,220', '角色编辑', 'B', NULL, '', NULL, 'sys:role:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2204, 220, '0,1,220', '角色删除', 'B', NULL, '', NULL, 'sys:role:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2205, 220, '0,1,220', '角色分配权限', 'B', NULL, '', NULL, 'sys:role:assign', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL); |
|||
|
|||
INSERT INTO `sys_menu` VALUES (230, 1, '0,1', '菜单管理', 'M', 'SysMenu', 'menu', 'system/menu/index', NULL, NULL, 1, 1, 3, 'menu', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2301, 230, '0,1,230', '菜单查询', 'B', NULL, '', NULL, 'sys:menu:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2302, 230, '0,1,230', '菜单新增', 'B', NULL, '', NULL, 'sys:menu:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2303, 230, '0,1,230', '菜单编辑', 'B', NULL, '', NULL, 'sys:menu:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2304, 230, '0,1,230', '菜单删除', 'B', NULL, '', NULL, 'sys:menu:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); |
|||
|
|||
INSERT INTO `sys_menu` VALUES (240, 1, '0,1', '部门管理', 'M', 'Dept', 'dept', 'system/dept/index', NULL, NULL, 1, 1, 4, 'tree', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2401, 240, '0,1,240', '部门查询', 'B', NULL, '', NULL, 'sys:dept:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2402, 240, '0,1,240', '部门新增', 'B', NULL, '', NULL, 'sys:dept:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2403, 240, '0,1,240', '部门编辑', 'B', NULL, '', NULL, 'sys:dept:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2404, 240, '0,1,240', '部门删除', 'B', NULL, '', NULL, 'sys:dept:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); |
|||
|
|||
INSERT INTO `sys_menu` VALUES (250, 1, '0,1', '字典管理', 'M', 'Dict', 'dict', 'system/dict/index', NULL, NULL, 1, 1, 5, 'dict', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2501, 250, '0,1,250', '字典查询', 'B', NULL, '', NULL, 'sys:dict:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2502, 250, '0,1,250', '字典新增', 'B', NULL, '', NULL, 'sys:dict:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2503, 250, '0,1,250', '字典编辑', 'B', NULL, '', NULL, 'sys:dict:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2504, 250, '0,1,250', '字典删除', 'B', NULL, '', NULL, 'sys:dict:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); |
|||
|
|||
INSERT INTO `sys_menu` VALUES (251, 1, '0,1', '字典项', 'M', 'DictItem', 'dict-item', 'system/dict/dict-item', NULL, 0, 1, 0, 6, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2511, 251, '0,1,251', '字典项查询', 'B', NULL, '', NULL, 'sys:dict-item:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2512, 251, '0,1,251', '字典项新增', 'B', NULL, '', NULL, 'sys:dict-item:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2513, 251, '0,1,251', '字典项编辑', 'B', NULL, '', NULL, 'sys:dict-item:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2514, 251, '0,1,251', '字典项删除', 'B', NULL, '', NULL, 'sys:dict-item:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); |
|||
|
|||
INSERT INTO `sys_menu` VALUES (260, 1, '0,1', '系统日志', 'M', 'Log', 'log', 'system/log/index', NULL, 0, 1, 1, 7, 'document', NULL, now(), now(), NULL); |
|||
|
|||
INSERT INTO `sys_menu` VALUES (270, 1, '0,1', '系统配置', 'M', 'Config', 'config', 'system/config/index', NULL, 0, 1, 1, 8, 'setting', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2701, 270, '0,1,270', '系统配置查询', 'B', NULL, '', NULL, 'sys:config:list', 0, 1, 1, 1, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2702, 270, '0,1,270', '系统配置新增', 'B', NULL, '', NULL, 'sys:config:create', 0, 1, 1, 2, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2703, 270, '0,1,270', '系统配置修改', 'B', NULL, '', NULL, 'sys:config:update', 0, 1, 1, 3, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2704, 270, '0,1,270', '系统配置删除', 'B', NULL, '', NULL, 'sys:config:delete', 0, 1, 1, 4, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2705, 270, '0,1,270', '系统配置刷新', 'B', NULL, '', NULL, 'sys:config:refresh', 0, 1, 1, 5, '', NULL, now(), now(), NULL); |
|||
|
|||
INSERT INTO `sys_menu` VALUES (280, 1, '0,1', '通知公告', 'M', 'Notice', 'notice', 'system/notice/index', NULL, NULL, NULL, 1, 9, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2801, 280, '0,1,280', '通知查询', 'B', NULL, '', NULL, 'sys:notice:list', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2802, 280, '0,1,280', '通知新增', 'B', NULL, '', NULL, 'sys:notice:create', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2803, 280, '0,1,280', '通知编辑', 'B', NULL, '', NULL, 'sys:notice:update', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2804, 280, '0,1,280', '通知删除', 'B', NULL, '', NULL, 'sys:notice:delete', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2805, 280, '0,1,280', '通知发布', 'B', NULL, '', NULL, 'sys:notice:publish', 0, 1, 1, 5, '', NULL, now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (2806, 280, '0,1,280', '通知撤回', 'B', NULL, '', NULL, 'sys:notice:revoke', 0, 1, 1, 6, '', NULL, now(), now(), NULL); |
|||
|
|||
-- 代码生成 |
|||
INSERT INTO `sys_menu` VALUES (310, 2, '0,2', '代码生成', 'M', 'Codegen', 'codegen', 'codegen/index', NULL, NULL, 1, 1, 1, 'code', NULL, now(), now(), NULL); |
|||
|
|||
-- 平台文档(外链通过 route_path 识别) |
|||
INSERT INTO `sys_menu` VALUES (501, 4, '0,4', '平台文档(外链)', 'M', NULL, 'https://juejin.cn/post/7228990409909108793', '', NULL, NULL, NULL, 1, 1, 'document', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (502, 4, '0,4', '后端文档', 'M', NULL, 'https://youlai.blog.csdn.net/article/details/145178880', '', NULL, NULL, NULL, 1, 2, 'document', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (503, 4, '0,4', '移动端文档', 'M', NULL, 'https://youlai.blog.csdn.net/article/details/143222890', '', NULL, NULL, NULL, 1, 3, 'document', '', now(), now(), NULL); |
|||
INSERT INTO `sys_menu` VALUES (504, 4, '0,4', '内部文档', 'M', NULL, 'internal-doc', 'demo/internal-doc', NULL, NULL, NULL, 1, 4, 'document', '', now(), now(), NULL); |
|||
|
|||
-- 接口文档 |
|||
INSERT INTO `sys_menu` VALUES (601, 5, '0,5', 'Apifox', 'M', 'Apifox', 'apifox', 'demo/api/apifox', NULL, NULL, 1, 1, 1, 'api', '', now(), now(), NULL); |
|||
|
|||
-- ---------------------------- |
|||
-- Table structure for sys_role |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `sys_role`; |
|||
CREATE TABLE `sys_role` ( |
|||
`id` bigint NOT NULL AUTO_INCREMENT, |
|||
`name` varchar(64) NOT NULL COMMENT '角色名称', |
|||
`code` varchar(32) NOT NULL COMMENT '角色编码', |
|||
`sort` int NULL COMMENT '显示顺序', |
|||
`status` tinyint(1) DEFAULT 1 COMMENT '角色状态(1-正常 0-停用)', |
|||
`data_scope` tinyint NULL COMMENT '数据权限(1-所有数据 2-部门及子部门数据 3-本部门数据 4-本人数据 5-自定义部门数据)', |
|||
`create_by` bigint NULL COMMENT '创建人 ID', |
|||
`create_time` datetime NULL COMMENT '创建时间', |
|||
`update_by` bigint NULL COMMENT '更新人ID', |
|||
`update_time` datetime NULL COMMENT '更新时间', |
|||
`is_deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)', |
|||
PRIMARY KEY (`id`) USING BTREE, |
|||
UNIQUE INDEX `uk_name`(`name` ASC) USING BTREE COMMENT '角色名称唯一索引', |
|||
UNIQUE INDEX `uk_code`(`code` ASC) USING BTREE COMMENT '角色编码唯一索引' |
|||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统角色表'; |
|||
|
|||
-- ---------------------------- |
|||
-- Records of sys_role |
|||
-- ---------------------------- |
|||
INSERT INTO `sys_role` VALUES (1, '超级管理员', 'ROOT', 1, 1, 1, NULL, now(), NULL, now(), 0); |
|||
INSERT INTO `sys_role` VALUES (2, '系统管理员', 'ADMIN', 2, 1, 1, NULL, now(), NULL, NULL, 0); |
|||
INSERT INTO `sys_role` VALUES (3, '访问游客', 'GUEST', 3, 1, 3, NULL, now(), NULL, now(), 0); |
|||
INSERT INTO `sys_role` VALUES (4, '部门主管', 'DEPT_MANAGER', 4, 1, 2, NULL, now(), NULL, now(), 0); |
|||
INSERT INTO `sys_role` VALUES (5, '部门成员', 'DEPT_MEMBER', 5, 1, 3, NULL, now(), NULL, now(), 0); |
|||
INSERT INTO `sys_role` VALUES (6, '普通员工', 'EMPLOYEE', 6, 1, 4, NULL, now(), NULL, now(), 0); |
|||
INSERT INTO `sys_role` VALUES (7, '自定义权限用户', 'CUSTOM_USER', 7, 1, 5, NULL, now(), NULL, now(), 0); |
|||
|
|||
-- ---------------------------- |
|||
-- Table structure for sys_role_menu |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `sys_role_menu`; |
|||
CREATE TABLE `sys_role_menu` ( |
|||
`role_id` bigint NOT NULL COMMENT '角色ID', |
|||
`menu_id` bigint NOT NULL COMMENT '菜单ID', |
|||
UNIQUE INDEX `uk_roleid_menuid`(`role_id` ASC, `menu_id` ASC) USING BTREE COMMENT '角色菜单唯一索引' |
|||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '角色菜单关联表'; |
|||
|
|||
-- ---------------------------- |
|||
-- Table structure for sys_role_dept |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `sys_role_dept`; |
|||
CREATE TABLE `sys_role_dept` ( |
|||
`role_id` bigint NOT NULL COMMENT '角色ID', |
|||
`dept_id` bigint NOT NULL COMMENT '部门ID', |
|||
UNIQUE INDEX `uk_roleid_deptid`(`role_id` ASC, `dept_id` ASC) USING BTREE COMMENT '角色部门唯一索引' |
|||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '角色部门关联表'; |
|||
|
|||
-- ---------------------------- |
|||
-- Records of sys_role_dept |
|||
-- ---------------------------- |
|||
INSERT IGNORE INTO `sys_role_dept` VALUES (7, 1); |
|||
INSERT IGNORE INTO `sys_role_dept` VALUES (7, 2); |
|||
|
|||
-- ============================================ |
|||
-- 系统管理员角色菜单权限(role_id=2) |
|||
-- 顶级目录 |
|||
INSERT INTO `sys_role_menu` VALUES (2, 1), (2, 2), (2, 4), (2, 5); |
|||
-- 系统管理 |
|||
INSERT INTO `sys_role_menu` VALUES (2, 210), (2, 2101), (2, 2102), (2, 2103), (2, 2104), (2, 2105), (2, 2106), (2, 2107); |
|||
INSERT INTO `sys_role_menu` VALUES (2, 220), (2, 2201), (2, 2202), (2, 2203), (2, 2204), (2, 2205); |
|||
INSERT INTO `sys_role_menu` VALUES (2, 230), (2, 2301), (2, 2302), (2, 2303), (2, 2304); |
|||
INSERT INTO `sys_role_menu` VALUES (2, 240), (2, 2401), (2, 2402), (2, 2403), (2, 2404); |
|||
INSERT INTO `sys_role_menu` VALUES (2, 250), (2, 2501), (2, 2502), (2, 2503), (2, 2504); |
|||
INSERT INTO `sys_role_menu` VALUES (2, 251), (2, 2511), (2, 2512), (2, 2513), (2, 2514); |
|||
INSERT INTO `sys_role_menu` VALUES (2, 260), (2, 2601); |
|||
INSERT INTO `sys_role_menu` VALUES (2, 270), (2, 2701), (2, 2702), (2, 2703), (2, 2704), (2, 2705); |
|||
INSERT INTO `sys_role_menu` VALUES (2, 280), (2, 2801), (2, 2802), (2, 2803), (2, 2804), (2, 2805), (2, 2806); |
|||
|
|||
INSERT IGNORE INTO `sys_role_menu` VALUES (4, 1); |
|||
INSERT IGNORE INTO `sys_role_menu` VALUES (4, 210), (4, 2101), (4, 2102), (4, 2103), (4, 2104), (4, 2105), (4, 2106), (4, 2107); |
|||
INSERT IGNORE INTO `sys_role_menu` VALUES (4, 220), (4, 2201), (4, 2202), (4, 2203), (4, 2204), (4, 2205); |
|||
|
|||
INSERT IGNORE INTO `sys_role_menu` VALUES (5, 1); |
|||
INSERT IGNORE INTO `sys_role_menu` VALUES (5, 210), (5, 2101), (5, 2102), (5, 2103), (5, 2104), (5, 2105), (5, 2106), (5, 2107); |
|||
INSERT IGNORE INTO `sys_role_menu` VALUES (5, 220), (5, 2201), (5, 2202), (5, 2203), (5, 2204), (5, 2205); |
|||
|
|||
INSERT IGNORE INTO `sys_role_menu` VALUES (6, 1); |
|||
INSERT IGNORE INTO `sys_role_menu` VALUES (6, 210), (6, 2101), (6, 2102), (6, 2103), (6, 2104), (6, 2105), (6, 2106), (6, 2107); |
|||
INSERT IGNORE INTO `sys_role_menu` VALUES (6, 220), (6, 2201), (6, 2202), (6, 2203), (6, 2204), (6, 2205); |
|||
|
|||
INSERT IGNORE INTO `sys_role_menu` VALUES (7, 1); |
|||
INSERT IGNORE INTO `sys_role_menu` VALUES (7, 210), (7, 2101), (7, 2102), (7, 2103), (7, 2104), (7, 2105), (7, 2106), (7, 2107); |
|||
INSERT IGNORE INTO `sys_role_menu` VALUES (7, 220), (7, 2201), (7, 2202), (7, 2203), (7, 2204), (7, 2205); |
|||
-- 代码生成 |
|||
INSERT INTO `sys_role_menu` VALUES (2, 310); |
|||
-- 平台文档 |
|||
INSERT INTO `sys_role_menu` VALUES (2, 501), (2, 502), (2, 503), (2, 504); |
|||
-- 接口文档 |
|||
INSERT INTO `sys_role_menu` VALUES (2, 601); |
|||
|
|||
-- ---------------------------- |
|||
-- Table structure for sys_user |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `sys_user`; |
|||
CREATE TABLE `sys_user` ( |
|||
`id` bigint NOT NULL AUTO_INCREMENT, |
|||
`username` varchar(64) COMMENT '用户名', |
|||
`nickname` varchar(64) COMMENT '昵称', |
|||
`gender` tinyint(1) DEFAULT 1 COMMENT '性别((1-男 2-女 0-保密)', |
|||
`password` varchar(100) COMMENT '密码', |
|||
`dept_id` int COMMENT '部门ID', |
|||
`avatar` varchar(255) COMMENT '用户头像', |
|||
`mobile` varchar(20) COMMENT '联系方式', |
|||
`status` tinyint(1) DEFAULT 1 COMMENT '状态(1-正常 0-禁用)', |
|||
`email` varchar(128) COMMENT '用户邮箱', |
|||
`create_time` datetime COMMENT '创建时间', |
|||
`create_by` bigint COMMENT '创建人ID', |
|||
`update_time` datetime COMMENT '更新时间', |
|||
`update_by` bigint COMMENT '修改人ID', |
|||
`is_deleted` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)', |
|||
PRIMARY KEY (`id`) USING BTREE |
|||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '系统用户表'; |
|||
|
|||
-- ---------------------------- |
|||
-- Records of sys_user |
|||
-- ---------------------------- |
|||
INSERT INTO `sys_user` VALUES (1, 'root', '有来技术', 0, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', NULL, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345677', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0); |
|||
INSERT INTO `sys_user` VALUES (2, 'admin', '系统管理员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18888888888', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0); |
|||
INSERT INTO `sys_user` VALUES (3, 'test', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345679', 1, 'youlaitech@163.com', now(), NULL, now(), NULL, 0); |
|||
INSERT INTO `sys_user` VALUES (4, 'dept_manager', '部门主管', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345680', 1, 'manager@youlaitech.com', now(), NULL, now(), NULL, 0); |
|||
INSERT INTO `sys_user` VALUES (5, 'dept_member', '部门成员', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 1, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345681', 1, 'member@youlaitech.com', now(), NULL, now(), NULL, 0); |
|||
INSERT INTO `sys_user` VALUES (6, 'employee', '普通员工', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 2, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345682', 1, 'employee@youlaitech.com', now(), NULL, now(), NULL, 0); |
|||
INSERT INTO `sys_user` VALUES (7, 'custom_user', '自定义权限用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18812345683', 1, 'custom@youlaitech.com', now(), NULL, now(), NULL, 0); |
|||
|
|||
-- ---------------------------- |
|||
-- Table structure for sys_user_role |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `sys_user_role`; |
|||
CREATE TABLE `sys_user_role` ( |
|||
`user_id` bigint NOT NULL COMMENT '用户ID', |
|||
`role_id` bigint NOT NULL COMMENT '角色ID', |
|||
PRIMARY KEY (`user_id`, `role_id`) USING BTREE |
|||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '用户角色关联表'; |
|||
|
|||
-- ---------------------------- |
|||
-- Records of sys_user_role |
|||
-- ---------------------------- |
|||
INSERT INTO `sys_user_role` VALUES (1, 1); |
|||
INSERT INTO `sys_user_role` VALUES (2, 2); |
|||
INSERT INTO `sys_user_role` VALUES (3, 3); |
|||
INSERT IGNORE INTO `sys_user_role` VALUES (4, 4); |
|||
INSERT IGNORE INTO `sys_user_role` VALUES (5, 5); |
|||
INSERT IGNORE INTO `sys_user_role` VALUES (6, 6); |
|||
INSERT IGNORE INTO `sys_user_role` VALUES (7, 7); |
|||
|
|||
-- ---------------------------- |
|||
-- Table structure for sys_log |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `sys_log`; |
|||
CREATE TABLE `sys_log` ( |
|||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', |
|||
`module` TINYINT NOT NULL COMMENT '模块,数字枚举,参考 LogModule 枚举', |
|||
`action_type` TINYINT NOT NULL COMMENT '操作类型,数字枚举,参考 ActionType 枚举', |
|||
`title` VARCHAR(100) NOT NULL COMMENT '前端显示标题', |
|||
`content` TEXT COMMENT '自定义日志内容', |
|||
`operator_id` BIGINT COMMENT '操作人ID', |
|||
`operator_name` VARCHAR(50) COMMENT '操作人名称', |
|||
`request_uri` VARCHAR(255) COMMENT '请求路径', |
|||
`request_method` VARCHAR(10) COMMENT '请求方法', |
|||
`ip` VARCHAR(45) COMMENT 'IP地址', |
|||
`province` VARCHAR(100) COMMENT '省份', |
|||
`city` VARCHAR(100) COMMENT '城市', |
|||
`device` VARCHAR(100) COMMENT '设备', |
|||
`os` VARCHAR(100) COMMENT '操作系统', |
|||
`browser` VARCHAR(100) COMMENT '浏览器', |
|||
`status` TINYINT DEFAULT 1 COMMENT '0失败 1成功', |
|||
`error_msg` VARCHAR(255) COMMENT '错误信息', |
|||
`execution_time` INT COMMENT '执行时间(ms)', |
|||
`create_time` DATETIME COMMENT '操作时间', |
|||
PRIMARY KEY (`id`) USING BTREE, |
|||
KEY `idx_module_action_time` (`module`, `action_type`, `create_time`), |
|||
KEY `idx_operator_time` (`operator_id`, `create_time`), |
|||
KEY `idx_time` (`create_time`) |
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统操作日志表'; |
|||
|
|||
-- ---------------------------- |
|||
-- Table structure for gen_table |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `gen_table`; |
|||
CREATE TABLE `gen_table` ( |
|||
`id` bigint NOT NULL AUTO_INCREMENT, |
|||
`table_name` varchar(100) NOT NULL COMMENT '表名', |
|||
`module_name` varchar(100) COMMENT '模块名', |
|||
`package_name` varchar(255) NOT NULL COMMENT '包名', |
|||
`business_name` varchar(100) NOT NULL COMMENT '业务名', |
|||
`entity_name` varchar(100) NOT NULL COMMENT '实体类名', |
|||
`author` varchar(50) NOT NULL COMMENT '作者', |
|||
`parent_menu_id` bigint COMMENT '上级菜单ID,对应sys_menu的id ', |
|||
`remove_table_prefix` varchar(20) COMMENT '要移除的表前缀,如: sys_', |
|||
`page_type` varchar(20) COMMENT '页面类型(classic|curd)', |
|||
`create_time` datetime COMMENT '创建时间', |
|||
`update_time` datetime COMMENT '更新时间', |
|||
`is_deleted` tinyint(4) DEFAULT 0 COMMENT '是否删除', |
|||
PRIMARY KEY (`id`), |
|||
UNIQUE KEY `uk_tablename` (`table_name`) |
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成配置表'; |
|||
|
|||
-- ---------------------------- |
|||
-- Table structure for gen_table_column |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `gen_table_column`; |
|||
CREATE TABLE `gen_table_column` ( |
|||
`id` bigint NOT NULL AUTO_INCREMENT, |
|||
`table_id` bigint NOT NULL COMMENT '关联的表配置ID', |
|||
`column_name` varchar(100) , |
|||
`column_type` varchar(50) , |
|||
`column_length` int , |
|||
`field_name` varchar(100) NOT NULL COMMENT '字段名称', |
|||
`field_type` varchar(100) COMMENT '字段类型', |
|||
`field_sort` int COMMENT '字段排序', |
|||
`field_comment` varchar(255) COMMENT '字段描述', |
|||
`max_length` int , |
|||
`is_required` tinyint(1) COMMENT '是否必填', |
|||
`is_show_in_list` tinyint(1) DEFAULT '0' COMMENT '是否在列表显示', |
|||
`is_show_in_form` tinyint(1) DEFAULT '0' COMMENT '是否在表单显示', |
|||
`is_show_in_query` tinyint(1) DEFAULT '0' COMMENT '是否在查询条件显示', |
|||
`query_type` tinyint COMMENT '查询方式', |
|||
`form_type` tinyint COMMENT '表单类型', |
|||
`dict_type` varchar(50) COMMENT '字典类型', |
|||
`create_time` datetime COMMENT '创建时间', |
|||
`update_time` datetime COMMENT '更新时间', |
|||
PRIMARY KEY (`id`), |
|||
KEY `idx_table_id` (`table_id`) |
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代码生成字段配置表'; |
|||
|
|||
-- ---------------------------- |
|||
-- 系统配置表 |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `sys_config`; |
|||
CREATE TABLE `sys_config` ( |
|||
`id` bigint NOT NULL AUTO_INCREMENT, |
|||
`config_name` varchar(50) NOT NULL COMMENT '配置名称', |
|||
`config_key` varchar(50) NOT NULL COMMENT '配置key', |
|||
`config_value` varchar(100) NOT NULL COMMENT '配置值', |
|||
`remark` varchar(255) COMMENT '备注', |
|||
`create_time` datetime COMMENT '创建时间', |
|||
`create_by` bigint COMMENT '创建人ID', |
|||
`update_time` datetime COMMENT '更新时间', |
|||
`update_by` bigint COMMENT '更新人ID', |
|||
`is_deleted` tinyint(4) DEFAULT '0' NOT NULL COMMENT '逻辑删除标识(0-未删除 1-已删除)', |
|||
PRIMARY KEY (`id`) |
|||
) ENGINE=InnoDB COMMENT='系统配置表'; |
|||
|
|||
INSERT INTO `sys_config` VALUES (1, '系统限流QPS', 'IP_QPS_THRESHOLD_LIMIT', '10', '单个IP请求的最大每秒查询数(QPS)阈值Key', now(), 1, NULL, NULL, 0); |
|||
|
|||
-- ---------------------------- |
|||
-- 通知公告表 |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `sys_notice`; |
|||
CREATE TABLE `sys_notice` ( |
|||
`id` bigint NOT NULL AUTO_INCREMENT, |
|||
`title` varchar(50) COMMENT '通知标题', |
|||
`content` text COMMENT '通知内容', |
|||
`type` tinyint NOT NULL COMMENT '通知类型(关联字典编码:notice_type)', |
|||
`level` varchar(5) NOT NULL COMMENT '通知等级(字典code:notice_level)', |
|||
`target_type` tinyint NOT NULL COMMENT '目标类型(1: 全体, 2: 指定)', |
|||
`target_user_ids` varchar(255) COMMENT '目标人ID集合(多个使用英文逗号,分割)', |
|||
`publisher_id` bigint COMMENT '发布人ID', |
|||
`publish_status` tinyint DEFAULT '0' COMMENT '发布状态(0: 未发布, 1: 已发布, -1: 已撤回)', |
|||
`publish_time` datetime COMMENT '发布时间', |
|||
`revoke_time` datetime COMMENT '撤回时间', |
|||
`create_by` bigint NOT NULL COMMENT '创建人ID', |
|||
`create_time` datetime NOT NULL COMMENT '创建时间', |
|||
`update_by` bigint COMMENT '更新人ID', |
|||
`update_time` datetime COMMENT '更新时间', |
|||
`is_deleted` tinyint(1) DEFAULT '0' COMMENT '是否删除(0: 未删除, 1: 已删除)', |
|||
PRIMARY KEY (`id`) USING BTREE |
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统通知公告表'; |
|||
|
|||
INSERT INTO `sys_notice` VALUES (1, 'v3.0.0 版本发布 - 多租户功能上线', '<p>🎉 新版本发布,主要更新内容:</p><p>1. 新增多租户功能,支持租户隔离和数据管理</p><p>2. 优化系统性能,提升响应速度</p><p>3. 完善权限管理,增强安全性</p><p>4. 修复已知问题,提升系统稳定性</p>', 1, 'H', 1, NULL, 1, 1, '2024-12-15 10:00:00', NULL, 1, '2024-12-15 10:00:00', 1, '2024-12-15 10:00:00', 0); |
|||
INSERT INTO `sys_notice` VALUES (2, '系统维护通知 - 2024年12月20日', '<p>⏰ 系统维护通知</p><p>系统将于 <strong>2024年12月20日(本周五)凌晨 2:00-4:00</strong> 进行例行维护升级。</p><p>维护期间系统将暂停服务,请提前做好数据备份工作。</p><p>给您带来的不便,敬请谅解!</p>', 2, 'H', 1, NULL, 1, 1, '2024-12-18 14:30:00', NULL, 1, '2024-12-18 14:30:00', 1, '2024-12-18 14:30:00', 0); |
|||
INSERT INTO `sys_notice` VALUES (3, '安全提醒 - 防范钓鱼邮件', '<p>⚠️ 安全提醒</p><p>近期发现有不法分子通过钓鱼邮件进行网络攻击,请大家提高警惕:</p><p>1. 不要点击来源不明的邮件链接</p><p>2. 不要下载可疑附件</p><p>3. 遇到可疑邮件请及时联系IT部门</p><p>4. 定期修改密码,使用强密码策略</p>', 3, 'H', 1, NULL, 1, 1, '2024-12-10 09:00:00', NULL, 1, '2024-12-10 09:00:00', 1, '2024-12-10 09:00:00', 0); |
|||
INSERT INTO `sys_notice` VALUES (4, '元旦假期安排通知', '<p>📅 元旦假期安排</p><p>根据国家法定节假日安排,公司元旦假期时间为:</p><p><strong>2024年12月30日(周一)至 2025年1月1日(周三)</strong>,共3天。</p><p>2024年12月29日(周日)正常上班。</p><p>祝大家元旦快乐,假期愉快!</p>', 4, 'M', 1, NULL, 1, 1, '2024-12-25 16:00:00', NULL, 1, '2024-12-25 16:00:00', 1, '2024-12-25 16:00:00', 0); |
|||
INSERT INTO `sys_notice` VALUES (5, '新产品发布会邀请', '<p>🎊 新产品发布会邀请</p><p>公司将于 <strong>2025年1月15日下午14:00</strong> 在总部会议室举办新产品发布会。</p><p>届时将展示最新研发的产品和技术成果,欢迎全体员工参加。</p><p>请各部门提前安排好工作,准时参加。</p>', 5, 'M', 1, NULL, 1, 1, '2024-12-28 11:00:00', NULL, 1, '2024-12-28 11:00:00', 1, '2024-12-28 11:00:00', 0); |
|||
INSERT INTO `sys_notice` VALUES (6, 'v2.16.1 版本更新', '<p>✨ 版本更新</p><p>v2.16.1 版本已发布,主要修复内容:</p><p>1. 修复 WebSocket 重复连接导致的后台线程阻塞问题</p><p>2. 优化通知公告功能,提升用户体验</p><p>3. 修复部分已知bug</p><p>建议尽快更新到最新版本。</p>', 1, 'M', 1, NULL, 1, 1, '2024-12-05 15:30:00', NULL, 1, '2024-12-05 15:30:00', 1, '2024-12-05 15:30:00', 0); |
|||
INSERT INTO `sys_notice` VALUES (7, '年终总结会议通知', '<p>📋 年终总结会议通知</p><p>各部门年终总结会议将于 <strong>2024年12月30日上午9:00</strong> 召开。</p><p>请各部门负责人提前准备好年度工作总结和下年度工作计划。</p><p>会议地点:总部大会议室</p>', 5, 'M', 2, '1,2', 1, 1, '2024-12-22 10:00:00', NULL, 1, '2024-12-22 10:00:00', 1, '2024-12-22 10:00:00', 0); |
|||
INSERT INTO `sys_notice` VALUES (8, '系统功能优化完成', '<p>✅ 系统功能优化</p><p>已完成以下功能优化:</p><p>1. 优化用户管理界面,提升操作体验</p><p>2. 增强数据导出功能,支持更多格式</p><p>3. 优化搜索功能,提升查询效率</p><p>4. 修复部分界面显示问题</p>', 1, 'L', 1, NULL, 1, 1, '2024-12-12 14:20:00', NULL, 1, '2024-12-12 14:20:00', 1, '2024-12-12 14:20:00', 0); |
|||
INSERT INTO `sys_notice` VALUES (9, '员工培训计划', '<p>📚 员工培训计划</p><p>为提升员工专业技能,公司将于 <strong>2025年1月8日-10日</strong> 组织技术培训。</p><p>培训内容:</p><p>1. 新技术框架应用</p><p>2. 代码规范与最佳实践</p><p>3. 系统架构设计</p><p>请各部门合理安排工作,确保培训顺利进行。</p>', 5, 'M', 1, NULL, 1, 1, '2024-12-20 09:30:00', NULL, 1, '2024-12-20 09:30:00', 1, '2024-12-20 09:30:00', 0); |
|||
INSERT INTO `sys_notice` VALUES (10, '数据备份提醒', '<p>💾 数据备份提醒</p><p>请各部门注意定期备份重要数据,建议每周至少备份一次。</p><p>备份方式:</p><p>1. 使用系统自带备份功能</p><p>2. 手动导出重要数据</p><p>3. 联系IT部门协助备份</p><p>数据安全,人人有责!</p>', 3, 'L', 1, NULL, 1, 1, '2024-12-08 08:00:00', NULL, 1, '2024-12-08 08:00:00', 1, '2024-12-08 08:00:00', 0); |
|||
|
|||
-- ---------------------------- |
|||
-- 用户通知公告表 |
|||
-- ---------------------------- |
|||
DROP TABLE IF EXISTS `sys_user_notice`; |
|||
CREATE TABLE `sys_user_notice` ( |
|||
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id', |
|||
`notice_id` bigint NOT NULL COMMENT '公共通知id', |
|||
`user_id` bigint NOT NULL COMMENT '用户id', |
|||
`is_read` bigint DEFAULT '0' COMMENT '读取状态(0: 未读, 1: 已读)', |
|||
`read_time` datetime COMMENT '阅读时间', |
|||
`create_time` datetime NOT NULL COMMENT '创建时间', |
|||
`update_time` datetime COMMENT '更新时间', |
|||
`is_deleted` tinyint DEFAULT '0' COMMENT '逻辑删除(0: 未删除, 1: 已删除)', |
|||
PRIMARY KEY (`id`) USING BTREE |
|||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户通知公告关联表'; |
|||
|
|||
INSERT INTO `sys_user_notice` VALUES (1, 1, 2, 1, NULL, now(), now(), 0); |
|||
INSERT INTO `sys_user_notice` VALUES (2, 2, 2, 1, NULL, now(), now(), 0); |
|||
INSERT INTO `sys_user_notice` VALUES (3, 3, 2, 1, NULL, now(), now(), 0); |
|||
INSERT INTO `sys_user_notice` VALUES (4, 4, 2, 1, NULL, now(), now(), 0); |
|||
INSERT INTO `sys_user_notice` VALUES (5, 5, 2, 1, NULL, now(), now(), 0); |
|||
INSERT INTO `sys_user_notice` VALUES (6, 6, 2, 1, NULL, now(), now(), 0); |
|||
INSERT INTO `sys_user_notice` VALUES (7, 7, 2, 1, NULL, now(), now(), 0); |
|||
INSERT INTO `sys_user_notice` VALUES (8, 8, 2, 1, NULL, now(), now(), 0); |
|||
INSERT INTO `sys_user_notice` VALUES (9, 9, 2, 1, NULL, now(), now(), 0); |
|||
INSERT INTO `sys_user_notice` VALUES (10, 10, 2, 1, NULL, now(), now(), 0); |
|||
@ -0,0 +1,19 @@ |
|||
package com.youlai.boot; |
|||
|
|||
import org.springframework.boot.SpringApplication; |
|||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
|||
|
|||
/** |
|||
* 应用启动类 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 0.0.1 |
|||
*/ |
|||
@SpringBootApplication |
|||
public class YouLaiBootApplication { |
|||
|
|||
public static void main(String[] args) { |
|||
SpringApplication.run(YouLaiBootApplication.class, args); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
package com.youlai.boot.auth.controller; |
|||
|
|||
import com.youlai.boot.auth.model.LoginReq; |
|||
import com.youlai.boot.common.enums.ActionTypeEnum; |
|||
import com.youlai.boot.common.enums.LogModuleEnum; |
|||
import com.youlai.boot.common.result.Result; |
|||
import com.youlai.boot.auth.service.AuthService; |
|||
import com.youlai.boot.common.annotation.Log; |
|||
import com.youlai.boot.framework.captcha.model.CaptchaInfo; |
|||
import com.youlai.boot.framework.security.model.AuthenticationToken; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import jakarta.validation.Valid; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
/** |
|||
* 认证控制层 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 0.0.1 |
|||
*/ |
|||
@Tag(name = "01.认证中心") |
|||
@RestController |
|||
@RequestMapping("/api/v1/auth") |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class AuthController { |
|||
|
|||
private final AuthService authService; |
|||
|
|||
@Operation(summary = "获取验证码") |
|||
@GetMapping("/captcha") |
|||
public Result<CaptchaInfo> getCaptcha() { |
|||
CaptchaInfo captcha = authService.getCaptcha(); |
|||
return Result.success(captcha); |
|||
} |
|||
|
|||
@Operation(summary = "账号密码登录") |
|||
@PostMapping("/login") |
|||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) |
|||
public Result<AuthenticationToken> login(@RequestBody @Valid LoginReq request) { |
|||
AuthenticationToken authenticationToken = authService.login(request.getUsername(), request.getPassword()); |
|||
return Result.success(authenticationToken); |
|||
} |
|||
|
|||
@Operation(summary = "短信验证码登录") |
|||
@PostMapping("/login/sms") |
|||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) |
|||
public Result<AuthenticationToken> loginBySms( |
|||
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile, |
|||
@Parameter(description = "验证码", example = "123456") @RequestParam String code |
|||
) { |
|||
AuthenticationToken loginResult = authService.loginBySms(mobile, code); |
|||
return Result.success(loginResult); |
|||
} |
|||
|
|||
@Operation(summary = "发送登录短信验证码") |
|||
@PostMapping("/sms/code") |
|||
public Result<Void> sendSmsCode( |
|||
@Parameter(description = "手机号", example = "18888888888") @RequestParam String mobile |
|||
) { |
|||
authService.sendSmsCode(mobile); |
|||
return Result.success(); |
|||
} |
|||
|
|||
@Operation(summary = "退出登录") |
|||
@DeleteMapping("/logout") |
|||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGOUT) |
|||
public Result<Void> logout() { |
|||
authService.logout(); |
|||
return Result.success(); |
|||
} |
|||
|
|||
@Operation(summary = "刷新令牌") |
|||
@PostMapping("/refresh-token") |
|||
public Result<AuthenticationToken> refreshToken( |
|||
@Parameter(description = "刷新令牌", example = "xxx.xxx.xxx") @RequestParam String refreshToken |
|||
) { |
|||
AuthenticationToken authenticationToken = authService.refreshToken(refreshToken); |
|||
return Result.success(authenticationToken); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
package com.youlai.boot.auth.controller; |
|||
|
|||
import com.youlai.boot.auth.model.WxMaBindMobileReq; |
|||
import com.youlai.boot.auth.model.WxMaPhoneLoginReq; |
|||
import com.youlai.boot.auth.model.WxMaLoginResp; |
|||
import com.youlai.boot.auth.service.WxMaAuthService; |
|||
import com.youlai.boot.common.annotation.Log; |
|||
import com.youlai.boot.common.enums.ActionTypeEnum; |
|||
import com.youlai.boot.common.enums.LogModuleEnum; |
|||
import com.youlai.boot.common.result.Result; |
|||
import com.youlai.boot.framework.security.model.AuthenticationToken; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import jakarta.validation.Valid; |
|||
|
|||
/** |
|||
* 微信小程序认证控制层 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2.4.0 |
|||
*/ |
|||
@Tag(name = "02.微信小程序认证") |
|||
@RestController |
|||
@RequestMapping("/api/v1/wxma/auth") |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class WxMaAuthController { |
|||
|
|||
private final WxMaAuthService wxMaAuthService; |
|||
|
|||
/** |
|||
* 静默登录 |
|||
* <p> |
|||
* 适用场景:个人小程序、无需手机号的登录场景 |
|||
* <ul> |
|||
* <li>已绑定手机号的用户:直接返回 token,登录成功</li> |
|||
* <li>未绑定手机号的用户:返回 openid,需调用绑定手机号接口</li> |
|||
* </ul> |
|||
*/ |
|||
@Operation(summary = "静默登录", description = "通过微信 code 登录,已绑定用户直接返回 token,未绑定用户返回 openid 需绑定手机号") |
|||
@PostMapping("/silent-login") |
|||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) |
|||
public Result<WxMaLoginResp> silentLogin( |
|||
@Parameter(description = "微信登录凭证(wx.login 获取)", required = true, example = "0xxx") |
|||
@RequestParam String code |
|||
) { |
|||
WxMaLoginResp result = wxMaAuthService.silentLogin(code); |
|||
return Result.success(result); |
|||
} |
|||
|
|||
/** |
|||
* 手机号快捷登录 |
|||
* <p> |
|||
* 适用场景:企业认证小程序(已开通手机号快捷登录权限) |
|||
* <p> |
|||
* 一步完成登录,无需绑定流程,自动创建新用户 |
|||
*/ |
|||
@Operation(summary = "手机号快捷登录", description = "同时使用微信 code 和手机号授权 code 登录,适用于企业认证小程序") |
|||
@PostMapping("/phone-login") |
|||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) |
|||
public Result<AuthenticationToken> phoneLogin(@Valid @RequestBody WxMaPhoneLoginReq req) { |
|||
AuthenticationToken result = wxMaAuthService.phoneLogin(req.getLoginCode(), req.getPhoneCode()); |
|||
return Result.success(result); |
|||
} |
|||
|
|||
/** |
|||
* 绑定手机号 |
|||
* <p> |
|||
* 适用场景:静默登录后未绑定手机号的用户 |
|||
* <p> |
|||
* 绑定成功后自动完成登录 |
|||
*/ |
|||
@Operation(summary = "绑定手机号", description = "为静默登录用户绑定手机号,绑定成功后自动登录") |
|||
@PostMapping("/bind-mobile") |
|||
@Log(module = LogModuleEnum.LOGIN, value = ActionTypeEnum.LOGIN) |
|||
public Result<AuthenticationToken> bindMobile(@Valid @RequestBody WxMaBindMobileReq req) { |
|||
AuthenticationToken result = wxMaAuthService.bindMobile(req.getOpenid(), req.getMobile(), req.getSmsCode()); |
|||
return Result.success(result); |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
package com.youlai.boot.auth.model; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import jakarta.validation.constraints.NotBlank; |
|||
|
|||
/** |
|||
* 登录请求参数 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 3.0.0 |
|||
*/ |
|||
@Schema(description = "登录请求参数") |
|||
@Data |
|||
public class LoginReq { |
|||
|
|||
@Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "admin") |
|||
@NotBlank(message = "用户名不能为空") |
|||
private String username; |
|||
|
|||
@Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") |
|||
@NotBlank(message = "密码不能为空") |
|||
private String password; |
|||
|
|||
@Schema(description = "验证码缓存ID", example = "captcha_id_123") |
|||
private String captchaId; |
|||
|
|||
@Schema(description = "验证码", example = "123456") |
|||
private String captchaCode; |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
package com.youlai.boot.auth.model; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import jakarta.validation.constraints.NotBlank; |
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 微信小程序绑定手机号请求 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 3.0.0 |
|||
*/ |
|||
@Schema(description = "微信小程序绑定手机号请求") |
|||
@Data |
|||
public class WxMaBindMobileReq { |
|||
|
|||
@NotBlank(message = "openid 不能为空") |
|||
@Schema(description = "微信用户唯一标识(静默登录返回)", example = "oVBkZ0aYgDMDIywRdgPW8-joxXc4") |
|||
private String openid; |
|||
|
|||
@NotBlank(message = "手机号不能为空") |
|||
@Schema(description = "手机号码", example = "18888888888") |
|||
private String mobile; |
|||
|
|||
@NotBlank(message = "短信验证码不能为空") |
|||
@Schema(description = "短信验证码", example = "123456") |
|||
private String smsCode; |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
package com.youlai.boot.auth.model; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
/** |
|||
* 微信小程序登录响应 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2.4.0 |
|||
*/ |
|||
@Data |
|||
@Builder |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
@Schema(description = "微信小程序登录响应") |
|||
public class WxMaLoginResp { |
|||
|
|||
@Schema(description = "是否新用户") |
|||
private Boolean isNewUser; |
|||
|
|||
@Schema(description = "是否需要绑定手机号") |
|||
private Boolean needBindMobile; |
|||
|
|||
@Schema(description = "微信openid(绑定手机号时需要)") |
|||
private String openid; |
|||
|
|||
@Schema(description = "访问令牌") |
|||
private String accessToken; |
|||
|
|||
@Schema(description = "刷新令牌") |
|||
private String refreshToken; |
|||
|
|||
@Schema(description = "令牌类型") |
|||
private String tokenType; |
|||
|
|||
@Schema(description = "过期时间(秒)") |
|||
private Integer expiresIn; |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
package com.youlai.boot.auth.model; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import jakarta.validation.constraints.NotBlank; |
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 微信小程序手机号快捷登录请求 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 3.0.0 |
|||
*/ |
|||
@Schema(description = "微信小程序手机号快捷登录请求") |
|||
@Data |
|||
public class WxMaPhoneLoginReq { |
|||
|
|||
@NotBlank(message = "微信登录凭证不能为空") |
|||
@Schema(description = "微信登录凭证(wx.login 获取)", example = "0xxx") |
|||
private String loginCode; |
|||
|
|||
@NotBlank(message = "手机号授权凭证不能为空") |
|||
@Schema(description = "手机号授权凭证(getPhoneNumber 事件获取)", example = "0xxx") |
|||
private String phoneCode; |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
package com.youlai.boot.auth.service; |
|||
|
|||
import com.youlai.boot.framework.captcha.model.CaptchaInfo; |
|||
import com.youlai.boot.framework.security.model.AuthenticationToken; |
|||
|
|||
/** |
|||
* 认证服务接口 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2.4.0 |
|||
*/ |
|||
public interface AuthService { |
|||
|
|||
/** |
|||
* 账号密码登录 |
|||
* |
|||
* @param username 用户名 |
|||
* @param password 密码 |
|||
* @return 认证令牌 |
|||
*/ |
|||
AuthenticationToken login(String username, String password); |
|||
|
|||
/** |
|||
* 短信验证码登录 |
|||
* |
|||
* @param mobile 手机号 |
|||
* @param code 验证码 |
|||
* @return 认证令牌 |
|||
*/ |
|||
AuthenticationToken loginBySms(String mobile, String code); |
|||
|
|||
/** |
|||
* 发送短信验证码 |
|||
* |
|||
* @param mobile 手机号 |
|||
*/ |
|||
void sendSmsCode(String mobile); |
|||
|
|||
/** |
|||
* 退出登录 |
|||
*/ |
|||
void logout(); |
|||
|
|||
/** |
|||
* 获取验证码 |
|||
*/ |
|||
CaptchaInfo getCaptcha(); |
|||
|
|||
/** |
|||
* 刷新令牌 |
|||
* |
|||
* @param refreshToken 刷新令牌 |
|||
* @return 认证令牌 |
|||
*/ |
|||
AuthenticationToken refreshToken(String refreshToken); |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
package com.youlai.boot.auth.service; |
|||
|
|||
import com.youlai.boot.auth.model.WxMaLoginResp; |
|||
import com.youlai.boot.framework.security.model.AuthenticationToken; |
|||
|
|||
/** |
|||
* 微信小程序认证服务接口 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2.4.0 |
|||
*/ |
|||
public interface WxMaAuthService { |
|||
|
|||
/** |
|||
* 静默登录 |
|||
* <p> |
|||
* 通过微信登录凭证(code)获取用户唯一标识(openid), |
|||
* 如果用户已绑定手机号则直接登录成功,否则返回需绑定手机号的提示。 |
|||
* </p> |
|||
* |
|||
* @param code 微信登录凭证(wx.login 获取) |
|||
* @return 登录结果(成功返回 token,需绑定返回 openid) |
|||
*/ |
|||
WxMaLoginResp silentLogin(String code); |
|||
|
|||
/** |
|||
* 手机号快捷登录 |
|||
* <p> |
|||
* 同时使用微信登录凭证和手机号授权凭证, |
|||
* 一步完成用户注册/登录,无需额外绑定流程。 |
|||
* 适用于企业认证的小程序(已开通手机号快捷登录权限)。 |
|||
* </p> |
|||
* |
|||
* @param loginCode 微信登录凭证(wx.login 获取) |
|||
* @param phoneCode 手机号授权凭证(getPhoneNumber 事件获取) |
|||
* @return 认证令牌 |
|||
*/ |
|||
AuthenticationToken phoneLogin(String loginCode, String phoneCode); |
|||
|
|||
/** |
|||
* 绑定手机号 |
|||
* <p> |
|||
* 为已静默登录但未绑定手机号的用户绑定手机号, |
|||
* 绑定成功后自动完成登录。 |
|||
* </p> |
|||
* |
|||
* @param openid 微信用户唯一标识 |
|||
* @param mobile 手机号码 |
|||
* @param smsCode 短信验证码 |
|||
* @return 认证令牌 |
|||
*/ |
|||
AuthenticationToken bindMobile(String openid, String mobile, String smsCode); |
|||
} |
|||
@ -0,0 +1,151 @@ |
|||
package com.youlai.boot.auth.service.impl; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.youlai.boot.auth.service.AuthService; |
|||
import com.youlai.boot.common.constant.RedisConstants; |
|||
import com.youlai.boot.framework.captcha.model.CaptchaInfo; |
|||
import com.youlai.boot.framework.captcha.service.CaptchaService; |
|||
import com.youlai.boot.framework.security.model.AuthenticationToken; |
|||
import com.youlai.boot.framework.security.model.SmsAuthenticationToken; |
|||
import com.youlai.boot.framework.security.token.TokenManager; |
|||
import com.youlai.boot.framework.security.util.SecurityUtils; |
|||
import com.youlai.boot.framework.integration.sms.enums.SmsTypeEnum; |
|||
import com.youlai.boot.framework.integration.sms.service.SmsService; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.data.redis.core.RedisTemplate; |
|||
import org.springframework.security.authentication.AuthenticationManager; |
|||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
|||
import org.springframework.security.core.Authentication; |
|||
import org.springframework.security.core.context.SecurityContextHolder; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
/** |
|||
* 认证服务实现类 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2.4.0 |
|||
*/ |
|||
@Service |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class AuthServiceImpl implements AuthService { |
|||
|
|||
private final AuthenticationManager authenticationManager; |
|||
private final TokenManager tokenManager; |
|||
|
|||
private final SmsService smsService; |
|||
private final RedisTemplate<String, Object> redisTemplate; |
|||
private final CaptchaService captchaService; |
|||
|
|||
/** |
|||
* 用户名密码登录 |
|||
* |
|||
* @param username 用户名 |
|||
* @param password 密码 |
|||
* @return 访问令牌 |
|||
*/ |
|||
@Override |
|||
public AuthenticationToken login(String username, String password) { |
|||
// 1. 创建用于密码认证的令牌(未认证)
|
|||
UsernamePasswordAuthenticationToken authenticationToken = |
|||
new UsernamePasswordAuthenticationToken(username.trim(), password); |
|||
|
|||
// 2. 执行认证(认证中)
|
|||
// 说明:这里的认证流程由 Spring Security 提供的 AuthenticationManager 执行。
|
|||
// 默认情况下会委托给 DaoAuthenticationProvider:
|
|||
// 1) retrieveUser(...):内部通过 UserDetailsService.loadUserByUsername(...) 获取用户信息(本项目为 SysUserDetailsService 实现)
|
|||
// 2) additionalAuthenticationChecks(...):对比请求密码与用户存储密码(由 PasswordEncoder 完成匹配)
|
|||
// 认证通过后返回已认证的 Authentication(principal 为 SysUserDetails,authorities 为角色/权限集合)。
|
|||
Authentication authentication = authenticationManager.authenticate(authenticationToken); |
|||
|
|||
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
|||
AuthenticationToken authenticationTokenResponse = |
|||
tokenManager.generateToken(authentication); |
|||
SecurityContextHolder.getContext().setAuthentication(authentication); |
|||
return authenticationTokenResponse; |
|||
} |
|||
|
|||
/** |
|||
* 发送登录短信验证码 |
|||
* |
|||
* @param mobile 手机号 |
|||
*/ |
|||
@Override |
|||
public void sendSmsCode(String mobile) { |
|||
|
|||
// 随机生成4位验证码
|
|||
// String code = String.valueOf((int) ((Math.random() * 9 + 1) * 1000));
|
|||
// TODO 为了方便测试,验证码固定为 1234,实际开发中在配置了厂商短信服务后,可以使用上面的随机验证码
|
|||
String code = "1234"; |
|||
|
|||
// 发送短信验证码
|
|||
Map<String, String> templateParams = new HashMap<>(); |
|||
templateParams.put("code", code); |
|||
try { |
|||
smsService.sendSms(mobile, SmsTypeEnum.LOGIN, templateParams); |
|||
} catch (Exception e) { |
|||
log.error("发送短信验证码失败", e); |
|||
} |
|||
// 缓存验证码至Redis,用于登录校验
|
|||
redisTemplate.opsForValue().set(StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile), code, 5, TimeUnit.MINUTES); |
|||
} |
|||
|
|||
/** |
|||
* 短信验证码登录 |
|||
* |
|||
* @param mobile 手机号 |
|||
* @param code 验证码 |
|||
* @return 访问令牌 |
|||
*/ |
|||
@Override |
|||
public AuthenticationToken loginBySms(String mobile, String code) { |
|||
// 1. 创建用户短信验证码认证的令牌(未认证)
|
|||
SmsAuthenticationToken smsAuthenticationToken = new SmsAuthenticationToken(mobile, code); |
|||
|
|||
// 2. 执行认证(认证中)
|
|||
Authentication authentication = authenticationManager.authenticate(smsAuthenticationToken); |
|||
|
|||
// 3. 认证成功后生成 JWT 令牌,并存入 Security 上下文,供登录日志 AOP 使用(已认证)
|
|||
AuthenticationToken authenticationToken = tokenManager.generateToken(authentication); |
|||
SecurityContextHolder.getContext().setAuthentication(authentication); |
|||
return authenticationToken; |
|||
} |
|||
|
|||
/** |
|||
* 注销登录 |
|||
*/ |
|||
@Override |
|||
public void logout() { |
|||
String token = SecurityUtils.getAccessToken(); |
|||
if (StrUtil.isNotBlank(token)) { |
|||
tokenManager.invalidateToken(token); |
|||
// 清除Security上下文
|
|||
SecurityContextHolder.clearContext(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取验证码 |
|||
*/ |
|||
@Override |
|||
public CaptchaInfo getCaptcha() { |
|||
return captchaService.generate(); |
|||
} |
|||
|
|||
/** |
|||
* 刷新token |
|||
* |
|||
* @param refreshToken 刷新令牌 |
|||
* @return 新的访问令牌 |
|||
*/ |
|||
@Override |
|||
public AuthenticationToken refreshToken(String refreshToken) { |
|||
return tokenManager.refreshToken(refreshToken); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,245 @@ |
|||
package com.youlai.boot.auth.service.impl; |
|||
|
|||
import cn.binarywang.wx.miniapp.api.WxMaService; |
|||
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult; |
|||
import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; |
|||
import cn.hutool.core.util.IdUtil; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.youlai.boot.auth.model.WxMaLoginResp; |
|||
import com.youlai.boot.auth.service.WxMaAuthService; |
|||
import com.youlai.boot.common.constant.RedisConstants; |
|||
import com.youlai.boot.framework.security.exception.NeedBindMobileException; |
|||
import com.youlai.boot.framework.security.model.AuthenticationToken; |
|||
import com.youlai.boot.framework.security.model.SysUserDetails; |
|||
import com.youlai.boot.framework.security.model.WxMaAuthenticationToken; |
|||
import com.youlai.boot.framework.security.token.TokenManager; |
|||
import com.youlai.boot.system.enums.SocialPlatformEnum; |
|||
import com.youlai.boot.system.model.entity.SysUser; |
|||
import com.youlai.boot.system.service.UserSocialService; |
|||
import com.youlai.boot.system.service.UserService; |
|||
import com.youlai.boot.system.service.UserRoleService; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.data.redis.core.RedisTemplate; |
|||
import org.springframework.security.authentication.AuthenticationManager; |
|||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
|||
import org.springframework.security.core.Authentication; |
|||
import org.springframework.security.core.context.SecurityContextHolder; |
|||
import org.springframework.stereotype.Service; |
|||
import org.springframework.transaction.annotation.Transactional; |
|||
|
|||
import java.time.LocalDateTime; |
|||
import java.util.Collections; |
|||
|
|||
/** |
|||
* 微信小程序认证服务实现 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 4.0.0 |
|||
*/ |
|||
@Service |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class WxMaAuthServiceImpl implements WxMaAuthService { |
|||
|
|||
private final WxMaService wxMaService; |
|||
private final AuthenticationManager authenticationManager; |
|||
private final TokenManager tokenManager; |
|||
private final UserService userService; |
|||
private final UserSocialService userSocialService; |
|||
private final UserRoleService userRoleService; |
|||
private final RedisTemplate<String, Object> redisTemplate; |
|||
|
|||
/** |
|||
* 静默登录 |
|||
*/ |
|||
@Override |
|||
public WxMaLoginResp silentLogin(String code) { |
|||
WxMaAuthenticationToken token = new WxMaAuthenticationToken(code); |
|||
|
|||
try { |
|||
Authentication authentication = authenticationManager.authenticate(token); |
|||
AuthenticationToken authToken = tokenManager.generateToken(authentication); |
|||
SecurityContextHolder.getContext().setAuthentication(authentication); |
|||
return WxMaLoginResp.builder() |
|||
.isNewUser(false) |
|||
.needBindMobile(false) |
|||
.accessToken(authToken.getAccessToken()) |
|||
.refreshToken(authToken.getRefreshToken()) |
|||
.tokenType(authToken.getTokenType()) |
|||
.expiresIn(authToken.getExpiresIn()) |
|||
.build(); |
|||
} catch (NeedBindMobileException e) { |
|||
return WxMaLoginResp.builder() |
|||
.isNewUser(true) |
|||
.needBindMobile(true) |
|||
.openid(e.getOpenid()) |
|||
.build(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 手机号快捷登录 |
|||
*/ |
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public AuthenticationToken phoneLogin(String loginCode, String phoneCode) { |
|||
// 1. 解析微信登录凭证,获取会话信息
|
|||
WxMaJscode2SessionResult session = resolveSession(loginCode); |
|||
String openid = session.getOpenid(); |
|||
|
|||
// 2. 解析手机号授权凭证,获取手机号
|
|||
String mobile = resolvePhoneNumber(phoneCode); |
|||
|
|||
log.info("微信小程序手机号快捷登录:openid={}, mobile={}", openid, mobile); |
|||
|
|||
// 3. 查询或创建用户
|
|||
SysUser user = findOrCreateUser(mobile); |
|||
|
|||
// 4. 绑定微信 openid
|
|||
bindWechatOpenid(user, session); |
|||
|
|||
// 5. 生成认证令牌
|
|||
return generateAuthToken(mobile); |
|||
} |
|||
|
|||
/** |
|||
* 绑定手机号 |
|||
*/ |
|||
@Override |
|||
@Transactional(rollbackFor = Exception.class) |
|||
public AuthenticationToken bindMobile(String openid, String mobile, String smsCode) { |
|||
// 1. 验证短信验证码
|
|||
validateSmsCode(mobile, smsCode); |
|||
|
|||
// 2. 查询或创建用户
|
|||
SysUser user = findOrCreateUser(mobile); |
|||
|
|||
// 3. 绑定微信 openid
|
|||
userSocialService.bindOrUpdate( |
|||
user.getId(), |
|||
SocialPlatformEnum.WECHAT_MINI, |
|||
openid, |
|||
null, null, null, null |
|||
); |
|||
|
|||
log.info("微信小程序绑定手机号成功:mobile={}, openid={}", mobile, openid); |
|||
|
|||
// 4. 生成认证令牌
|
|||
return generateAuthToken(mobile); |
|||
} |
|||
|
|||
// ==================== 私有方法 ====================
|
|||
|
|||
/** |
|||
* 解析微信登录凭证,获取会话信息 |
|||
*/ |
|||
private WxMaJscode2SessionResult resolveSession(String loginCode) { |
|||
try { |
|||
return wxMaService.jsCode2SessionInfo(loginCode); |
|||
} catch (Exception e) { |
|||
log.error("获取微信会话信息失败,loginCode={}", loginCode, e); |
|||
throw new IllegalArgumentException("微信登录失败:" + e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 解析手机号授权凭证,获取手机号 |
|||
*/ |
|||
private String resolvePhoneNumber(String phoneCode) { |
|||
try { |
|||
WxMaPhoneNumberInfo phoneInfo = wxMaService.getUserService().getPhoneNoInfo(phoneCode); |
|||
return phoneInfo.getPhoneNumber(); |
|||
} catch (Exception e) { |
|||
log.error("获取微信手机号失败,phoneCode={}", phoneCode, e); |
|||
throw new IllegalArgumentException("获取手机号失败:" + e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 查询或创建用户 |
|||
*/ |
|||
private SysUser findOrCreateUser(String mobile) { |
|||
SysUser user = userService.lambdaQuery() |
|||
.eq(SysUser::getMobile, mobile) |
|||
.one(); |
|||
|
|||
if (user == null) { |
|||
user = createNewUser(mobile); |
|||
log.info("微信小程序登录:创建新用户,mobile={}, userId={}", mobile, user.getId()); |
|||
} |
|||
|
|||
return user; |
|||
} |
|||
|
|||
/** |
|||
* 创建新用户 |
|||
* <p> |
|||
* 新用户默认分配 GUEST(访问游客)角色 |
|||
* </p> |
|||
*/ |
|||
private SysUser createNewUser(String mobile) { |
|||
SysUser user = new SysUser(); |
|||
user.setMobile(mobile); |
|||
user.setUsername("wx_" + IdUtil.fastSimpleUUID().substring(0, 8)); |
|||
user.setNickname("微信用户"); |
|||
user.setStatus(1); |
|||
user.setIsDeleted(0); |
|||
user.setCreateTime(LocalDateTime.now()); |
|||
user.setUpdateTime(LocalDateTime.now()); |
|||
userService.save(user); |
|||
|
|||
// 分配 GUEST 角色(角色ID=3)
|
|||
userRoleService.saveUserRoles(user.getId(), Collections.singletonList(3L)); |
|||
|
|||
return user; |
|||
} |
|||
|
|||
/** |
|||
* 绑定微信 openid |
|||
*/ |
|||
private void bindWechatOpenid(SysUser user, WxMaJscode2SessionResult session) { |
|||
try { |
|||
userSocialService.bindOrUpdate( |
|||
user.getId(), |
|||
SocialPlatformEnum.WECHAT_MINI, |
|||
session.getOpenid(), |
|||
session.getUnionid(), |
|||
user.getNickname(), |
|||
user.getAvatar(), |
|||
session.getSessionKey() |
|||
); |
|||
} catch (Exception e) { |
|||
// 绑定失败不影响登录
|
|||
log.warn("绑定微信 openid 失败,userId={}, openid={}", user.getId(), session.getOpenid(), e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 验证短信验证码 |
|||
*/ |
|||
private void validateSmsCode(String mobile, String smsCode) { |
|||
String cacheKey = StrUtil.format(RedisConstants.Captcha.SMS_LOGIN_CODE, mobile); |
|||
String cachedCode = (String) redisTemplate.opsForValue().get(cacheKey); |
|||
|
|||
if (!StrUtil.equals(smsCode, cachedCode)) { |
|||
throw new IllegalArgumentException("验证码错误"); |
|||
} |
|||
|
|||
// 验证成功后删除验证码
|
|||
redisTemplate.delete(cacheKey); |
|||
} |
|||
|
|||
/** |
|||
* 生成认证令牌 |
|||
*/ |
|||
private AuthenticationToken generateAuthToken(String mobile) { |
|||
SysUserDetails userDetails = new SysUserDetails(userService.getAuthInfoByMobile(mobile)); |
|||
Authentication authentication = new UsernamePasswordAuthenticationToken( |
|||
userDetails, null, userDetails.getAuthorities() |
|||
); |
|||
AuthenticationToken authToken = tokenManager.generateToken(authentication); |
|||
SecurityContextHolder.getContext().setAuthentication(authentication); |
|||
return authToken; |
|||
} |
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
package com.youlai.boot.codegen.config; |
|||
|
|||
import cn.hutool.core.io.file.FileNameUtil; |
|||
import cn.hutool.core.map.MapUtil; |
|||
import lombok.Data; |
|||
import org.springframework.boot.context.properties.ConfigurationProperties; |
|||
import org.springframework.boot.context.properties.EnableConfigurationProperties; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 代码生成配置属性 |
|||
* |
|||
* @author Ray |
|||
* @since 2.11.0 |
|||
*/ |
|||
@Component |
|||
@EnableConfigurationProperties(CodegenProperties.class) |
|||
@ConfigurationProperties(prefix = "codegen") |
|||
@Data |
|||
public class CodegenProperties { |
|||
|
|||
|
|||
/** |
|||
* 默认配置 |
|||
*/ |
|||
private DefaultConfig defaultConfig ; |
|||
|
|||
/** |
|||
* 模板配置 |
|||
*/ |
|||
private Map<String, TemplateConfig> templateConfigs = MapUtil.newHashMap(true); |
|||
|
|||
/** |
|||
* 后端应用名 |
|||
*/ |
|||
private String backendAppName; |
|||
|
|||
/** |
|||
* 前端应用名 |
|||
*/ |
|||
private String frontendAppName; |
|||
|
|||
/** |
|||
* 下载文件名 |
|||
*/ |
|||
private String downloadFileName; |
|||
|
|||
/** |
|||
* 排除数据表 |
|||
*/ |
|||
private List<String> excludeTables; |
|||
|
|||
/** |
|||
* 模板配置 |
|||
*/ |
|||
@Data |
|||
public static class TemplateConfig { |
|||
|
|||
/** |
|||
* 模板路径 (e.g. /templates/codegen/controller.java.vm) |
|||
*/ |
|||
private String templatePath; |
|||
|
|||
/** |
|||
* 子包名 (e.g. controller/service/mapper/model) |
|||
*/ |
|||
private String subpackageName; |
|||
|
|||
/** |
|||
* 文件扩展名,如 .java |
|||
*/ |
|||
private String extension = FileNameUtil.EXT_JAVA; |
|||
|
|||
} |
|||
|
|||
/** |
|||
* 默认配置 |
|||
*/ |
|||
@Data |
|||
public static class DefaultConfig { |
|||
|
|||
/** |
|||
* 作者 (e.g. Ray) |
|||
*/ |
|||
private String author; |
|||
|
|||
/** |
|||
* 默认模块名(e.g. system) |
|||
*/ |
|||
private String moduleName; |
|||
|
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,112 @@ |
|||
package com.youlai.boot.codegen.controller; |
|||
|
|||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
|||
import com.youlai.boot.common.result.PageResult; |
|||
import com.youlai.boot.common.result.Result; |
|||
import com.youlai.boot.codegen.config.CodegenProperties; |
|||
import com.youlai.boot.common.enums.ActionTypeEnum; |
|||
import com.youlai.boot.common.enums.LogModuleEnum; |
|||
import com.youlai.boot.codegen.service.CodegenService; |
|||
import com.youlai.boot.codegen.model.form.GenConfigForm; |
|||
import com.youlai.boot.codegen.model.query.TableQuery; |
|||
import com.youlai.boot.codegen.model.vo.CodegenPreviewVO; |
|||
import com.youlai.boot.codegen.model.vo.TablePageVO; |
|||
import com.youlai.boot.common.annotation.Log; |
|||
import com.youlai.boot.codegen.service.GenTableService; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import jakarta.servlet.ServletOutputStream; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import java.io.IOException; |
|||
import java.net.URLEncoder; |
|||
import java.nio.charset.StandardCharsets; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 代码生成器控制层 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Tag(name = "11.代码生成") |
|||
@RestController |
|||
@RequestMapping("/api/v1/codegen") |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class CodegenController { |
|||
|
|||
private final CodegenService codegenService; |
|||
private final GenTableService genTableService; |
|||
private final CodegenProperties codegenProperties; |
|||
|
|||
@Operation(summary = "获取数据表分页列表") |
|||
@GetMapping("/table") |
|||
public PageResult<TablePageVO> getTablePage( |
|||
TableQuery queryParams |
|||
) { |
|||
Page<TablePageVO> result = codegenService.getTablePage(queryParams); |
|||
return PageResult.success(result); |
|||
} |
|||
|
|||
@Operation(summary = "获取代码生成配置") |
|||
@GetMapping("/{tableName}/config") |
|||
public Result<GenConfigForm> getGenTableFormData( |
|||
@Parameter(description = "表名", example = "sys_user") @PathVariable String tableName |
|||
) { |
|||
GenConfigForm formData = genTableService.getGenTableFormData(tableName); |
|||
return Result.success(formData); |
|||
} |
|||
|
|||
@Operation(summary = "保存代码生成配置") |
|||
@PostMapping("/{tableName}/config") |
|||
@Log(module = LogModuleEnum.CODEGEN, value = ActionTypeEnum.UPDATE) |
|||
public Result<?> saveGenConfig(@RequestBody GenConfigForm formData) { |
|||
genTableService.saveGenConfig(formData); |
|||
return Result.success(); |
|||
} |
|||
|
|||
@Operation(summary = "删除代码生成配置") |
|||
@DeleteMapping("/{tableName}/config") |
|||
public Result<?> deleteGenConfig( |
|||
@Parameter(description = "表名", example = "sys_user") @PathVariable String tableName |
|||
) { |
|||
genTableService.deleteGenConfig(tableName); |
|||
return Result.success(); |
|||
} |
|||
|
|||
@Operation(summary = "获取预览生成代码") |
|||
@GetMapping("/{tableName}/preview") |
|||
public Result<List<CodegenPreviewVO>> getTablePreviewData(@PathVariable String tableName, |
|||
@RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType, |
|||
@RequestParam(value = "type", required = false, defaultValue = "ts") String type) { |
|||
List<CodegenPreviewVO> list = codegenService.getCodegenPreviewData(tableName, pageType, type); |
|||
return Result.success(list); |
|||
} |
|||
|
|||
@Operation(summary = "下载代码") |
|||
@GetMapping("/{tableName}/download") |
|||
@Log(module = LogModuleEnum.CODEGEN, value = ActionTypeEnum.DOWNLOAD) |
|||
public void downloadZip(HttpServletResponse response, @PathVariable String tableName, |
|||
@RequestParam(value = "pageType", required = false, defaultValue = "classic") String pageType, |
|||
@RequestParam(value = "type", required = false, defaultValue = "ts") String type) { |
|||
String[] tableNames = tableName.split(","); |
|||
byte[] data = codegenService.downloadCode(tableNames, pageType, type); |
|||
|
|||
response.reset(); |
|||
response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(codegenProperties.getDownloadFileName(), StandardCharsets.UTF_8)); |
|||
response.setContentType("application/octet-stream; charset=UTF-8"); |
|||
|
|||
try (ServletOutputStream outputStream = response.getOutputStream()) { |
|||
outputStream.write(data); |
|||
outputStream.flush(); |
|||
} catch (IOException e) { |
|||
log.error("Error while writing the zip file1 to response", e); |
|||
throw new RuntimeException("Failed to write the zip file1 to response", e); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
package com.youlai.boot.codegen.converter; |
|||
|
|||
import com.youlai.boot.codegen.model.entity.GenTable; |
|||
import com.youlai.boot.codegen.model.entity.GenTableColumn; |
|||
import com.youlai.boot.codegen.model.form.GenConfigForm; |
|||
import org.mapstruct.Mapper; |
|||
import org.mapstruct.Mapping; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 代码生成配置转换器 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Mapper(componentModel = "spring") |
|||
public interface CodegenConverter { |
|||
|
|||
@Mapping(source = "genTable.tableName", target = "tableName") |
|||
@Mapping(source = "genTable.businessName", target = "businessName") |
|||
@Mapping(source = "genTable.moduleName", target = "moduleName") |
|||
@Mapping(source = "genTable.packageName", target = "packageName") |
|||
@Mapping(source = "genTable.entityName", target = "entityName") |
|||
@Mapping(source = "genTable.author", target = "author") |
|||
@Mapping(source = "genTable.pageType", target = "pageType") |
|||
@Mapping(source = "genTable.removeTablePrefix", target = "removeTablePrefix") |
|||
@Mapping(source = "fieldConfigs", target = "fieldConfigs") |
|||
GenConfigForm toGenConfigForm(GenTable genTable, List<GenTableColumn> fieldConfigs); |
|||
|
|||
List<GenConfigForm.FieldConfig> toGenTableColumnForm(List<GenTableColumn> fieldConfigs); |
|||
|
|||
GenConfigForm.FieldConfig toGenTableColumnForm(GenTableColumn genTableColumn); |
|||
|
|||
GenTable toGenTable(GenConfigForm formData); |
|||
|
|||
List<GenTableColumn> toGenTableColumn(List<GenConfigForm.FieldConfig> fieldConfigs); |
|||
|
|||
GenTableColumn toGenTableColumn(GenConfigForm.FieldConfig fieldConfig); |
|||
|
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
package com.youlai.boot.codegen.enums; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.EnumValue; |
|||
import com.fasterxml.jackson.annotation.JsonCreator; |
|||
import com.fasterxml.jackson.annotation.JsonValue; |
|||
import com.youlai.boot.common.base.IBaseEnum; |
|||
import lombok.Getter; |
|||
import lombok.RequiredArgsConstructor; |
|||
|
|||
/** |
|||
* 表单类型枚举 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Getter |
|||
@RequiredArgsConstructor |
|||
public enum FormTypeEnum implements IBaseEnum<Integer> { |
|||
|
|||
/** |
|||
* 输入框 |
|||
*/ |
|||
INPUT(1, "输入框"), |
|||
|
|||
/** |
|||
* 下拉框 |
|||
*/ |
|||
SELECT(2, "下拉框"), |
|||
|
|||
/** |
|||
* 单选框 |
|||
*/ |
|||
RADIO(3, "单选框"), |
|||
|
|||
/** |
|||
* 复选框 |
|||
*/ |
|||
CHECK_BOX(4, "复选框"), |
|||
|
|||
/** |
|||
* 数字输入框 |
|||
*/ |
|||
INPUT_NUMBER(5, "数字输入框"), |
|||
|
|||
/** |
|||
* 开关 |
|||
*/ |
|||
SWITCH(6, "开关"), |
|||
|
|||
/** |
|||
* 文本域 |
|||
*/ |
|||
TEXT_AREA(7, "文本域"), |
|||
|
|||
/** |
|||
* 日期时间框 |
|||
*/ |
|||
DATE(8, "日期框"), |
|||
|
|||
/** |
|||
* 日期框 |
|||
*/ |
|||
DATE_TIME(9, "日期时间框"), |
|||
|
|||
/** |
|||
* 隐藏域 |
|||
*/ |
|||
HIDDEN(10, "隐藏域"); |
|||
|
|||
|
|||
// Mybatis-Plus 提供注解表示插入数据库时插入该值
|
|||
@EnumValue |
|||
@JsonValue |
|||
private final Integer value; |
|||
|
|||
// @JsonValue // 表示对枚举序列化时返回此字段
|
|||
private final String label; |
|||
|
|||
|
|||
@JsonCreator |
|||
public static FormTypeEnum fromValue(Integer value) { |
|||
for (FormTypeEnum type : FormTypeEnum.values()) { |
|||
if (type.getValue().equals(value)) { |
|||
return type; |
|||
} |
|||
} |
|||
throw new IllegalArgumentException("No enum constant with value " + value); |
|||
} |
|||
} |
|||
@ -0,0 +1,108 @@ |
|||
package com.youlai.boot.codegen.enums; |
|||
|
|||
import lombok.Getter; |
|||
|
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 表单类型枚举 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Getter |
|||
public enum JavaTypeEnum { |
|||
|
|||
VARCHAR("varchar", "String", "string"), |
|||
CHAR("char", "String", "string"), |
|||
BLOB("blob", "byte[]", "Uint8Array"), |
|||
TEXT("text", "String", "string"), |
|||
JSON("json", "String", "any"), |
|||
INTEGER("int", "Integer", "number"), |
|||
TINYINT("tinyint", "Integer", "number"), |
|||
SMALLINT("smallint", "Integer", "number"), |
|||
MEDIUMINT("mediumint", "Integer", "number"), |
|||
BIGINT("bigint", "Long", "number"), |
|||
FLOAT("float", "Float", "number"), |
|||
DOUBLE("double", "Double", "number"), |
|||
DECIMAL("decimal", "BigDecimal", "number"), |
|||
DATE("date", "LocalDate", "string"), |
|||
DATETIME("datetime", "LocalDateTime", "string"), |
|||
TIMESTAMP("timestamp", "LocalDateTime", "string"), |
|||
BOOLEAN("boolean", "Boolean", "boolean"), |
|||
BIT("bit", "Boolean", "boolean"); |
|||
|
|||
// 数据库类型
|
|||
private final String dbType; |
|||
// Java类型
|
|||
private final String javaType; |
|||
// TypeScript类型
|
|||
private final String tsType; |
|||
|
|||
// 数据库类型和Java类型的映射
|
|||
private static final Map<String, JavaTypeEnum> typeMap = new HashMap<>(); |
|||
|
|||
// 初始化映射关系
|
|||
static { |
|||
for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { |
|||
typeMap.put(javaTypeEnum.getDbType(), javaTypeEnum); |
|||
} |
|||
} |
|||
|
|||
JavaTypeEnum(String dbType, String javaType, String tsType) { |
|||
this.dbType = dbType; |
|||
this.javaType = javaType; |
|||
this.tsType = tsType; |
|||
} |
|||
|
|||
/** |
|||
* 根据数据库类型获取对应的Java类型 |
|||
* |
|||
* @param columnType 列类型 |
|||
* @return 对应的Java类型 |
|||
*/ |
|||
public static String getJavaTypeByColumnType(String columnType) { |
|||
String normalized = normalizeColumnType(columnType); |
|||
JavaTypeEnum javaTypeEnum = typeMap.get(normalized); |
|||
if (javaTypeEnum != null) { |
|||
return javaTypeEnum.getJavaType(); |
|||
} |
|||
return "String"; |
|||
} |
|||
|
|||
/** |
|||
* 根据Java类型获取对应的TypeScript类型 |
|||
* |
|||
* @param javaType Java类型 |
|||
* @return 对应的TypeScript类型 |
|||
*/ |
|||
public static String getTsTypeByJavaType(String javaType) { |
|||
if (javaType == null) { |
|||
return "any"; |
|||
} |
|||
for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) { |
|||
if (javaTypeEnum.getJavaType().equals(javaType)) { |
|||
return javaTypeEnum.getTsType(); |
|||
} |
|||
} |
|||
return "any"; |
|||
} |
|||
|
|||
private static String normalizeColumnType(String columnType) { |
|||
if (columnType == null) { |
|||
return ""; |
|||
} |
|||
// Handle values like: varchar(255), bigint unsigned, INT
|
|||
String normalized = columnType.trim().toLowerCase(); |
|||
int parenIndex = normalized.indexOf('('); |
|||
if (parenIndex > -1) { |
|||
normalized = normalized.substring(0, parenIndex); |
|||
} |
|||
// Remove modifiers
|
|||
normalized = normalized.replace("unsigned", "").replace("zerofill", "").trim(); |
|||
// Collapse repeated spaces
|
|||
normalized = normalized.replaceAll("\\s+", " "); |
|||
return normalized; |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
package com.youlai.boot.codegen.enums; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.EnumValue; |
|||
import com.fasterxml.jackson.annotation.JsonCreator; |
|||
import com.fasterxml.jackson.annotation.JsonValue; |
|||
import com.youlai.boot.common.base.IBaseEnum; |
|||
import lombok.Getter; |
|||
import lombok.RequiredArgsConstructor; |
|||
|
|||
/** |
|||
* 查询类型枚举 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Getter |
|||
@RequiredArgsConstructor |
|||
public enum QueryTypeEnum implements IBaseEnum<Integer> { |
|||
|
|||
/** 等于 */ |
|||
EQ(1, "="), |
|||
|
|||
/** 模糊匹配 */ |
|||
LIKE(2, "LIKE '%s%'"), |
|||
|
|||
/** 包含 */ |
|||
IN(3, "IN"), |
|||
|
|||
/** 范围 */ |
|||
BETWEEN(4, "BETWEEN"), |
|||
|
|||
/** 大于 */ |
|||
GT(5, ">"), |
|||
|
|||
/** 大于等于 */ |
|||
GE(6, ">="), |
|||
|
|||
/** 小于 */ |
|||
LT(7, "<"), |
|||
|
|||
/** 小于等于 */ |
|||
LE(8, "<="), |
|||
|
|||
/** 不等于 */ |
|||
NE(9, "!="), |
|||
|
|||
/** 左模糊匹配 */ |
|||
LIKE_LEFT(10, "LIKE '%s'"), |
|||
|
|||
/** 右模糊匹配 */ |
|||
LIKE_RIGHT(11, "LIKE 's%'"); |
|||
|
|||
|
|||
// 存储在数据库中的枚举属性值
|
|||
@EnumValue |
|||
@JsonValue |
|||
private final Integer value; |
|||
|
|||
// 序列化成 JSON 时的属性值
|
|||
private final String label; |
|||
|
|||
|
|||
@JsonCreator |
|||
public static QueryTypeEnum fromValue(Integer value) { |
|||
for (QueryTypeEnum type : QueryTypeEnum.values()) { |
|||
if (type.getValue().equals(value)) { |
|||
return type; |
|||
} |
|||
} |
|||
throw new IllegalArgumentException("No enum constant with value " + value); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,41 @@ |
|||
package com.youlai.boot.codegen.mapper; |
|||
|
|||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
|||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
|||
import com.youlai.boot.codegen.model.vo.ColumnMetaVO; |
|||
import com.youlai.boot.codegen.model.vo.TableMetaVO; |
|||
import com.youlai.boot.codegen.model.query.TableQuery; |
|||
import com.youlai.boot.codegen.model.vo.TablePageVO; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
|
|||
import java.util.List; |
|||
|
|||
|
|||
/** |
|||
* 数据库映射层 |
|||
* |
|||
* @author Ray |
|||
* @since 2.9.0 |
|||
*/ |
|||
@Mapper |
|||
public interface DatabaseMapper extends BaseMapper { |
|||
|
|||
/** |
|||
* 获取表分页列表 |
|||
* |
|||
* @param page |
|||
* @param queryParams |
|||
* @return |
|||
*/ |
|||
Page<TablePageVO> getTablePage(Page<TablePageVO> page, TableQuery queryParams); |
|||
|
|||
/** |
|||
* 获取表字段列表 |
|||
* |
|||
* @param tableName |
|||
* @return |
|||
*/ |
|||
List<ColumnMetaVO> getTableColumns(String tableName); |
|||
|
|||
TableMetaVO getTableMetadata(String tableName); |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
package com.youlai.boot.codegen.mapper; |
|||
|
|||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
|||
import com.youlai.boot.codegen.model.entity.GenTableColumn; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
|
|||
/** |
|||
* 代码生成表字段配置访问层 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Mapper |
|||
public interface GenTableColumnMapper extends BaseMapper<GenTableColumn> { |
|||
|
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
@ -0,0 +1,20 @@ |
|||
package com.youlai.boot.codegen.mapper; |
|||
|
|||
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
|||
import com.youlai.boot.codegen.model.entity.GenTable; |
|||
import org.apache.ibatis.annotations.Mapper; |
|||
|
|||
/** |
|||
* 代码生成表配置访问层 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Mapper |
|||
public interface GenTableMapper extends BaseMapper<GenTable> { |
|||
|
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
@ -0,0 +1,65 @@ |
|||
package com.youlai.boot.codegen.model.entity; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.*; |
|||
|
|||
import com.youlai.boot.common.base.BaseEntity; |
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
|
|||
/** |
|||
* 代码生成表配置 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@TableName(value = "gen_table") |
|||
@Getter |
|||
@Setter |
|||
public class GenTable extends BaseEntity { |
|||
|
|||
/** |
|||
* 表名 |
|||
*/ |
|||
private String tableName; |
|||
|
|||
/** |
|||
* 包名 |
|||
*/ |
|||
private String packageName; |
|||
|
|||
/** |
|||
* 模块名 |
|||
*/ |
|||
private String moduleName; |
|||
|
|||
/** |
|||
* 实体类名 |
|||
*/ |
|||
private String entityName; |
|||
|
|||
/** |
|||
* 业务名 |
|||
*/ |
|||
private String businessName; |
|||
|
|||
/** |
|||
* 父菜单ID |
|||
*/ |
|||
private Long parentMenuId; |
|||
|
|||
/** |
|||
* 作者 |
|||
*/ |
|||
private String author; |
|||
|
|||
/** |
|||
* 页面类型 classic|curd |
|||
*/ |
|||
private String pageType; |
|||
|
|||
/** |
|||
* 要移除的表前缀,如: sys_ |
|||
*/ |
|||
private String removeTablePrefix; |
|||
} |
|||
|
|||
@ -0,0 +1,107 @@ |
|||
package com.youlai.boot.codegen.model.entity; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.TableField; |
|||
import com.baomidou.mybatisplus.annotation.TableName; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonIgnore; |
|||
import com.youlai.boot.common.base.BaseEntity; |
|||
import com.youlai.boot.codegen.enums.FormTypeEnum; |
|||
import com.youlai.boot.codegen.enums.QueryTypeEnum; |
|||
import lombok.Getter; |
|||
import lombok.Setter; |
|||
|
|||
/** |
|||
* 代码生成表字段配置实体 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@TableName(value = "gen_table_column") |
|||
@Getter |
|||
@Setter |
|||
public class GenTableColumn extends BaseEntity { |
|||
|
|||
|
|||
/** |
|||
* 关联的表配置ID |
|||
*/ |
|||
private Long tableId; |
|||
|
|||
/** |
|||
* 列名 |
|||
*/ |
|||
private String columnName; |
|||
|
|||
/** |
|||
* 列类型 |
|||
*/ |
|||
private String columnType; |
|||
|
|||
/** |
|||
* 字段长度 |
|||
*/ |
|||
private Long maxLength; |
|||
|
|||
/** |
|||
* 字段名称 |
|||
*/ |
|||
private String fieldName; |
|||
|
|||
/** |
|||
* 字段排序 |
|||
*/ |
|||
private Integer fieldSort; |
|||
|
|||
/** |
|||
* 字段类型 |
|||
*/ |
|||
private String fieldType; |
|||
|
|||
/** |
|||
* 字段描述 |
|||
*/ |
|||
private String fieldComment; |
|||
|
|||
/** |
|||
* 表单类型 |
|||
*/ |
|||
private FormTypeEnum formType; |
|||
|
|||
/** |
|||
* 查询方式 |
|||
*/ |
|||
private QueryTypeEnum queryType; |
|||
|
|||
/** |
|||
* 是否在列表显示 |
|||
*/ |
|||
private Integer isShowInList; |
|||
|
|||
/** |
|||
* 是否在表单显示 |
|||
*/ |
|||
private Integer isShowInForm; |
|||
|
|||
/** |
|||
* 是否在查询条件显示 |
|||
*/ |
|||
private Integer isShowInQuery; |
|||
|
|||
/** |
|||
* 是否必填 |
|||
*/ |
|||
private Integer isRequired; |
|||
|
|||
/** |
|||
* TypeScript类型 |
|||
*/ |
|||
@TableField(exist = false) |
|||
@JsonIgnore |
|||
private String tsType; |
|||
|
|||
/** |
|||
* 字典类型 |
|||
*/ |
|||
private String dictType; |
|||
} |
|||
|
|||
@ -0,0 +1,109 @@ |
|||
package com.youlai.boot.codegen.model.form; |
|||
|
|||
import com.youlai.boot.codegen.enums.FormTypeEnum; |
|||
import com.youlai.boot.codegen.enums.QueryTypeEnum; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 代码生成配置表单 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Schema(description = "代码生成配置表单") |
|||
@Data |
|||
public class GenConfigForm { |
|||
|
|||
@Schema(description = "主键",example = "1") |
|||
private Long id; |
|||
|
|||
@Schema(description = "表名",example = "sys_user") |
|||
private String tableName; |
|||
|
|||
@Schema(description = "业务名",example = "用户") |
|||
private String businessName; |
|||
|
|||
@Schema(description = "模块名",example = "system") |
|||
private String moduleName; |
|||
|
|||
@Schema(description = "包名",example = "com.youlai") |
|||
private String packageName; |
|||
|
|||
@Schema(description = "实体名",example = "User") |
|||
private String entityName; |
|||
|
|||
@Schema(description = "作者",example = "youlaitech") |
|||
private String author; |
|||
|
|||
@Schema(description = "上级菜单ID",example = "1") |
|||
private Long parentMenuId; |
|||
|
|||
@Schema(description = "字段配置列表") |
|||
private List<FieldConfig> fieldConfigs; |
|||
|
|||
@Schema(description = "后端应用名") |
|||
private String backendAppName; |
|||
|
|||
@Schema(description = "前端应用名") |
|||
private String frontendAppName; |
|||
|
|||
@Schema(description = "页面类型 classic|curd", example = "classic") |
|||
private String pageType; |
|||
|
|||
@Schema(description = "要移除的表前缀,如: sys_", example = "sys_") |
|||
private String removeTablePrefix; |
|||
|
|||
@Schema(description = "字段配置") |
|||
@Data |
|||
public static class FieldConfig { |
|||
|
|||
@Schema(description = "主键") |
|||
private Long id; |
|||
|
|||
@Schema(description = "列名") |
|||
private String columnName; |
|||
|
|||
@Schema(description = "列类型") |
|||
private String columnType; |
|||
|
|||
@Schema(description = "字段名") |
|||
private String fieldName; |
|||
|
|||
@Schema(description = "字段排序") |
|||
private Integer fieldSort; |
|||
|
|||
@Schema(description = "字段类型") |
|||
private String fieldType; |
|||
|
|||
@Schema(description = "字段描述") |
|||
private String fieldComment; |
|||
|
|||
@Schema(description = "是否在列表显示") |
|||
private Integer isShowInList; |
|||
|
|||
@Schema(description = "是否在表单显示") |
|||
private Integer isShowInForm; |
|||
|
|||
@Schema(description = "是否在查询条件显示") |
|||
private Integer isShowInQuery; |
|||
|
|||
@Schema(description = "是否必填") |
|||
private Integer isRequired; |
|||
|
|||
@Schema(description = "最大长度") |
|||
private Integer maxLength; |
|||
|
|||
@Schema(description = "表单类型") |
|||
private FormTypeEnum formType; |
|||
|
|||
@Schema(description = "查询类型") |
|||
private QueryTypeEnum queryType; |
|||
|
|||
@Schema(description = "字典类型") |
|||
private String dictType; |
|||
|
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
package com.youlai.boot.codegen.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.Getter; |
|||
import lombok.Setter; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 数据表分页查询对象 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Schema(description = "数据表分页查询对象") |
|||
@Getter |
|||
@Setter |
|||
public class TablePageQuery extends BaseQuery { |
|||
|
|||
@Schema(description="关键字(表名)") |
|||
private String keywords; |
|||
|
|||
/** |
|||
* 排除的表名 |
|||
*/ |
|||
@JsonIgnore |
|||
private List<String> excludeTables; |
|||
|
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
package com.youlai.boot.codegen.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.Getter; |
|||
import lombok.Setter; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 数据表查询对象 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Schema(description = "数据表查询对象") |
|||
@Getter |
|||
@Setter |
|||
public class TableQuery extends BaseQuery { |
|||
|
|||
@Schema(description="关键字(表名)") |
|||
private String keywords; |
|||
|
|||
/** |
|||
* 排除的表名 |
|||
*/ |
|||
@JsonIgnore |
|||
private List<String> excludeTables; |
|||
|
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
package com.youlai.boot.codegen.model.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
@Schema(description = "代码生成代码预览Vo") |
|||
@Data |
|||
public class CodegenPreviewVO { |
|||
|
|||
@Schema(description = "生成文件路径") |
|||
private String path; |
|||
|
|||
@Schema(description = "生成文件名称",example = "SysUser.java" ) |
|||
private String fileName; |
|||
|
|||
@Schema(description = "生成文件内容") |
|||
private String content; |
|||
|
|||
@Schema(description = "文件范围(frontend/backend)") |
|||
private String scope; |
|||
|
|||
@Schema(description = "文件语言(扩展名)") |
|||
private String language; |
|||
|
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
package com.youlai.boot.codegen.model.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
@Schema(description = "数据表字段元数据") |
|||
@Data |
|||
public class ColumnMetaVO { |
|||
|
|||
private String columnName; |
|||
|
|||
private String dataType; |
|||
|
|||
private String columnComment; |
|||
|
|||
private Long characterMaximumLength; |
|||
|
|||
private Integer isPrimaryKey; |
|||
|
|||
private String isNullable; |
|||
|
|||
private String characterSetName; |
|||
|
|||
private String collationName; |
|||
|
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
package com.youlai.boot.codegen.model.vo; |
|||
|
|||
import lombok.Data; |
|||
|
|||
/** |
|||
* 数据表元数据 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Data |
|||
public class TableMetaVO { |
|||
|
|||
private String tableName; |
|||
|
|||
private String tableComment; |
|||
|
|||
private String tableCollation; |
|||
|
|||
private String engine; |
|||
|
|||
private String charset; |
|||
|
|||
private String createTime; |
|||
|
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
package com.youlai.boot.codegen.model.vo; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
|
|||
@Schema(description = "表视图对象") |
|||
@Data |
|||
public class TablePageVO { |
|||
|
|||
@Schema(description = "表名称", example = "sys_user") |
|||
private String tableName; |
|||
|
|||
@Schema(description = "表描述",example = "用户表") |
|||
private String tableComment; |
|||
|
|||
@Schema(description = "表排序规则",example = "utf8mb4_general_ci") |
|||
private String tableCollation; |
|||
|
|||
@Schema(description = "存储引擎",example = "InnoDB") |
|||
private String engine; |
|||
|
|||
@Schema(description = "字符集",example = "utf8mb4") |
|||
private String charset; |
|||
|
|||
@Schema(description = "创建时间",example = "2023-08-08 08:08:08") |
|||
private String createTime; |
|||
|
|||
@Schema(description="是否已配置") |
|||
private Integer isConfigured; |
|||
|
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
package com.youlai.boot.codegen.service; |
|||
|
|||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
|||
import com.youlai.boot.codegen.model.query.TableQuery; |
|||
import com.youlai.boot.codegen.model.vo.CodegenPreviewVO; |
|||
import com.youlai.boot.codegen.model.vo.TablePageVO; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 代码生成配置接口 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
public interface CodegenService { |
|||
|
|||
/** |
|||
* 获取数据表分页列表 |
|||
* |
|||
* @param queryParams 查询参数 |
|||
* @return |
|||
*/ |
|||
Page<TablePageVO> getTablePage(TableQuery queryParams); |
|||
|
|||
/** |
|||
* 获取预览生成代码 |
|||
* |
|||
* @param tableName 表名 |
|||
* @return |
|||
*/ |
|||
List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType, String type); |
|||
|
|||
/** |
|||
* 下载代码 |
|||
* @param tableNames 表名 |
|||
* @return |
|||
*/ |
|||
byte[] downloadCode(String[] tableNames, String pageType, String type); |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
package com.youlai.boot.codegen.service; |
|||
|
|||
import com.baomidou.mybatisplus.extension.service.IService; |
|||
import com.youlai.boot.codegen.model.entity.GenTableColumn; |
|||
|
|||
/** |
|||
* 代码生成配置接口 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
public interface GenTableColumnService extends IService<GenTableColumn> { |
|||
|
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
package com.youlai.boot.codegen.service; |
|||
|
|||
import com.baomidou.mybatisplus.extension.service.IService; |
|||
import com.youlai.boot.codegen.model.entity.GenTable; |
|||
import com.youlai.boot.codegen.model.form.GenConfigForm; |
|||
|
|||
/** |
|||
* 代码生成配置接口 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
public interface GenTableService extends IService<GenTable> { |
|||
|
|||
/** |
|||
* 获取代码生成配置 |
|||
* |
|||
* @param tableName 表名 |
|||
* @return |
|||
*/ |
|||
GenConfigForm getGenTableFormData(String tableName); |
|||
|
|||
/** |
|||
* 保存代码生成配置 |
|||
* |
|||
* @param formData 表单数据 |
|||
* @return |
|||
*/ |
|||
void saveGenConfig(GenConfigForm formData); |
|||
|
|||
/** |
|||
* 删除代码生成配置 |
|||
* |
|||
* @param tableName 表名 |
|||
* @return |
|||
*/ |
|||
void deleteGenConfig(String tableName); |
|||
|
|||
} |
|||
@ -0,0 +1,427 @@ |
|||
package com.youlai.boot.codegen.service.impl; |
|||
|
|||
import cn.hutool.core.collection.CollectionUtil; |
|||
import cn.hutool.core.date.DateUtil; |
|||
import cn.hutool.core.io.file.FileNameUtil; |
|||
import cn.hutool.core.util.ObjectUtil; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import cn.hutool.extra.template.Template; |
|||
import cn.hutool.extra.template.TemplateConfig; |
|||
import cn.hutool.extra.template.TemplateEngine; |
|||
import cn.hutool.extra.template.TemplateUtil; |
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
|||
import com.youlai.boot.codegen.enums.JavaTypeEnum; |
|||
import com.youlai.boot.codegen.config.CodegenProperties; |
|||
import com.youlai.boot.codegen.service.GenTableService; |
|||
import com.youlai.boot.codegen.service.GenTableColumnService; |
|||
import com.youlai.boot.codegen.service.CodegenService; |
|||
import com.youlai.boot.common.exception.BusinessException; |
|||
import com.youlai.boot.codegen.mapper.DatabaseMapper; |
|||
import com.youlai.boot.codegen.model.entity.GenTable; |
|||
import com.youlai.boot.codegen.model.entity.GenTableColumn; |
|||
import com.youlai.boot.codegen.model.query.TableQuery; |
|||
import com.youlai.boot.codegen.model.vo.CodegenPreviewVO; |
|||
import com.youlai.boot.codegen.model.vo.TablePageVO; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.io.ByteArrayOutputStream; |
|||
import java.io.File; |
|||
import java.io.IOException; |
|||
import java.nio.charset.StandardCharsets; |
|||
import java.util.*; |
|||
import java.util.zip.ZipEntry; |
|||
import java.util.zip.ZipOutputStream; |
|||
|
|||
/** |
|||
* 代码生成服务实现类。 |
|||
* |
|||
* <p> |
|||
* 根据代码生成配置({@link CodegenProperties})与表/字段元数据,渲染模板并提供预览与下载能力。 |
|||
* </p> |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Service |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class CodegenServiceImpl implements CodegenService { |
|||
|
|||
private final DatabaseMapper databaseMapper; |
|||
private final CodegenProperties codegenProperties; |
|||
private final GenTableService genTableService; |
|||
private final GenTableColumnService genTableColumnService; |
|||
|
|||
/** |
|||
* 数据表分页列表 |
|||
* |
|||
* @param queryParams 查询参数 |
|||
* @return 分页结果 |
|||
*/ |
|||
public Page<TablePageVO> getTablePage(TableQuery queryParams) { |
|||
Page<TablePageVO> page = new Page<>(queryParams.getPageNum(), queryParams.getPageSize()); |
|||
// 设置排除的表
|
|||
List<String> excludeTables = codegenProperties.getExcludeTables(); |
|||
queryParams.setExcludeTables(excludeTables); |
|||
|
|||
return databaseMapper.getTablePage(page, queryParams); |
|||
} |
|||
|
|||
/** |
|||
* 解析前端模板路径 |
|||
* |
|||
* @param templateName 模板标识 |
|||
* @param templateConfig 模板配置 |
|||
* @param frontendType 前端类型 |
|||
* @return 模板路径 |
|||
*/ |
|||
private String resolveFrontendTemplatePath(String templateName, |
|||
CodegenProperties.TemplateConfig templateConfig, |
|||
String frontendType) { |
|||
if (!"js".equals(frontendType)) { |
|||
return templateConfig.getTemplatePath(); |
|||
} |
|||
if ("API".equals(templateName)) { |
|||
return "codegen/frontend/js/api.js.vm"; |
|||
} |
|||
if ("VIEW".equals(templateName)) { |
|||
return "codegen/frontend/js/index.js.vue.vm"; |
|||
} |
|||
return templateConfig.getTemplatePath(); |
|||
} |
|||
|
|||
/** |
|||
* 解析前端文件后缀 |
|||
* |
|||
* @param templateName 模板标识 |
|||
* @param templateConfig 模板配置 |
|||
* @param frontendType 前端类型 |
|||
* @return 文件后缀 |
|||
*/ |
|||
private String resolveFrontendExtension(String templateName, |
|||
CodegenProperties.TemplateConfig templateConfig, |
|||
String frontendType) { |
|||
if (!"js".equals(frontendType)) { |
|||
return templateConfig.getExtension(); |
|||
} |
|||
if ("API".equals(templateName) || "API_TYPES".equals(templateName)) { |
|||
return ".js"; |
|||
} |
|||
return templateConfig.getExtension(); |
|||
} |
|||
|
|||
/** |
|||
* 获取预览生成代码 |
|||
* |
|||
* @param tableName 表名 |
|||
* @return 预览数据 |
|||
*/ |
|||
@Override |
|||
public List<CodegenPreviewVO> getCodegenPreviewData(String tableName, String pageType, String type) { |
|||
|
|||
List<CodegenPreviewVO> list = new ArrayList<>(); |
|||
|
|||
GenTable genTable = genTableService.getOne(new LambdaQueryWrapper<GenTable>() |
|||
.eq(GenTable::getTableName, tableName) |
|||
); |
|||
if (genTable == null) { |
|||
throw new BusinessException("未找到表生成配置"); |
|||
} |
|||
|
|||
List<GenTableColumn> fieldConfigs = genTableColumnService.list(new LambdaQueryWrapper<GenTableColumn>() |
|||
.eq(GenTableColumn::getTableId, genTable.getId()) |
|||
.orderByAsc(GenTableColumn::getFieldSort) |
|||
|
|||
); |
|||
if (CollectionUtil.isEmpty(fieldConfigs)) { |
|||
throw new BusinessException("未找到字段生成配置"); |
|||
} |
|||
|
|||
// 遍历模板配置
|
|||
Map<String, CodegenProperties.TemplateConfig> templateConfigs = codegenProperties.getTemplateConfigs(); |
|||
String frontendType = StrUtil.blankToDefault(type, "ts").toLowerCase(); |
|||
for (Map.Entry<String, CodegenProperties.TemplateConfig> templateConfigEntry : templateConfigs.entrySet()) { |
|||
CodegenPreviewVO previewVo = new CodegenPreviewVO(); |
|||
|
|||
CodegenProperties.TemplateConfig templateConfig = templateConfigEntry.getValue(); |
|||
|
|||
String templateName = templateConfigEntry.getKey(); |
|||
if ("js".equals(frontendType) && "API_TYPES".equals(templateName)) { |
|||
continue; |
|||
} |
|||
|
|||
String effectiveTemplatePath = resolveFrontendTemplatePath(templateName, templateConfig, frontendType); |
|||
String extension = resolveFrontendExtension(templateName, templateConfig, frontendType); |
|||
|
|||
/* 1. 生成文件名 UserController */ |
|||
// User Role Menu Dept
|
|||
String entityName = genTable.getEntityName(); |
|||
// Controller Service Mapper Entity
|
|||
// .java .ts .vue
|
|||
|
|||
// 文件名 UserController.java
|
|||
String fileName = getFileName(entityName, templateName, extension); |
|||
previewVo.setFileName(fileName); |
|||
previewVo.setScope(resolveScope(templateName)); |
|||
previewVo.setLanguage(resolveLanguage(fileName)); |
|||
|
|||
/* 2. 生成文件路径 */ |
|||
// 包名:com.youlai.boot
|
|||
String packageName = genTable.getPackageName(); |
|||
// 模块名:system
|
|||
String moduleName = genTable.getModuleName(); |
|||
// 子包名:controller
|
|||
String subpackageName = templateConfig.getSubpackageName(); |
|||
// 组合成文件路径:src/main/java/com/youlai/boot/system/controller
|
|||
String filePath = getFilePath(templateName, moduleName, packageName, subpackageName, entityName); |
|||
previewVo.setPath(filePath); |
|||
|
|||
/* 3. 生成文件内容 */ |
|||
// 将模板文件中的变量替换为具体的值 生成代码内容
|
|||
// 优先使用保存的 ui,没有则使用请求参数
|
|||
String finalType = StrUtil.blankToDefault(genTable.getPageType(), pageType); |
|||
String content = getCodeContent( |
|||
effectiveTemplatePath, |
|||
templateConfig.getSubpackageName(), |
|||
genTable, |
|||
fieldConfigs, |
|||
finalType |
|||
); |
|||
previewVo.setContent(content); |
|||
|
|||
list.add(previewVo); |
|||
} |
|||
return list; |
|||
} |
|||
|
|||
private String resolveScope(String templateName) { |
|||
return switch (templateName) { |
|||
case "API", "API_TYPES", "VIEW" -> "frontend"; |
|||
default -> "backend"; |
|||
}; |
|||
} |
|||
|
|||
private String resolveLanguage(String fileName) { |
|||
return FileNameUtil.extName(fileName).toLowerCase(); |
|||
} |
|||
|
|||
/** |
|||
* 生成文件名。 |
|||
* |
|||
* <p>部分模板需要使用约定的命名规则(例如前端 API 文件)。</p> |
|||
* |
|||
* @param entityName 实体名(例如 User) |
|||
* @param templateName 模板名(例如 Entity、Controller、API) |
|||
* @param extension 文件后缀(例如 .java、.ts) |
|||
* @return 文件名 |
|||
*/ |
|||
private String getFileName(String entityName, String templateName, String extension) { |
|||
if ("Entity".equals(templateName)) { |
|||
return entityName + extension; |
|||
} else if ("MapperXml".equals(templateName)) { |
|||
return entityName + "Mapper" + extension; |
|||
} else if ("API".equals(templateName)) { |
|||
return "index" + extension; |
|||
} else if ("API_TYPES".equals(templateName)) { |
|||
return "types" + extension; |
|||
} else if ("VIEW".equals(templateName)) { |
|||
return "index.vue"; |
|||
} |
|||
return entityName + templateName + extension; |
|||
} |
|||
|
|||
/** |
|||
* 生成文件路径。 |
|||
* |
|||
* @param templateName 模板名 |
|||
* @param moduleName 模块名(例如 system) |
|||
* @param packageName 包名(例如 com.youlai.boot) |
|||
* @param subPackageName 子包名(例如 controller、service.impl、api、views) |
|||
* @param entityName 实体名(例如 User) |
|||
* @return 生成文件路径 |
|||
*/ |
|||
private String getFilePath(String templateName, String moduleName, String packageName, String subPackageName, String entityName) { |
|||
String path; |
|||
if ("MapperXml".equals(templateName)) { |
|||
path = (codegenProperties.getBackendAppName() |
|||
+ File.separator |
|||
+ "src" + File.separator + "main" + File.separator + "resources" |
|||
+ File.separator + subPackageName |
|||
+ File.separator + moduleName |
|||
); |
|||
} else if ("API".equals(templateName)) { |
|||
path = (codegenProperties.getFrontendAppName() |
|||
+ File.separator + "src" |
|||
+ File.separator + "api" |
|||
+ File.separator + moduleName |
|||
+ File.separator + StrUtil.toSymbolCase(entityName, '-') |
|||
); |
|||
} else if ("API_TYPES".equals(templateName)) { |
|||
path = (codegenProperties.getFrontendAppName() |
|||
+ File.separator + "src" |
|||
+ File.separator + "api" |
|||
+ File.separator + moduleName |
|||
+ File.separator + StrUtil.toSymbolCase(entityName, '-') |
|||
); |
|||
} else if ("VIEW".equals(templateName)) { |
|||
// path = "src/views/system/user";
|
|||
path = (codegenProperties.getFrontendAppName() |
|||
+ File.separator + "src" |
|||
+ File.separator + subPackageName |
|||
+ File.separator + moduleName |
|||
+ File.separator + StrUtil.toSymbolCase(entityName, '-') |
|||
); |
|||
} else { |
|||
path = (codegenProperties.getBackendAppName() |
|||
+ File.separator |
|||
+ "src" + File.separator + "main" + File.separator + "java" |
|||
+ File.separator + packageName |
|||
+ File.separator + moduleName |
|||
+ File.separator + subPackageName |
|||
); |
|||
} |
|||
|
|||
// subPackageName = model.entity => model/entity
|
|||
path = path.replace(".", File.separator); |
|||
|
|||
return path; |
|||
} |
|||
|
|||
/** |
|||
* 渲染模板,生成代码内容。 |
|||
* |
|||
* @param templateConfig 模板配置 |
|||
* @param genTable 表生成配置 |
|||
* @param fieldConfigs 字段配置 |
|||
* @param pageType 前端页面类型 |
|||
* @return 渲染后的代码内容 |
|||
*/ |
|||
private String getCodeContent(String templatePath, |
|||
String subpackageName, |
|||
GenTable genTable, |
|||
List<GenTableColumn> fieldConfigs, |
|||
String pageType) { |
|||
|
|||
Map<String, Object> bindMap = new HashMap<>(); |
|||
|
|||
String entityName = genTable.getEntityName(); |
|||
|
|||
bindMap.put("packageName", genTable.getPackageName()); |
|||
bindMap.put("moduleName", genTable.getModuleName()); |
|||
bindMap.put("subpackageName", subpackageName); |
|||
bindMap.put("date", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm")); |
|||
bindMap.put("entityName", entityName); |
|||
bindMap.put("tableName", genTable.getTableName()); |
|||
bindMap.put("author", genTable.getAuthor()); |
|||
String entityLowerCamel = StrUtil.lowerFirst(entityName); |
|||
String entityKebab = StrUtil.toSymbolCase(entityName, '-'); |
|||
String entityUpperSnake = StrUtil.toSymbolCase(entityName, '_').toUpperCase(); |
|||
bindMap.put("entityLowerCamel", entityLowerCamel); |
|||
bindMap.put("entityKebab", entityKebab); |
|||
bindMap.put("entityUpperSnake", entityUpperSnake); |
|||
bindMap.put("businessName", genTable.getBusinessName()); |
|||
bindMap.put("fieldConfigs", fieldConfigs); |
|||
|
|||
boolean hasLocalDateTime = false; |
|||
boolean hasBigDecimal = false; |
|||
boolean hasRequiredField = false; |
|||
|
|||
for (GenTableColumn fieldConfig : fieldConfigs) { |
|||
|
|||
if (StrUtil.isBlank(fieldConfig.getFieldType())) { |
|||
fieldConfig.setFieldType(JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType())); |
|||
} |
|||
|
|||
if ("LocalDateTime".equals(fieldConfig.getFieldType()) || "LocalDate".equals(fieldConfig.getFieldType())) { |
|||
hasLocalDateTime = true; |
|||
} |
|||
if ("BigDecimal".equals(fieldConfig.getFieldType())) { |
|||
hasBigDecimal = true; |
|||
} |
|||
if (ObjectUtil.equals(fieldConfig.getIsRequired(), 1)) { |
|||
hasRequiredField = true; |
|||
} |
|||
fieldConfig.setTsType(JavaTypeEnum.getTsTypeByJavaType(fieldConfig.getFieldType())); |
|||
} |
|||
|
|||
bindMap.put("hasLocalDateTime", hasLocalDateTime); |
|||
bindMap.put("hasBigDecimal", hasBigDecimal); |
|||
bindMap.put("hasRequiredField", hasRequiredField); |
|||
|
|||
TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH)); |
|||
// 根据 ui 选择不同的前端页面模板:默认 index.vue.vm;封装版使用 index.curd.vue.vm
|
|||
String path = templatePath; |
|||
if ("curd".equalsIgnoreCase(pageType)) { |
|||
if (path.endsWith("index.js.vue.vm")) { |
|||
path = path.replace("index.js.vue.vm", "index.curd.js.vue.vm"); |
|||
} else if (path.endsWith("index.vue.vm")) { |
|||
path = path.replace("index.vue.vm", "index.curd.vue.vm"); |
|||
} |
|||
} |
|||
Template template = templateEngine.getTemplate(path); |
|||
|
|||
return template.render(bindMap); |
|||
} |
|||
|
|||
/** |
|||
* 下载代码。 |
|||
* |
|||
* @param tableNames 表名数组,支持多张表 |
|||
* @param ui 页面类型 |
|||
* @return zip 压缩文件字节数组 |
|||
*/ |
|||
@Override |
|||
public byte[] downloadCode(String[] tableNames, String ui, String type) { |
|||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); |
|||
ZipOutputStream zip = new ZipOutputStream(outputStream)) { |
|||
|
|||
// 遍历每个表名,生成对应的代码并压缩到 zip 文件中
|
|||
for (String tableName : tableNames) { |
|||
generateAndZipCode(tableName, zip, ui, type); |
|||
} |
|||
// 确保所有压缩数据写入输出流,避免数据残留在内存缓冲区引发的数据不完整
|
|||
zip.finish(); |
|||
return outputStream.toByteArray(); |
|||
|
|||
} catch (IOException e) { |
|||
log.error("Error while generating zip for code download", e); |
|||
throw new RuntimeException("Failed to generate code zip file1", e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 根据表名生成代码并压缩到 zip 文件中。 |
|||
* |
|||
* @param tableName 表名 |
|||
* @param zip 压缩文件输出流 |
|||
* @param ui 页面类型 |
|||
*/ |
|||
private void generateAndZipCode(String tableName, ZipOutputStream zip, String ui, String type) { |
|||
List<CodegenPreviewVO> codePreviewList = getCodegenPreviewData(tableName, ui, type); |
|||
|
|||
for (CodegenPreviewVO codePreview : codePreviewList) { |
|||
String fileName = codePreview.getFileName(); |
|||
String content = codePreview.getContent(); |
|||
String path = codePreview.getPath(); |
|||
|
|||
try { |
|||
// 创建压缩条目
|
|||
ZipEntry zipEntry = new ZipEntry(path + File.separator + fileName); |
|||
zip.putNextEntry(zipEntry); |
|||
|
|||
// 写入文件内容
|
|||
zip.write(content.getBytes(StandardCharsets.UTF_8)); |
|||
|
|||
// 关闭当前压缩条目
|
|||
zip.closeEntry(); |
|||
|
|||
} catch (IOException e) { |
|||
log.error("Error while adding file1 {} to zip", fileName, e); |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
package com.youlai.boot.codegen.service.impl; |
|||
|
|||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
|||
import com.youlai.boot.codegen.mapper.GenTableColumnMapper; |
|||
import com.youlai.boot.codegen.model.entity.GenTableColumn; |
|||
import com.youlai.boot.codegen.service.GenTableColumnService; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
/** |
|||
* 代码生成字段配置服务实现类 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Service |
|||
@RequiredArgsConstructor |
|||
public class GenTableColumnServiceImpl extends ServiceImpl<GenTableColumnMapper, GenTableColumn> implements GenTableColumnService { |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,227 @@ |
|||
package com.youlai.boot.codegen.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.extension.service.impl.ServiceImpl; |
|||
import com.youlai.boot.YouLaiBootApplication; |
|||
import com.youlai.boot.common.enums.EnvEnum; |
|||
import com.youlai.boot.codegen.enums.FormTypeEnum; |
|||
import com.youlai.boot.codegen.enums.JavaTypeEnum; |
|||
import com.youlai.boot.codegen.enums.QueryTypeEnum; |
|||
import com.youlai.boot.common.exception.BusinessException; |
|||
import com.youlai.boot.codegen.config.CodegenProperties; |
|||
import com.youlai.boot.codegen.converter.CodegenConverter; |
|||
import com.youlai.boot.codegen.mapper.DatabaseMapper; |
|||
import com.youlai.boot.codegen.mapper.GenTableMapper; |
|||
import com.youlai.boot.codegen.model.vo.ColumnMetaVO; |
|||
import com.youlai.boot.codegen.model.vo.TableMetaVO; |
|||
import com.youlai.boot.codegen.model.entity.GenTable; |
|||
import com.youlai.boot.codegen.model.entity.GenTableColumn; |
|||
import com.youlai.boot.codegen.model.form.GenConfigForm; |
|||
import com.youlai.boot.codegen.service.GenTableService; |
|||
import com.youlai.boot.codegen.service.GenTableColumnService; |
|||
import com.youlai.boot.system.service.MenuService; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.Comparator; |
|||
import java.util.List; |
|||
import java.util.Objects; |
|||
|
|||
/** |
|||
* 数据库服务实现类 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Service |
|||
@RequiredArgsConstructor |
|||
public class GenTableServiceImpl extends ServiceImpl<GenTableMapper, GenTable> implements GenTableService { |
|||
|
|||
private final DatabaseMapper databaseMapper; |
|||
private final CodegenProperties codegenProperties; |
|||
private final GenTableColumnService genTableColumnService; |
|||
private final CodegenConverter codegenConverter; |
|||
|
|||
@Value("${spring.profiles.active}") |
|||
private String springProfilesActive; |
|||
|
|||
private final MenuService menuService; |
|||
|
|||
/** |
|||
* 获取代码生成配置 |
|||
* |
|||
* @param tableName 表名 eg: sys_user |
|||
* @return 代码生成配置 |
|||
*/ |
|||
@Override |
|||
public GenConfigForm getGenTableFormData(String tableName) { |
|||
// 查询表生成配置
|
|||
GenTable genTable = this.getOne( |
|||
new LambdaQueryWrapper<>(GenTable.class) |
|||
.eq(GenTable::getTableName, tableName) |
|||
.last("LIMIT 1") |
|||
); |
|||
|
|||
// 是否有代码生成配置
|
|||
boolean hasGenTable = genTable != null; |
|||
|
|||
// 如果没有代码生成配置,则根据表的元数据生成默认配置
|
|||
if (genTable == null) { |
|||
TableMetaVO tableMetadata = databaseMapper.getTableMetadata(tableName); |
|||
Assert.isTrue(tableMetadata != null, "未找到表元数据"); |
|||
|
|||
genTable = new GenTable(); |
|||
genTable.setTableName(tableName); |
|||
|
|||
// 表注释作为业务名称,去掉表字 例如:用户表 -> 用户
|
|||
String tableComment = tableMetadata.getTableComment(); |
|||
if (StrUtil.isNotBlank(tableComment)) { |
|||
genTable.setBusinessName(tableComment.replace("表", "").trim()); |
|||
} |
|||
// 根据表名生成实体类名,支持去除前缀 例如:sys_user -> SysUser
|
|||
String removePrefix = genTable.getRemoveTablePrefix(); |
|||
String processedTable = tableName; |
|||
if (StrUtil.isNotBlank(removePrefix) && StrUtil.startWith(tableName, removePrefix)) { |
|||
processedTable = StrUtil.removePrefix(tableName, removePrefix); |
|||
} |
|||
genTable.setEntityName(StrUtil.toCamelCase(StrUtil.upperFirst(StrUtil.toCamelCase(processedTable)))); |
|||
|
|||
genTable.setPackageName(YouLaiBootApplication.class.getPackageName()); |
|||
genTable.setModuleName(codegenProperties.getDefaultConfig().getModuleName()); // 默认模块名
|
|||
genTable.setAuthor(codegenProperties.getDefaultConfig().getAuthor()); |
|||
} |
|||
|
|||
// 根据表的列 + 已经存在的字段生成配置 得到 组合后的字段生成配置
|
|||
List<GenTableColumn> genTableColumns = new ArrayList<>(); |
|||
|
|||
// 获取表的列
|
|||
List<ColumnMetaVO> tableColumns = databaseMapper.getTableColumns(tableName); |
|||
if (CollectionUtil.isNotEmpty(tableColumns)) { |
|||
// 查询字段生成配置
|
|||
List<GenTableColumn> fieldConfigList = genTableColumnService.list( |
|||
new LambdaQueryWrapper<GenTableColumn>() |
|||
.eq(GenTableColumn::getTableId, genTable.getId()) |
|||
.orderByAsc(GenTableColumn::getFieldSort) |
|||
); |
|||
Integer maxSort = fieldConfigList.stream() |
|||
.map(GenTableColumn::getFieldSort) |
|||
.filter(Objects::nonNull) // 过滤掉空值
|
|||
.max(Integer::compareTo) |
|||
.orElse(0); |
|||
for (ColumnMetaVO tableColumn : tableColumns) { |
|||
// 根据列名获取字段生成配置
|
|||
String columnName = tableColumn.getColumnName(); |
|||
GenTableColumn fieldConfig = fieldConfigList.stream() |
|||
.filter(item -> StrUtil.equals(item.getColumnName(), columnName)) |
|||
.findFirst() |
|||
.orElseGet(() -> createDefaultFieldConfig(tableColumn)); |
|||
if (fieldConfig.getFieldSort() == null) { |
|||
fieldConfig.setFieldSort(++maxSort); |
|||
} |
|||
// 根据列类型设置字段类型
|
|||
String fieldType = fieldConfig.getFieldType(); |
|||
if (StrUtil.isBlank(fieldType)) { |
|||
String javaType = JavaTypeEnum.getJavaTypeByColumnType(fieldConfig.getColumnType()); |
|||
fieldConfig.setFieldType(javaType); |
|||
} |
|||
// 如果没有代码生成配置,则默认展示在列表和表单
|
|||
if (!hasGenTable) { |
|||
fieldConfig.setIsShowInList(1); |
|||
fieldConfig.setIsShowInForm(1); |
|||
} |
|||
genTableColumns.add(fieldConfig); |
|||
} |
|||
} |
|||
// 对 genTableColumns 按照 fieldSort 排序
|
|||
genTableColumns = genTableColumns.stream().sorted(Comparator.comparing(GenTableColumn::getFieldSort)).toList(); |
|||
GenConfigForm genConfigForm = codegenConverter.toGenConfigForm(genTable, genTableColumns); |
|||
|
|||
genConfigForm.setFrontendAppName(codegenProperties.getFrontendAppName()); |
|||
genConfigForm.setBackendAppName(codegenProperties.getBackendAppName()); |
|||
return genConfigForm; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 创建默认字段配置 |
|||
* |
|||
* @param columnMetaVO 表字段元数据 |
|||
* @return |
|||
*/ |
|||
private GenTableColumn createDefaultFieldConfig(ColumnMetaVO columnMetaVO) { |
|||
GenTableColumn fieldConfig = new GenTableColumn(); |
|||
fieldConfig.setColumnName(columnMetaVO.getColumnName()); |
|||
fieldConfig.setColumnType(columnMetaVO.getDataType()); |
|||
fieldConfig.setFieldComment(columnMetaVO.getColumnComment()); |
|||
fieldConfig.setFieldName(StrUtil.toCamelCase(columnMetaVO.getColumnName())); |
|||
fieldConfig.setIsRequired("YES".equals(columnMetaVO.getIsNullable()) ? 0 : 1); |
|||
|
|||
String columnType = StrUtil.blankToDefault(fieldConfig.getColumnType(), "").toLowerCase(); |
|||
if ("date".equals(columnType)) { |
|||
fieldConfig.setFormType(FormTypeEnum.DATE); |
|||
} else if ("datetime".equals(columnType) || "timestamp".equals(columnType)) { |
|||
fieldConfig.setFormType(FormTypeEnum.DATE_TIME); |
|||
} else { |
|||
fieldConfig.setFormType(FormTypeEnum.INPUT); |
|||
} |
|||
|
|||
fieldConfig.setQueryType(QueryTypeEnum.EQ); |
|||
fieldConfig.setMaxLength(columnMetaVO.getCharacterMaximumLength()); |
|||
return fieldConfig; |
|||
} |
|||
|
|||
/** |
|||
* 保存代码生成配置 |
|||
* |
|||
* @param formData 代码生成配置表单 |
|||
*/ |
|||
@Override |
|||
public void saveGenConfig(GenConfigForm formData) { |
|||
GenTable genTable = codegenConverter.toGenTable(formData); |
|||
this.saveOrUpdate(genTable); |
|||
|
|||
// 如果选择上级菜单且当前环境不是生产环境,则保存菜单
|
|||
Long parentMenuId = formData.getParentMenuId(); |
|||
if (parentMenuId != null && !EnvEnum.PROD.getValue().equals(springProfilesActive)) { |
|||
menuService.addMenuForCodegen(parentMenuId, genTable); |
|||
} |
|||
|
|||
List<GenTableColumn> genTableColumns = codegenConverter.toGenTableColumn(formData.getFieldConfigs()); |
|||
|
|||
if (CollectionUtil.isEmpty(genTableColumns)) { |
|||
throw new BusinessException("字段配置不能为空"); |
|||
} |
|||
genTableColumns.forEach(genTableColumn -> { |
|||
genTableColumn.setTableId(genTable.getId()); |
|||
}); |
|||
genTableColumnService.saveOrUpdateBatch(genTableColumns); |
|||
} |
|||
|
|||
/** |
|||
* 删除代码生成配置 |
|||
* |
|||
* @param tableName 表名 |
|||
*/ |
|||
@Override |
|||
public void deleteGenConfig(String tableName) { |
|||
GenTable genTable = this.getOne(new LambdaQueryWrapper<GenTable>() |
|||
.eq(GenTable::getTableName, tableName)); |
|||
|
|||
boolean result = this.remove(new LambdaQueryWrapper<GenTable>() |
|||
.eq(GenTable::getTableName, tableName) |
|||
); |
|||
if (result) { |
|||
genTableColumnService.remove(new LambdaQueryWrapper<GenTableColumn>() |
|||
.eq(GenTableColumn::getTableId, genTable.getId()) |
|||
); |
|||
} |
|||
} |
|||
|
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
package com.youlai.boot.common.annotation; |
|||
|
|||
import java.lang.annotation.*; |
|||
|
|||
/** |
|||
* 数据权限注解 |
|||
* |
|||
* @author zc |
|||
* @since 2.0.0 |
|||
*/ |
|||
@Documented |
|||
@Retention(RetentionPolicy.RUNTIME) |
|||
@Target({ElementType.TYPE, ElementType.METHOD}) |
|||
public @interface DataPermission { |
|||
|
|||
/** |
|||
* 数据权限 {@link com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor} |
|||
*/ |
|||
String deptAlias() default ""; |
|||
|
|||
String deptIdColumnName() default "dept_id"; |
|||
|
|||
String userAlias() default ""; |
|||
|
|||
String userIdColumnName() default "create_by"; |
|||
|
|||
} |
|||
|
|||
@ -0,0 +1,47 @@ |
|||
package com.youlai.boot.common.annotation; |
|||
|
|||
import com.youlai.boot.common.enums.ActionTypeEnum; |
|||
import com.youlai.boot.common.enums.LogModuleEnum; |
|||
|
|||
import java.lang.annotation.*; |
|||
|
|||
/** |
|||
* 日志注解 |
|||
* |
|||
* @author Ray |
|||
* @since 2024/6/25 |
|||
*/ |
|||
@Retention(RetentionPolicy.RUNTIME) |
|||
@Target(ElementType.METHOD) |
|||
@Documented |
|||
public @interface Log { |
|||
|
|||
/** |
|||
* 模块 |
|||
* |
|||
* @return 模块 |
|||
*/ |
|||
LogModuleEnum module(); |
|||
|
|||
/** |
|||
* 操作类型 |
|||
* |
|||
* @return 操作类型 |
|||
*/ |
|||
ActionTypeEnum value(); |
|||
|
|||
/** |
|||
* 操作标题(可选,默认使用枚举描述) |
|||
* |
|||
* @return 标题 |
|||
*/ |
|||
String title() default ""; |
|||
|
|||
/** |
|||
* 自定义日志内容(可选,用于记录操作细节) |
|||
* |
|||
* @return 日志内容 |
|||
*/ |
|||
String content() default ""; |
|||
|
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
package com.youlai.boot.common.annotation; |
|||
|
|||
|
|||
import java.lang.annotation.*; |
|||
|
|||
/** |
|||
* 防止重复提交注解 |
|||
* <p> |
|||
* 该注解用于方法上,防止在指定时间内的重复提交。 默认时间为5秒。 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2.3.0 |
|||
*/ |
|||
@Target(ElementType.METHOD) |
|||
@Retention(RetentionPolicy.RUNTIME) |
|||
@Documented |
|||
@Inherited |
|||
public @interface RepeatSubmit { |
|||
|
|||
/** |
|||
* 锁过期时间(秒) |
|||
* <p> |
|||
* 默认5秒内不允许重复提交 |
|||
*/ |
|||
int expire() default 5; |
|||
|
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
package com.youlai.boot.common.annotation; |
|||
|
|||
import com.youlai.boot.common.validator.FieldValidator; |
|||
import jakarta.validation.Constraint; |
|||
import jakarta.validation.Payload; |
|||
|
|||
import java.lang.annotation.*; |
|||
|
|||
/** |
|||
* 用于验证字段值是否合法的注解 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2.18.0 |
|||
*/ |
|||
@Documented |
|||
@Constraint(validatedBy = FieldValidator.class) |
|||
@Target({ElementType.FIELD, ElementType.PARAMETER}) |
|||
@Retention(RetentionPolicy.RUNTIME) |
|||
public @interface ValidField { |
|||
|
|||
/** |
|||
* 验证失败时的错误信息。 |
|||
*/ |
|||
String message() default "非法字段"; |
|||
|
|||
Class<?>[] groups() default {}; |
|||
|
|||
Class<? extends Payload>[] payload() default {}; |
|||
|
|||
/** |
|||
* 允许的合法值列表。 |
|||
*/ |
|||
String[] allowedValues(); |
|||
|
|||
} |
|||
@ -0,0 +1,144 @@ |
|||
package com.youlai.boot.common.aspect; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import cn.hutool.http.useragent.UserAgent; |
|||
import cn.hutool.http.useragent.UserAgentUtil; |
|||
import com.youlai.boot.common.annotation.Log; |
|||
import com.youlai.boot.common.enums.ActionTypeEnum; |
|||
import com.youlai.boot.common.enums.LogModuleEnum; |
|||
import com.youlai.boot.common.util.IPUtils; |
|||
import com.youlai.boot.framework.security.util.SecurityUtils; |
|||
import com.youlai.boot.system.model.entity.SysLog; |
|||
import com.youlai.boot.system.service.LogService; |
|||
import jakarta.servlet.http.HttpServletRequest; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.aspectj.lang.ProceedingJoinPoint; |
|||
import org.aspectj.lang.annotation.Around; |
|||
import org.aspectj.lang.annotation.Aspect; |
|||
import org.aspectj.lang.annotation.Pointcut; |
|||
import org.springframework.scheduling.annotation.Async; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.web.context.request.RequestContextHolder; |
|||
import org.springframework.web.context.request.ServletRequestAttributes; |
|||
|
|||
import java.time.LocalDateTime; |
|||
|
|||
/** |
|||
* 日志切面 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Aspect |
|||
@Component |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class LogAspect { |
|||
|
|||
private final LogService logService; |
|||
|
|||
/** |
|||
* 日志注解切点 |
|||
*/ |
|||
@Pointcut("@annotation(logAnnotation)") |
|||
public void logPointCut(Log logAnnotation) { |
|||
} |
|||
|
|||
/** |
|||
* 环绕通知:记录操作日志 |
|||
*/ |
|||
@Around(value = "logPointCut(logAnnotation)", argNames = "pjp,logAnnotation") |
|||
public Object around(ProceedingJoinPoint pjp, Log logAnnotation) throws Throwable { |
|||
|
|||
long startTime = System.currentTimeMillis(); |
|||
// 在方法执行前获取用户信息,避免 logout 等操作清除 SecurityContext 后无法获取
|
|||
Long userId = SecurityUtils.getUserId(); |
|||
String username = SecurityUtils.getUsername(); |
|||
Object result = null; |
|||
Exception exception = null; |
|||
|
|||
try { |
|||
result = pjp.proceed(); |
|||
return result; |
|||
} catch (Exception e) { |
|||
exception = e; |
|||
throw e; |
|||
} finally { |
|||
long executionTime = System.currentTimeMillis() - startTime; |
|||
// fallback:登录等场景在 proceed() 前未认证,需在 proceed() 后获取
|
|||
if (userId == null) { |
|||
userId = SecurityUtils.getUserId(); |
|||
username = SecurityUtils.getUsername(); |
|||
} |
|||
try { |
|||
saveLogAsync(logAnnotation, executionTime, exception, userId, username); |
|||
} catch (Exception ex) { |
|||
log.error("保存操作日志失败", ex); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 异步保存日志 |
|||
*/ |
|||
@Async |
|||
public void saveLogAsync(Log logAnnotation, long executionTime, Exception exception, Long userId, String username) { |
|||
try { |
|||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); |
|||
if (attributes == null) { |
|||
return; |
|||
} |
|||
|
|||
HttpServletRequest request = attributes.getRequest(); |
|||
|
|||
// 解析 User-Agent
|
|||
String userAgentStr = request.getHeader("User-Agent"); |
|||
UserAgent userAgent = UserAgentUtil.parse(userAgentStr); |
|||
|
|||
// 解析 IP 地区
|
|||
String ip = IPUtils.getIpAddr(request); |
|||
String region = IPUtils.getRegion(ip); |
|||
String province = null; |
|||
String city = null; |
|||
if (StrUtil.isNotBlank(region)) { |
|||
String[] parts = region.split("\\|"); |
|||
if (parts.length >= 3) { |
|||
province = StrUtil.blankToDefault(parts[2], null); |
|||
city = StrUtil.blankToDefault(parts[3], null); |
|||
} |
|||
} |
|||
|
|||
// 构建日志实体
|
|||
LogModuleEnum module = logAnnotation.module(); |
|||
ActionTypeEnum actionType = logAnnotation.value(); |
|||
String title = StrUtil.blankToDefault(logAnnotation.title(), |
|||
module.getLabel() + "-" + actionType.getLabel()); |
|||
String content = logAnnotation.content(); |
|||
|
|||
SysLog logEntity = new SysLog(); |
|||
logEntity.setModule(module); |
|||
logEntity.setActionType(actionType); |
|||
logEntity.setTitle(title); |
|||
logEntity.setContent(content); |
|||
logEntity.setOperatorId(userId); |
|||
logEntity.setOperatorName(username); |
|||
logEntity.setRequestUri(request.getRequestURI()); |
|||
logEntity.setRequestMethod(request.getMethod()); |
|||
logEntity.setIp(ip); |
|||
logEntity.setProvince(province); |
|||
logEntity.setCity(city); |
|||
logEntity.setDevice(userAgent.getOs().getName()); |
|||
logEntity.setOs(userAgent.getOs().getName()); |
|||
logEntity.setBrowser(userAgent.getBrowser().getName()); |
|||
logEntity.setStatus(exception == null ? 1 : 0); |
|||
logEntity.setErrorMsg(exception != null ? exception.getMessage() : null); |
|||
logEntity.setExecutionTime((int) executionTime); |
|||
logEntity.setCreateTime(LocalDateTime.now()); |
|||
|
|||
logService.save(logEntity); |
|||
} catch (Exception e) { |
|||
log.error("保存操作日志异常: {}", e.getMessage()); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
package com.youlai.boot.common.aspect; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import cn.hutool.crypto.digest.DigestUtil; |
|||
import com.youlai.boot.common.constant.RedisConstants; |
|||
import com.youlai.boot.common.constant.SecurityConstants; |
|||
import com.youlai.boot.common.result.ResultCode; |
|||
import com.youlai.boot.common.exception.BusinessException; |
|||
import com.youlai.boot.common.annotation.RepeatSubmit; |
|||
import com.youlai.boot.common.util.IPUtils; |
|||
import jakarta.servlet.http.HttpServletRequest; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.aspectj.lang.ProceedingJoinPoint; |
|||
import org.aspectj.lang.annotation.Around; |
|||
import org.aspectj.lang.annotation.Aspect; |
|||
import org.aspectj.lang.annotation.Pointcut; |
|||
import org.redisson.api.RLock; |
|||
import org.redisson.api.RedissonClient; |
|||
import org.springframework.http.HttpHeaders; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.web.context.request.RequestContextHolder; |
|||
import org.springframework.web.context.request.ServletRequestAttributes; |
|||
|
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
/** |
|||
* 防重复提交切面 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2.3.0 |
|||
*/ |
|||
@Aspect |
|||
@Component |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class RepeatSubmitAspect { |
|||
|
|||
private final RedissonClient redissonClient; |
|||
|
|||
/** |
|||
* 防重复提交切点 |
|||
*/ |
|||
@Pointcut("@annotation(repeatSubmit)") |
|||
public void repeatSubmitPointCut(RepeatSubmit repeatSubmit) { |
|||
} |
|||
|
|||
/** |
|||
* 环绕通知:处理防重复提交逻辑 |
|||
*/ |
|||
@Around(value = "repeatSubmitPointCut(repeatSubmit)", argNames = "pjp,repeatSubmit") |
|||
public Object handleRepeatSubmit(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable { |
|||
String lockKey = buildLockKey(); |
|||
|
|||
int expire = repeatSubmit.expire(); |
|||
RLock lock = redissonClient.getLock(lockKey); |
|||
|
|||
boolean locked = lock.tryLock(0, expire, TimeUnit.SECONDS); |
|||
if (!locked) { |
|||
throw new BusinessException(ResultCode.DUPLICATE_SUBMISSION); |
|||
} |
|||
return pjp.proceed(); |
|||
} |
|||
|
|||
/** |
|||
* 生成防重复提交锁的 key |
|||
* @return 锁的 key |
|||
*/ |
|||
private String buildLockKey() { |
|||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); |
|||
// 用户唯一标识
|
|||
String userIdentifier = getUserIdentifier(request); |
|||
// 请求唯一标识 = 请求方法 + 请求路径 + 请求参数(严谨的做法)
|
|||
String requestIdentifier = StrUtil.join(":", request.getMethod(), request.getRequestURI()); |
|||
return StrUtil.format(RedisConstants.Lock.RESUBMIT, userIdentifier, requestIdentifier); |
|||
} |
|||
|
|||
/** |
|||
* 获取用户唯一标识 |
|||
* 1. 从请求头中获取 Token,使用 SHA-256 加密 Token 作为用户唯一标识 |
|||
* 2. 如果 Token 为空,使用 IP 作为用户唯一标识 |
|||
* |
|||
* @param request 请求对象 |
|||
* @return 用户唯一标识 |
|||
*/ |
|||
private String getUserIdentifier(HttpServletRequest request) { |
|||
// 用户身份唯一标识
|
|||
String userIdentifier; |
|||
// 从请求头中获取 Token
|
|||
String tokenHeader = request.getHeader(HttpHeaders.AUTHORIZATION); |
|||
if (StrUtil.isNotBlank(tokenHeader) && tokenHeader.startsWith(SecurityConstants.BEARER_TOKEN_PREFIX)) { |
|||
String rawToken = tokenHeader.substring(SecurityConstants.BEARER_TOKEN_PREFIX.length()); // 去掉 Bearer 后的 Token
|
|||
userIdentifier = DigestUtil.sha256Hex(rawToken); // 使用 SHA-256 加密 Token 作为用户唯一标识
|
|||
} else { |
|||
userIdentifier = IPUtils.getIpAddr(request); // 使用 IP 作为用户唯一标识
|
|||
} |
|||
return userIdentifier; |
|||
} |
|||
|
|||
|
|||
} |
|||
|
|||
@ -0,0 +1,48 @@ |
|||
package com.youlai.boot.common.base; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.*; |
|||
import com.fasterxml.jackson.annotation.JsonFormat; |
|||
import com.fasterxml.jackson.annotation.JsonInclude; |
|||
import lombok.Data; |
|||
|
|||
import java.io.Serial; |
|||
import java.io.Serializable; |
|||
import java.time.LocalDateTime; |
|||
|
|||
/** |
|||
* 基础实体类 |
|||
* |
|||
* <p>实体类的基类,包含了实体类的公共属性,如创建时间、更新时间、逻辑删除标识等</p> |
|||
* |
|||
* @author Ray |
|||
* @since 2024/6/23 |
|||
*/ |
|||
@Data |
|||
public class BaseEntity implements Serializable { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
/** |
|||
* 主键ID |
|||
*/ |
|||
@TableId(type = IdType.AUTO) |
|||
private Long id; |
|||
|
|||
/** |
|||
* 创建时间 |
|||
*/ |
|||
@TableField(fill = FieldFill.INSERT) |
|||
@JsonInclude(value = JsonInclude.Include.NON_NULL) |
|||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
|||
private LocalDateTime createTime; |
|||
|
|||
/** |
|||
* 更新时间 |
|||
*/ |
|||
@TableField(fill = FieldFill.INSERT_UPDATE) |
|||
@JsonInclude(value = JsonInclude.Include.NON_NULL) |
|||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") |
|||
private LocalDateTime updateTime; |
|||
|
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
package com.youlai.boot.common.base; |
|||
|
|||
import com.youlai.boot.common.annotation.ValidField; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
import java.io.Serial; |
|||
import java.io.Serializable; |
|||
|
|||
@Data |
|||
@Schema |
|||
public class BaseQuery implements Serializable { |
|||
|
|||
@Serial |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
@Schema(description = "页码", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "1") |
|||
private Integer pageNum = 1; |
|||
|
|||
@Schema(description = "每页记录数", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "10") |
|||
private Integer pageSize = 10; |
|||
|
|||
@Schema(description = "排序字段", requiredMode = Schema.RequiredMode.NOT_REQUIRED) |
|||
@ValidField(allowedValues = {"create_time", "update_time"}) |
|||
private String sortBy; |
|||
|
|||
@Schema(description = "排序方式(正序:ASC;反序:DESC)", requiredMode = Schema.RequiredMode.NOT_REQUIRED) |
|||
private String order; |
|||
|
|||
public boolean isPaged() { |
|||
return pageNum != null && pageSize != null && pageSize > 0; |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
package com.youlai.boot.common.base; |
|||
|
|||
|
|||
import cn.hutool.core.util.ObjectUtil; |
|||
|
|||
import java.util.EnumSet; |
|||
import java.util.Objects; |
|||
|
|||
/** |
|||
* 枚举通用接口 |
|||
* |
|||
* @author haoxr |
|||
* @since 2022/3/27 12:06 |
|||
*/ |
|||
public interface IBaseEnum<T> { |
|||
|
|||
T getValue(); |
|||
|
|||
String getLabel(); |
|||
|
|||
/** |
|||
* 根据值获取枚举 |
|||
* |
|||
* @param value |
|||
* @param clazz |
|||
* @param <E> 枚举 |
|||
* @return |
|||
*/ |
|||
static <E extends Enum<E> & IBaseEnum> E getEnumByValue(Object value, Class<E> clazz) { |
|||
Objects.requireNonNull(value); |
|||
EnumSet<E> allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举
|
|||
E matchEnum = allEnums.stream() |
|||
.filter(e -> ObjectUtil.equal(e.getValue(), value)) |
|||
.findFirst() |
|||
.orElse(null); |
|||
return matchEnum; |
|||
} |
|||
|
|||
/** |
|||
* 根据文本标签获取值 |
|||
* |
|||
* @param value |
|||
* @param clazz |
|||
* @param <E> |
|||
* @return |
|||
*/ |
|||
static <E extends Enum<E> & IBaseEnum> String getLabelByValue(Object value, Class<E> clazz) { |
|||
Objects.requireNonNull(value); |
|||
EnumSet<E> allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举
|
|||
E matchEnum = allEnums.stream() |
|||
.filter(e -> ObjectUtil.equal(e.getValue(), value)) |
|||
.findFirst() |
|||
.orElse(null); |
|||
|
|||
String label = null; |
|||
if (matchEnum != null) { |
|||
label = matchEnum.getLabel(); |
|||
} |
|||
return label; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 根据文本标签获取值 |
|||
* |
|||
* @param label |
|||
* @param clazz |
|||
* @param <E> |
|||
* @return |
|||
*/ |
|||
static <E extends Enum<E> & IBaseEnum> Object getValueByLabel(String label, Class<E> clazz) { |
|||
Objects.requireNonNull(label); |
|||
EnumSet<E> allEnums = EnumSet.allOf(clazz); // 获取类型下的所有枚举
|
|||
String finalLabel = label; |
|||
E matchEnum = allEnums.stream() |
|||
.filter(e -> ObjectUtil.equal(e.getLabel(), finalLabel)) |
|||
.findFirst() |
|||
.orElse(null); |
|||
|
|||
Object value = null; |
|||
if (matchEnum != null) { |
|||
value = matchEnum.getValue(); |
|||
} |
|||
return value; |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,48 @@ |
|||
package com.youlai.boot.common.constant; |
|||
|
|||
/** |
|||
* JWT Claims声明常量 |
|||
* <p> |
|||
* JWT Claims 属于 Payload 的一部分,包含了一些实体(通常指的用户)的状态和额外的元数据。 |
|||
* |
|||
* @author haoxr |
|||
* @since 2023/11/24 |
|||
*/ |
|||
public interface JwtClaimConstants { |
|||
|
|||
/** |
|||
* 令牌类型 |
|||
*/ |
|||
String TOKEN_TYPE = "tokenType"; |
|||
|
|||
/** |
|||
* 用户ID |
|||
*/ |
|||
String USER_ID = "userId"; |
|||
|
|||
/** |
|||
* 部门ID |
|||
*/ |
|||
String DEPT_ID = "deptId"; |
|||
|
|||
/** |
|||
* 数据权限列表 |
|||
* <p> |
|||
* 存储用户所有角色的数据权限范围,用于实现多角色权限合并(并集策略) |
|||
*/ |
|||
String DATA_SCOPES = "dataScopes"; |
|||
|
|||
/** |
|||
* 权限(角色Code)集合 |
|||
*/ |
|||
String AUTHORITIES = "authorities"; |
|||
|
|||
/** |
|||
* Token 版本号 |
|||
* <p> |
|||
* 用于用户级会话失效,当用户修改密码、被禁用、强制下线时递增版本号, |
|||
* 使该用户之前签发的所有 Token 失效。 |
|||
*/ |
|||
String TOKEN_VERSION = "tokenVersion"; |
|||
|
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
package com.youlai.boot.common.constant; |
|||
|
|||
/** |
|||
* Redis 常量 |
|||
* |
|||
* @author Theo |
|||
* @since 2024-7-29 11:46:08 |
|||
*/ |
|||
public interface RedisConstants { |
|||
|
|||
/** |
|||
* 限流相关键 |
|||
*/ |
|||
interface RateLimiter { |
|||
String IP = "rate_limiter:ip:{}"; // IP限流(示例:rate_limiter:ip:192.168.1.1)
|
|||
} |
|||
|
|||
/** |
|||
* 分布式锁相关键 |
|||
*/ |
|||
interface Lock { |
|||
String RESUBMIT = "lock:resubmit:{}:{}"; // 防重复提交(示例:lock:resubmit:userIdentifier:requestIdentifier)
|
|||
} |
|||
|
|||
/** |
|||
* 认证模块 |
|||
*/ |
|||
interface Auth { |
|||
// 存储访问令牌对应的用户会话信息(accessToken -> UserSession)
|
|||
String ACCESS_TOKEN_USER = "auth:token:access:{}"; |
|||
// 存储刷新令牌对应的用户会话信息(refreshToken -> UserSession)
|
|||
String REFRESH_TOKEN_USER = "auth:token:refresh:{}"; |
|||
// 用户与访问令牌的映射(userId -> accessToken)
|
|||
String USER_ACCESS_TOKEN = "auth:user:access:{}"; |
|||
// 用户与刷新令牌的映射(userId -> refreshToken
|
|||
String USER_REFRESH_TOKEN = "auth:user:refresh:{}"; |
|||
// 已撤销 Token 的 JTI(单端退出/会话注销):如果 jti 在撤销列表中,则 Token 立即无效
|
|||
String BLACKLIST_TOKEN = "auth:token:blacklist:{}"; |
|||
String REVOKED_JTI = BLACKLIST_TOKEN; |
|||
// 用户 Token 版本号(用于按用户失效历史 JWT):token.tokenVersion != redis.tokenVersion => token 无效
|
|||
String USER_TOKEN_VERSION = "auth:user:token_version:{}"; |
|||
} |
|||
|
|||
/** |
|||
* 验证码模块 |
|||
*/ |
|||
interface Captcha { |
|||
String IMAGE_CODE = "captcha:image:{}"; // 图形验证码
|
|||
String SMS_LOGIN_CODE = "captcha:sms_login:{}"; // 登录短信验证码
|
|||
String SMS_REGISTER_CODE = "captcha:sms_register:{}";// 注册短信验证码
|
|||
String MOBILE_CODE = "captcha:mobile:{}"; // 绑定、更换手机验证码
|
|||
String EMAIL_CODE = "captcha:email:{}"; // 邮箱验证码
|
|||
} |
|||
|
|||
/** |
|||
* 系统模块 |
|||
*/ |
|||
interface System { |
|||
String CONFIG = "system:config"; // 系统配置
|
|||
String ROLE_PERMS = "system:role:perms"; // 系统角色和权限映射
|
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
package com.youlai.boot.common.constant; |
|||
|
|||
/** |
|||
* 安全模块常量 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2023/11/24 |
|||
*/ |
|||
public interface SecurityConstants { |
|||
|
|||
/** |
|||
* 登录路径 |
|||
*/ |
|||
String LOGIN_PATH = "/api/v1/auth/login"; |
|||
|
|||
/** |
|||
* JWT Token 前缀 |
|||
*/ |
|||
String BEARER_TOKEN_PREFIX = "Bearer "; |
|||
|
|||
/** |
|||
* 角色前缀,用于区分 authorities 角色和权限, ROLE_* 角色 、没有前缀的是权限 |
|||
*/ |
|||
String ROLE_PREFIX = "ROLE_"; |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
package com.youlai.boot.common.constant; |
|||
|
|||
/** |
|||
* 系统常量 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 1.0.0 |
|||
*/ |
|||
public interface SystemConstants { |
|||
|
|||
/** |
|||
* 根节点ID |
|||
*/ |
|||
Long ROOT_NODE_ID = 0L; |
|||
|
|||
/** |
|||
* 系统默认密码 |
|||
*/ |
|||
String DEFAULT_PASSWORD = "123456"; |
|||
|
|||
/** |
|||
* 超级管理员角色编码 |
|||
*/ |
|||
String ROOT_ROLE_CODE = "ROOT"; |
|||
|
|||
|
|||
/** |
|||
* 系统配置 IP的QPS限流的KEY |
|||
*/ |
|||
String SYSTEM_CONFIG_IP_QPS_LIMIT_KEY = "IP_QPS_THRESHOLD_LIMIT"; |
|||
|
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
package com.youlai.boot.common.enums; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.EnumValue; |
|||
import com.fasterxml.jackson.annotation.JsonValue; |
|||
import com.youlai.boot.common.base.IBaseEnum; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Getter; |
|||
|
|||
/** |
|||
* 操作类型枚举 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Schema(enumAsRef = true) |
|||
@Getter |
|||
public enum ActionTypeEnum implements IBaseEnum<Integer> { |
|||
|
|||
LOGIN(1, "登录"), |
|||
LOGOUT(2, "登出"), |
|||
INSERT(3, "新增"), |
|||
UPDATE(4, "修改"), |
|||
DELETE(5, "删除"), |
|||
GRANT(6, "授权"), |
|||
EXPORT(7, "导出"), |
|||
IMPORT(8, "导入"), |
|||
UPLOAD(9, "上传"), |
|||
DOWNLOAD(10, "下载"), |
|||
CHANGE_PASSWORD(11, "修改密码"), |
|||
RESET_PASSWORD(12, "重置密码"), |
|||
ENABLE(13, "启用"), |
|||
DISABLE(14, "禁用"), |
|||
LIST(15, "查询列表"), |
|||
OTHER(99, "其他"); |
|||
|
|||
@EnumValue |
|||
private final Integer value; |
|||
|
|||
@JsonValue |
|||
private final String label; |
|||
|
|||
ActionTypeEnum(Integer value, String label) { |
|||
this.value = value; |
|||
this.label = label; |
|||
} |
|||
|
|||
@Override |
|||
public Integer getValue() { |
|||
return this.value; |
|||
} |
|||
|
|||
@Override |
|||
public String getLabel() { |
|||
return this.label; |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
package com.youlai.boot.common.enums; |
|||
|
|||
/** |
|||
* EasyCaptcha 验证码类型枚举 |
|||
* |
|||
* @author haoxr |
|||
* @since 2.5.1 |
|||
*/ |
|||
public enum CaptchaTypeEnum { |
|||
|
|||
/** |
|||
* 圆圈干扰验证码 |
|||
*/ |
|||
CIRCLE, |
|||
/** |
|||
* GIF验证码 |
|||
*/ |
|||
GIF, |
|||
/** |
|||
* 干扰线验证码 |
|||
*/ |
|||
LINE, |
|||
/** |
|||
* 扭曲干扰验证码 |
|||
*/ |
|||
SHEAR |
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
package com.youlai.boot.common.enums; |
|||
|
|||
import com.youlai.boot.common.base.IBaseEnum; |
|||
import lombok.Getter; |
|||
|
|||
/** |
|||
* 数据权限枚举 |
|||
* <p> |
|||
* 多角色数据权限合并策略:取并集(OR),即用户能看到所有角色权限范围内的数据。 |
|||
* 如果任一角色是 ALL,则直接跳过数据权限过滤。 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2.3.0 |
|||
*/ |
|||
@Getter |
|||
public enum DataScopeEnum implements IBaseEnum<Integer> { |
|||
|
|||
/** |
|||
* 所有数据权限 - 最高权限,可查看所有数据 |
|||
*/ |
|||
ALL(1, "所有数据"), |
|||
|
|||
/** |
|||
* 部门及子部门数据 - 可查看本部门及其下属所有部门的数据 |
|||
*/ |
|||
DEPT_AND_SUB(2, "部门及子部门数据"), |
|||
|
|||
/** |
|||
* 本部门数据 - 仅可查看本部门的数据 |
|||
*/ |
|||
DEPT(3, "本部门数据"), |
|||
|
|||
/** |
|||
* 本人数据 - 仅可查看自己的数据 |
|||
*/ |
|||
SELF(4, "本人数据"), |
|||
|
|||
/** |
|||
* 自定义部门数据 - 可查看指定部门的数据 |
|||
* <p> |
|||
* 需要配合 sys_role_dept 表使用,存储角色可访问的部门ID列表 |
|||
*/ |
|||
CUSTOM(5, "自定义部门数据"); |
|||
|
|||
private final Integer value; |
|||
|
|||
private final String label; |
|||
|
|||
DataScopeEnum(Integer value, String label) { |
|||
this.value = value; |
|||
this.label = label; |
|||
} |
|||
|
|||
/** |
|||
* 判断是否为全部数据权限 |
|||
* |
|||
* @param value 数据权限值 |
|||
* @return 是否为全部数据权限 |
|||
*/ |
|||
public static boolean isAll(Integer value) { |
|||
return ALL.getValue().equals(value); |
|||
} |
|||
|
|||
/** |
|||
* 根据值获取枚举 |
|||
* |
|||
* @param value 数据权限值 |
|||
* @return 枚举对象,未找到则返回 null |
|||
*/ |
|||
public static DataScopeEnum getByValue(Integer value) { |
|||
if (value == null) { |
|||
return null; |
|||
} |
|||
for (DataScopeEnum dataScope : values()) { |
|||
if (dataScope.getValue().equals(value)) { |
|||
return dataScope; |
|||
} |
|||
} |
|||
return null; |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
package com.youlai.boot.common.enums; |
|||
|
|||
import com.youlai.boot.common.base.IBaseEnum; |
|||
import lombok.Getter; |
|||
|
|||
/** |
|||
* 环境枚举 |
|||
* |
|||
* @author Ray |
|||
* @since 4.0.0 |
|||
*/ |
|||
@Getter |
|||
public enum EnvEnum implements IBaseEnum<String> { |
|||
|
|||
DEV("dev", "开发环境"), |
|||
PROD("prod", "生产环境"); |
|||
|
|||
private final String value; |
|||
|
|||
private final String label; |
|||
|
|||
EnvEnum(String value, String label) { |
|||
this.value = value; |
|||
this.label = label; |
|||
} |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
package com.youlai.boot.common.enums; |
|||
|
|||
import com.baomidou.mybatisplus.annotation.EnumValue; |
|||
import com.fasterxml.jackson.annotation.JsonValue; |
|||
import com.youlai.boot.common.base.IBaseEnum; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Getter; |
|||
|
|||
/** |
|||
* 日志模块枚举 |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Schema(enumAsRef = true) |
|||
@Getter |
|||
public enum LogModuleEnum implements IBaseEnum<Integer> { |
|||
|
|||
LOGIN(1, "登录"), |
|||
USER(2, "用户管理"), |
|||
ROLE(3, "角色管理"), |
|||
DEPT(4, "部门管理"), |
|||
MENU(5, "菜单管理"), |
|||
DICT(6, "字典管理"), |
|||
CONFIG(7, "系统配置"), |
|||
FILE(8, "文件管理"), |
|||
NOTICE(9, "通知公告"), |
|||
LOG(10, "日志管理"), |
|||
CODEGEN(11, "代码生成"), |
|||
OTHER(99, "其他"); |
|||
|
|||
@EnumValue |
|||
private final Integer value; |
|||
|
|||
@JsonValue |
|||
private final String label; |
|||
|
|||
LogModuleEnum(Integer value, String label) { |
|||
this.value = value; |
|||
this.label = label; |
|||
} |
|||
|
|||
@Override |
|||
public Integer getValue() { |
|||
return this.value; |
|||
} |
|||
|
|||
@Override |
|||
public String getLabel() { |
|||
return this.label; |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
package com.youlai.boot.common.enums; |
|||
|
|||
import com.youlai.boot.common.base.IBaseEnum; |
|||
import lombok.Getter; |
|||
|
|||
/** |
|||
* 状态枚举 |
|||
* |
|||
* @author haoxr |
|||
* @since 2022/10/14 |
|||
*/ |
|||
@Getter |
|||
public enum StatusEnum implements IBaseEnum<Integer> { |
|||
|
|||
ENABLE(1, "启用"), |
|||
DISABLE (0, "禁用"); |
|||
|
|||
private final Integer value; |
|||
|
|||
|
|||
private final String label; |
|||
|
|||
StatusEnum(Integer value, String label) { |
|||
this.value = value; |
|||
this.label = label; |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
package com.youlai.boot.common.exception; |
|||
|
|||
import com.youlai.boot.common.result.IResultCode; |
|||
import lombok.Getter; |
|||
import org.slf4j.helpers.MessageFormatter; |
|||
|
|||
/** |
|||
* 自定义业务异常 |
|||
* |
|||
* @author Ray |
|||
* @since 2022/7/31 |
|||
*/ |
|||
@Getter |
|||
public class BusinessException extends RuntimeException { |
|||
|
|||
public IResultCode resultCode; |
|||
|
|||
public BusinessException(IResultCode errorCode) { |
|||
super(errorCode.getMsg()); |
|||
this.resultCode = errorCode; |
|||
} |
|||
|
|||
|
|||
public BusinessException(IResultCode errorCode,String message) { |
|||
super(message); |
|||
this.resultCode = errorCode; |
|||
} |
|||
|
|||
|
|||
public BusinessException(String message, Throwable cause) { |
|||
super(message, cause); |
|||
} |
|||
|
|||
public BusinessException(Throwable cause) { |
|||
super(cause); |
|||
} |
|||
|
|||
public BusinessException(String message, Object... args) { |
|||
super(formatMessage(message, args)); |
|||
} |
|||
|
|||
private static String formatMessage(String message, Object... args) { |
|||
return MessageFormatter.arrayFormat(message, args).getMessage(); |
|||
} |
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
package com.youlai.boot.common.model; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
|
|||
/** |
|||
* 键值对 |
|||
* |
|||
* @author haoxr |
|||
* @since 2024/5/25 |
|||
*/ |
|||
@Schema(description = "键值对") |
|||
@Data |
|||
@NoArgsConstructor |
|||
public class KeyValue { |
|||
|
|||
public KeyValue(String key, String value) { |
|||
this.key = key; |
|||
this.value = value; |
|||
} |
|||
|
|||
@Schema(description = "选项的值") |
|||
private String key; |
|||
|
|||
@Schema(description = "选项的标签") |
|||
private String value; |
|||
|
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
package com.youlai.boot.common.model; |
|||
|
|||
import com.fasterxml.jackson.annotation.JsonInclude; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 下拉选项对象 |
|||
* |
|||
* @author haoxr |
|||
* @since 2022/1/22 |
|||
*/ |
|||
@Schema(description ="下拉选项对象") |
|||
@Data |
|||
@NoArgsConstructor |
|||
public class Option<T> { |
|||
|
|||
public Option(T value, String label) { |
|||
this.value = value; |
|||
this.label = label; |
|||
} |
|||
|
|||
public Option(T value, String label, List<Option<T>> children) { |
|||
this.value = value; |
|||
this.label = label; |
|||
this.children= children; |
|||
} |
|||
|
|||
public Option(T value, String label, String tag) { |
|||
this.value = value; |
|||
this.label = label; |
|||
this.tag= tag; |
|||
} |
|||
|
|||
|
|||
@Schema(description="选项的值") |
|||
private T value; |
|||
|
|||
@Schema(description="选项的标签") |
|||
private String label; |
|||
|
|||
@Schema(description = "标签类型") |
|||
@JsonInclude(value = JsonInclude.Include.NON_EMPTY) |
|||
private String tag; |
|||
|
|||
@Schema(description="子选项列表") |
|||
@JsonInclude(value = JsonInclude.Include.NON_EMPTY) |
|||
private List<Option<T>> children; |
|||
|
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
package com.youlai.boot.common.result; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* Excel导出响应结构体 |
|||
* |
|||
* @author Theo |
|||
* @since 2025/1/14 11:46:08 |
|||
*/ |
|||
@Data |
|||
public class ExcelResult { |
|||
|
|||
/** |
|||
* 响应码,来确定是否导入成功 |
|||
*/ |
|||
private String code; |
|||
|
|||
/** |
|||
* 有效条数 |
|||
*/ |
|||
private Integer validCount; |
|||
|
|||
/** |
|||
* 无效条数 |
|||
*/ |
|||
private Integer invalidCount; |
|||
|
|||
/** |
|||
* 错误提示信息 |
|||
*/ |
|||
private List<String> messageList; |
|||
|
|||
public ExcelResult() { |
|||
this.code = ResultCode.SUCCESS.getCode(); |
|||
this.validCount = 0; |
|||
this.invalidCount = 0; |
|||
this.messageList = new ArrayList<>(); |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
package com.youlai.boot.common.result; |
|||
|
|||
/** |
|||
* 响应码接口 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 1.0.0 |
|||
**/ |
|||
public interface IResultCode { |
|||
|
|||
String getCode(); |
|||
|
|||
String getMsg(); |
|||
|
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
package com.youlai.boot.common.result; |
|||
|
|||
import com.baomidou.mybatisplus.core.metadata.IPage; |
|||
import lombok.Data; |
|||
|
|||
import java.io.Serializable; |
|||
import java.util.Collections; |
|||
import java.util.List; |
|||
|
|||
/** |
|||
* 分页响应结构体 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2022/2/18 |
|||
*/ |
|||
@Data |
|||
public class PageResult<T> implements Serializable { |
|||
|
|||
private String code; |
|||
|
|||
private String msg; |
|||
|
|||
private PageData<T> data; |
|||
|
|||
/** |
|||
* 构建分页结果(MyBatis-Plus {@link IPage})。 |
|||
* |
|||
* <p>data 为当前页记录列表;page 提供分页元信息。</p> |
|||
*/ |
|||
public static <T> PageResult<T> success(IPage<T> page) { |
|||
PageResult<T> result = new PageResult<>(); |
|||
result.setCode(ResultCode.SUCCESS.getCode()); |
|||
result.setMsg(ResultCode.SUCCESS.getMsg()); |
|||
|
|||
List<T> records = |
|||
(page == null || page.getRecords() == null) |
|||
? Collections.emptyList() |
|||
: page.getRecords(); |
|||
PageData<T> pageData = new PageData<>(); |
|||
pageData.setList(records); |
|||
pageData.setTotal(page != null ? page.getTotal() : 0L); |
|||
result.setData(pageData); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
/** |
|||
* 构建列表结果(无分页)。 |
|||
* |
|||
* <p>page 置为 null,用于与分页返回区分。</p> |
|||
*/ |
|||
public static <T> PageResult<T> success(List<T> list) { |
|||
PageResult<T> result = new PageResult<>(); |
|||
result.setCode(ResultCode.SUCCESS.getCode()); |
|||
result.setMsg(ResultCode.SUCCESS.getMsg()); |
|||
PageData<T> pageData = new PageData<>(); |
|||
pageData.setList(list != null ? list : Collections.emptyList()); |
|||
pageData.setTotal(0L); |
|||
result.setData(pageData); |
|||
return result; |
|||
} |
|||
|
|||
@Data |
|||
public static class PageData<T> { |
|||
|
|||
private List<T> list; |
|||
|
|||
private long total; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,122 @@ |
|||
package com.youlai.boot.common.result; |
|||
|
|||
import cn.hutool.extra.servlet.JakartaServletUtil; |
|||
import cn.hutool.json.JSONUtil; |
|||
import jakarta.servlet.http.HttpServletResponse; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.http.HttpStatus; |
|||
import org.springframework.http.MediaType; |
|||
|
|||
import java.nio.charset.StandardCharsets; |
|||
|
|||
/** |
|||
* 响应写入器 |
|||
* <p> |
|||
* 用于在过滤器、Security处理器等无法使用 @RestControllerAdvice 的场景中统一写入HTTP响应。 |
|||
* 支持写入成功响应和错误响应。 |
|||
* 此类为工具类,所有方法均为静态方法,禁止实例化。 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2.0.0 |
|||
*/ |
|||
@Slf4j |
|||
public final class ResponseWriter { |
|||
|
|||
/** |
|||
* 私有构造函数,防止实例化 |
|||
*/ |
|||
private ResponseWriter() { |
|||
throw new UnsupportedOperationException("工具类不允许实例化"); |
|||
} |
|||
|
|||
/** |
|||
* 写入成功响应 |
|||
* |
|||
* @param response HttpServletResponse |
|||
* @param data 响应数据(可选) |
|||
*/ |
|||
public static void writeSuccess(HttpServletResponse response, Object data) { |
|||
writeResult(response, Result.success(data), HttpStatus.OK.value()); |
|||
} |
|||
|
|||
/** |
|||
* 写入成功响应(无数据) |
|||
* |
|||
* @param response HttpServletResponse |
|||
*/ |
|||
public static void writeSuccess(HttpServletResponse response) { |
|||
writeSuccess(response, null); |
|||
} |
|||
|
|||
/** |
|||
* 写入错误响应 |
|||
* |
|||
* @param response HttpServletResponse |
|||
* @param resultCode 响应结果码 |
|||
*/ |
|||
public static void writeError(HttpServletResponse response, ResultCode resultCode) { |
|||
writeError(response, resultCode, null); |
|||
} |
|||
|
|||
/** |
|||
* 写入错误响应(带自定义消息) |
|||
* |
|||
* @param response HttpServletResponse |
|||
* @param resultCode 响应结果码 |
|||
* @param message 自定义消息(可选,为 null 时使用 resultCode 的默认消息) |
|||
*/ |
|||
public static void writeError(HttpServletResponse response, ResultCode resultCode, String message) { |
|||
Result<?> result = message == null |
|||
? Result.failed(resultCode) |
|||
: Result.failed(resultCode, message); |
|||
|
|||
int httpStatus = mapHttpStatus(resultCode); |
|||
writeResult(response, result, httpStatus); |
|||
} |
|||
|
|||
/** |
|||
* 写入响应结果(通用方法) |
|||
* |
|||
* @param response HttpServletResponse |
|||
* @param result 响应结果对象 |
|||
* @param httpStatus HTTP状态码 |
|||
*/ |
|||
private static void writeResult(HttpServletResponse response, Result<?> result, int httpStatus) { |
|||
try { |
|||
// 设置HTTP状态码
|
|||
response.setStatus(httpStatus); |
|||
|
|||
// 设置响应编码和内容类型
|
|||
response.setCharacterEncoding(StandardCharsets.UTF_8.toString()); |
|||
response.setContentType(MediaType.APPLICATION_JSON_VALUE); |
|||
|
|||
// 写入响应
|
|||
JakartaServletUtil.write(response, |
|||
JSONUtil.toJsonStr(result), |
|||
MediaType.APPLICATION_JSON_VALUE |
|||
); |
|||
|
|||
} catch (Exception e) { |
|||
log.error("写入响应时发生未知异常: httpStatus={}, result={}", httpStatus, result, e); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 根据业务结果码映射HTTP状态码 |
|||
* 401: 未认证(token无效/过期) |
|||
* 403: 权限不足 |
|||
* 400: 其他业务错误 |
|||
* |
|||
* @param resultCode 业务结果码 |
|||
* @return HTTP状态码 |
|||
*/ |
|||
private static int mapHttpStatus(ResultCode resultCode) { |
|||
return switch (resultCode) { |
|||
case ACCESS_UNAUTHORIZED, |
|||
ACCESS_TOKEN_INVALID, |
|||
REFRESH_TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value(); |
|||
case ACCESS_PERMISSION_EXCEPTION -> HttpStatus.FORBIDDEN.value(); |
|||
default -> HttpStatus.BAD_REQUEST.value(); |
|||
}; |
|||
} |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
package com.youlai.boot.common.result; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import lombok.Data; |
|||
|
|||
import java.io.Serializable; |
|||
|
|||
/** |
|||
* 统一响应结构体 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2022/1/30 |
|||
**/ |
|||
@Data |
|||
public class Result<T> implements Serializable { |
|||
|
|||
private String code; |
|||
|
|||
private T data; |
|||
|
|||
private String msg; |
|||
|
|||
public static <T> Result<T> success() { |
|||
return success(null); |
|||
} |
|||
|
|||
public static <T> Result<T> success(T data) { |
|||
Result<T> result = new Result<>(); |
|||
result.setCode(ResultCode.SUCCESS.getCode()); |
|||
result.setMsg(ResultCode.SUCCESS.getMsg()); |
|||
result.setData(data); |
|||
return result; |
|||
} |
|||
|
|||
public static <T> Result<T> failed() { |
|||
return result(ResultCode.SYSTEM_ERROR.getCode(), ResultCode.SYSTEM_ERROR.getMsg(), null); |
|||
} |
|||
|
|||
public static <T> Result<T> failed(String msg) { |
|||
return result(ResultCode.SYSTEM_ERROR.getCode(), msg, null); |
|||
} |
|||
|
|||
public static <T> Result<T> judge(boolean status) { |
|||
if (status) { |
|||
return success(); |
|||
} else { |
|||
return failed(); |
|||
} |
|||
} |
|||
|
|||
public static <T> Result<T> failed(IResultCode resultCode) { |
|||
return result(resultCode.getCode(), resultCode.getMsg(), null); |
|||
} |
|||
|
|||
public static <T> Result<T> failed(IResultCode resultCode, String msg) { |
|||
return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), null); |
|||
} |
|||
|
|||
public static <T> Result<T> failed(IResultCode resultCode, T data) { |
|||
return result(resultCode.getCode(), resultCode.getMsg(), data); |
|||
} |
|||
|
|||
public static <T> Result<T> failed(IResultCode resultCode, String msg, T data) { |
|||
return result(resultCode.getCode(), StrUtil.isNotBlank(msg) ? msg : resultCode.getMsg(), data); |
|||
} |
|||
|
|||
private static <T> Result<T> result(IResultCode resultCode, T data) { |
|||
return result(resultCode.getCode(), resultCode.getMsg(), data); |
|||
} |
|||
|
|||
private static <T> Result<T> result(String code, String msg, T data) { |
|||
Result<T> result = new Result<>(); |
|||
result.setCode(code); |
|||
result.setData(data); |
|||
result.setMsg(msg); |
|||
return result; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,155 @@ |
|||
package com.youlai.boot.common.result; |
|||
|
|||
import java.io.Serializable; |
|||
|
|||
/** |
|||
* 响应码枚举 |
|||
* <p> |
|||
* 参考《阿里巴巴 Java 开发手册》错误码设计建议: |
|||
* 00000 表示成功。 |
|||
* A**** 表示用户端错误(如参数错误、认证失败等)。 |
|||
* B**** 表示当前系统执行出错(如系统超时等)。 |
|||
* C**** 表示调用第三方服务出错(如中间件、数据库等外部依赖)。 |
|||
* <p> |
|||
* 错误码位数与号段说明: |
|||
* - 错误码为字符串类型,共 5 位:错误产生来源(A/B/C) + 四位数字编号。 |
|||
* - 四位数字编号范围 0001~9999,大类之间建议按步长 100 预留号段(如 A0200、A0300、A0400)。 |
|||
* - 错误码后三位编号与 HTTP 状态码无关。 |
|||
* <p> |
|||
* 说明: |
|||
* - 本项目仅保留实际使用的错误码,并在 A/B/C 各保留少量示例,避免枚举无限膨胀。 |
|||
* - 如需扩展业务错误码,建议在对应宏观分类下按场景划分号段并保持全局唯一。 |
|||
* <p> |
|||
* 附表(节选):错误码列表(示例/项目使用项) |
|||
* <pre> |
|||
* | 错误码 | 中文描述 | 说明 | |
|||
* |-------|----------------------|------------------| |
|||
* | 00000 | 成功 | 正常执行后的返回 | |
|||
* | A0001 | 用户端错误 | 一级宏观错误码 | |
|||
* | A0100 | 用户注册错误 | 二级宏观错误码 | |
|||
* | A0101 | 用户未同意隐私协议 | 二级宏观错误码 | |
|||
* | A0200 | 用户登录异常 | 二级宏观错误码 | |
|||
* | A0201 | 用户账户不存在 | 二级宏观错误码 | |
|||
* | A0202 | 用户账户被冻结 | 二级宏观错误码 | |
|||
* | A0230 | 访问令牌无效或已过期 | 令牌校验失败 | |
|||
* | A0241 | 用户验证码尝试次数超限 | 二级宏观错误码 | |
|||
* | A0300 | 访问权限异常 | 二级宏观错误码 | |
|||
* | A0301 | 访问未授权 | 二级宏观错误码 | |
|||
* | A0400 | 用户请求参数错误 | 二级宏观错误码 | |
|||
* | A0410 | 请求必填参数为空 | 二级宏观错误码 | |
|||
* | A0500 | 用户请求服务异常 | 二级宏观错误码 | |
|||
* | A0502 | 请求并发数超出限制 | 二级宏观错误码 | |
|||
* | A0506 | 请勿重复提交 | 二级宏观错误码 | |
|||
* | B0001 | 系统执行出错 | 一级宏观错误码 | |
|||
* | B0100 | 系统执行超时 | 二级宏观错误码 | |
|||
* | C0001 | 调用第三方服务出错 | 一级宏观错误码 | |
|||
* | C0113 | 接口不存在 | 二级宏观错误码 | |
|||
* | C0300 | 数据库服务出错 | 二级宏观错误码 | |
|||
* </pre> |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2020/6/23 |
|||
**/ |
|||
public enum ResultCode implements IResultCode, Serializable { |
|||
|
|||
SUCCESS("00000", "成功"), |
|||
|
|||
/** 一级宏观错误码:用户端错误(由客户端输入/认证/权限/请求方式等引起,需客户端配合修正) */ |
|||
USER_ERROR("A0001", "用户端错误"), |
|||
|
|||
|
|||
/** 二级宏观错误码:用户端具体错误(按号段细分,便于定位是注册/登录/令牌/参数/防重等问题) */ |
|||
|
|||
/** A01xx:用户注册错误 */ |
|||
USER_REGISTRATION_ERROR("A0100", "用户注册错误"), |
|||
USER_NOT_AGREE_PRIVACY_AGREEMENT("A0101", "用户未同意隐私协议"), |
|||
|
|||
/** A013x:校验码输入错误 */ |
|||
VERIFICATION_CODE_INPUT_ERROR("A0130", "校验码输入错误"), |
|||
|
|||
/** A02xx:用户登录异常 */ |
|||
USER_LOGIN_EXCEPTION("A0200", "用户登录异常"), |
|||
ACCOUNT_NOT_FOUND("A0201", "用户账户不存在"), |
|||
ACCOUNT_FROZEN("A0202", "用户账户被冻结"), |
|||
USER_PASSWORD_ERROR("A0210", "用户名或密码错误"), |
|||
/** A023x:令牌无效或已过期 */ |
|||
ACCESS_TOKEN_INVALID("A0230", "访问令牌无效或已过期"), |
|||
REFRESH_TOKEN_INVALID("A0231", "刷新令牌无效或已过期"), |
|||
/** A024x:验证码错误 */ |
|||
USER_VERIFICATION_CODE_ERROR("A0240", "验证码错误"), |
|||
USER_VERIFICATION_CODE_ATTEMPT_LIMIT_EXCEEDED("A0241", "用户验证码尝试次数超限"), |
|||
USER_VERIFICATION_CODE_EXPIRED("A0242", "用户验证码过期"), |
|||
|
|||
/** A03xx:访问权限异常 */ |
|||
ACCESS_PERMISSION_EXCEPTION("A0300", "访问权限异常"), |
|||
ACCESS_UNAUTHORIZED("A0301", "访问未授权"), |
|||
|
|||
/** A04xx:用户请求参数错误 */ |
|||
USER_REQUEST_PARAMETER_ERROR("A0400", "用户请求参数错误"), |
|||
INVALID_USER_INPUT("A0402", "无效的用户输入"), |
|||
REQUEST_REQUIRED_PARAMETER_IS_EMPTY("A0410", "请求必填参数为空"), |
|||
PARAMETER_FORMAT_MISMATCH("A0421", "参数格式不匹配"), |
|||
|
|||
/** A05xx:用户请求服务异常 */ |
|||
USER_REQUEST_SERVICE_EXCEPTION("A0500", "用户请求服务异常"), |
|||
REQUEST_CONCURRENCY_LIMIT_EXCEEDED("A0502", "请求并发数超出限制"), |
|||
DUPLICATE_SUBMISSION("A0506", "请勿重复提交"), |
|||
|
|||
/** A07xx:文件处理异常 */ |
|||
UPLOAD_FILE_EXCEPTION("A0700", "上传文件异常"), |
|||
DELETE_FILE_EXCEPTION("A0710", "删除文件异常"), |
|||
|
|||
/** 一级宏观错误码:系统端错误(服务端内部异常/超时/不可用等,需后端排查修复) */ |
|||
SYSTEM_ERROR("B0001", "系统执行出错"), |
|||
|
|||
/** 二级宏观错误码:系统端具体错误(按号段细分,便于定位超时/限流/资源耗尽等) */ |
|||
SYSTEM_EXECUTION_TIMEOUT("B0100", "系统执行超时"), |
|||
|
|||
/** 一级宏观错误码:第三方服务错误(外部依赖/中间件/数据库等引起,需检查依赖健康与配置) */ |
|||
THIRD_PARTY_SERVICE_ERROR("C0001", "调用第三方服务出错"), |
|||
|
|||
/** 二级宏观错误码:第三方服务具体错误(按号段细分,便于定位是接口不存在/数据库异常等) */ |
|||
INTERFACE_NOT_EXIST("C0113", "接口不存在"), |
|||
DATABASE_SERVICE_ERROR("C0300", "数据库服务出错"), |
|||
DATABASE_EXECUTION_SYNTAX_ERROR("C0313", "数据库执行语法错误"), |
|||
INTEGRITY_CONSTRAINT_VIOLATION("C0342", "违反了完整性约束"), |
|||
DATABASE_ACCESS_DENIED("C0351", "演示环境已禁用数据库写入功能,请本地部署修改数据库链接或开启Mock模式进行体验"); |
|||
|
|||
private final String code; |
|||
|
|||
private final String msg; |
|||
|
|||
ResultCode(String code, String msg) { |
|||
this.code = code; |
|||
this.msg = msg; |
|||
} |
|||
|
|||
|
|||
@Override |
|||
public String getCode() { |
|||
return code; |
|||
} |
|||
|
|||
@Override |
|||
public String getMsg() { |
|||
return msg; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return "{" + |
|||
"\"code\":\"" + code + '\"' + |
|||
", \"msg\":\"" + msg + '\"' + |
|||
'}'; |
|||
} |
|||
|
|||
|
|||
public static ResultCode getValue(String code) { |
|||
for (ResultCode value : values()) { |
|||
if (value.getCode().equals(code)) { |
|||
return value; |
|||
} |
|||
} |
|||
return SYSTEM_ERROR; // 默认系统执行错误
|
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
package com.youlai.boot.common.util; |
|||
|
|||
import cn.idev.excel.EasyExcel; |
|||
import cn.idev.excel.event.AnalysisEventListener; |
|||
|
|||
import java.io.InputStream; |
|||
|
|||
/** |
|||
* Excel 工具类 |
|||
* |
|||
* @author haoxr |
|||
* @since 2023/03/01 |
|||
*/ |
|||
public class ExcelUtils { |
|||
|
|||
public static <T> void importExcel(InputStream is, Class clazz, AnalysisEventListener<T> listener) { |
|||
EasyExcel.read(is, clazz, listener).sheet().doRead(); |
|||
} |
|||
} |
|||
@ -0,0 +1,139 @@ |
|||
package com.youlai.boot.common.util; |
|||
|
|||
import cn.hutool.core.util.StrUtil; |
|||
import jakarta.annotation.PostConstruct; |
|||
import jakarta.servlet.http.HttpServletRequest; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.lionsoul.ip2region.xdb.Searcher; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import java.io.FileNotFoundException; |
|||
import java.io.InputStream; |
|||
import java.net.InetAddress; |
|||
import java.net.UnknownHostException; |
|||
import java.nio.file.Files; |
|||
import java.nio.file.Path; |
|||
import java.nio.file.StandardCopyOption; |
|||
|
|||
/** |
|||
* IP工具类 |
|||
* <p> |
|||
* 获取客户端IP地址和IP地址对应的地理位置信息 |
|||
* <p> |
|||
* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址 |
|||
* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址 |
|||
* </p> |
|||
* |
|||
* @author Ray |
|||
* @since 2.10.0 |
|||
*/ |
|||
@Slf4j |
|||
@Component |
|||
public class IPUtils { |
|||
|
|||
private static final String DB_PATH = "/data/ip2region.xdb"; |
|||
private static Searcher searcher; |
|||
|
|||
@PostConstruct |
|||
public void init() { |
|||
try { |
|||
// 从类路径加载资源文件
|
|||
InputStream inputStream = getClass().getResourceAsStream(DB_PATH); |
|||
if (inputStream == null) { |
|||
throw new FileNotFoundException("Resource not found: " + DB_PATH); |
|||
} |
|||
|
|||
// 将资源文件复制到临时文件
|
|||
Path tempDbPath = Files.createTempFile("ip2region", ".xdb"); |
|||
Files.copy(inputStream, tempDbPath, StandardCopyOption.REPLACE_EXISTING); |
|||
|
|||
// 使用临时文件初始化 Searcher 对象
|
|||
searcher = Searcher.newWithFileOnly(tempDbPath.toString()); |
|||
} catch (Exception e) { |
|||
log.error("IpRegionUtil initialization ERROR, {}", e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取IP地址 |
|||
* |
|||
* @param request HttpServletRequest对象 |
|||
* @return 客户端IP地址 |
|||
*/ |
|||
public static String getIpAddr(HttpServletRequest request) { |
|||
String ip = null; |
|||
try { |
|||
if (request == null) { |
|||
return ""; |
|||
} |
|||
ip = request.getHeader("x-forwarded-for"); |
|||
if (checkIp(ip)) { |
|||
ip = request.getHeader("Proxy-Client-IP"); |
|||
} |
|||
if (checkIp(ip)) { |
|||
ip = request.getHeader("WL-Proxy-Client-IP"); |
|||
} |
|||
if (checkIp(ip)) { |
|||
ip = request.getHeader("HTTP_CLIENT_IP"); |
|||
} |
|||
if (checkIp(ip)) { |
|||
ip = request.getHeader("HTTP_X_FORWARDED_FOR"); |
|||
} |
|||
if (checkIp(ip)) { |
|||
ip = request.getRemoteAddr(); |
|||
if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { |
|||
// 根据网卡取本机配置的IP
|
|||
ip = getLocalAddr(); |
|||
} |
|||
} |
|||
} catch (Exception e) { |
|||
log.error("IPUtils ERROR, {}", e.getMessage()); |
|||
} |
|||
|
|||
// 使用代理,则获取第一个IP地址
|
|||
if (StrUtil.isNotBlank(ip) && ip.indexOf(",") > 0) { |
|||
ip = ip.substring(0, ip.indexOf(",")); |
|||
} |
|||
|
|||
return ip; |
|||
} |
|||
|
|||
private static boolean checkIp(String ip) { |
|||
String unknown = "unknown"; |
|||
return StrUtil.isEmpty(ip) || unknown.equalsIgnoreCase(ip); |
|||
} |
|||
|
|||
/** |
|||
* 获取本机的IP地址 |
|||
* |
|||
* @return 本机IP地址 |
|||
*/ |
|||
private static String getLocalAddr() { |
|||
try { |
|||
return InetAddress.getLocalHost().getHostAddress(); |
|||
} catch (UnknownHostException e) { |
|||
log.error("InetAddress.getLocalHost()-error, {}", e.getMessage()); |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
/** |
|||
* 根据IP地址获取地理位置信息 |
|||
* |
|||
* @param ip IP地址 |
|||
* @return 地理位置信息 |
|||
*/ |
|||
public static String getRegion(String ip) { |
|||
if (searcher == null) { |
|||
log.error("Searcher is not initialized"); |
|||
return null; |
|||
} |
|||
|
|||
try { |
|||
return searcher.search(ip); |
|||
} catch (Exception e) { |
|||
log.error("IpRegionUtil ERROR, {}", e.getMessage()); |
|||
return null; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
package com.youlai.boot.common.validator; |
|||
|
|||
import com.youlai.boot.common.annotation.ValidField; |
|||
import jakarta.validation.ConstraintValidator; |
|||
import jakarta.validation.ConstraintValidatorContext; |
|||
|
|||
import java.util.Arrays; |
|||
|
|||
/** |
|||
* 字段校验器 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2024/11/18 |
|||
*/ |
|||
public class FieldValidator implements ConstraintValidator<ValidField, String> { |
|||
|
|||
private String[] allowedValues; |
|||
|
|||
@Override |
|||
public void initialize(ValidField constraintAnnotation) { |
|||
this.allowedValues = constraintAnnotation.allowedValues(); |
|||
} |
|||
|
|||
@Override |
|||
public boolean isValid(String value, ConstraintValidatorContext context) { |
|||
if (value == null) { |
|||
return true; |
|||
} |
|||
return Arrays.asList(allowedValues).contains(value); |
|||
} |
|||
} |
|||
@ -0,0 +1,55 @@ |
|||
package com.youlai.boot.file.controller; |
|||
|
|||
import com.youlai.boot.common.result.Result; |
|||
import com.youlai.boot.file.service.FileService; |
|||
import com.youlai.boot.file.model.FileInfo; |
|||
import io.swagger.v3.oas.annotations.Parameter; |
|||
import io.swagger.v3.oas.annotations.enums.ParameterIn; |
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import io.swagger.v3.oas.annotations.Operation; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.SneakyThrows; |
|||
import org.springframework.web.bind.annotation.*; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
/** |
|||
* 文件控制层 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2022/10/16 |
|||
*/ |
|||
@Tag(name = "10.文件接口") |
|||
@RestController |
|||
@RequestMapping("/api/v1/files") |
|||
@RequiredArgsConstructor |
|||
public class FileController { |
|||
|
|||
private final FileService fileService; |
|||
|
|||
@PostMapping |
|||
@Operation(summary = "文件上传") |
|||
public Result<FileInfo> uploadFile( |
|||
@Parameter( |
|||
name = "file", |
|||
description = "表单文件对象", |
|||
required = true, |
|||
in = ParameterIn.DEFAULT, |
|||
schema = @Schema(name = "file", format = "binary") |
|||
) |
|||
@RequestPart(value = "file") MultipartFile file |
|||
) { |
|||
FileInfo fileInfo = fileService.uploadFile(file); |
|||
return Result.success(fileInfo); |
|||
} |
|||
|
|||
@DeleteMapping |
|||
@Operation(summary = "文件删除") |
|||
@SneakyThrows |
|||
public Result<?> deleteFile( |
|||
@Parameter(description = "文件路径") @RequestParam String filePath |
|||
) { |
|||
boolean result = fileService.deleteFile(filePath); |
|||
return Result.judge(result); |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
package com.youlai.boot.file.model; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.Data; |
|||
|
|||
|
|||
/** |
|||
* 文件信息对象 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 1.0.0 |
|||
*/ |
|||
@Schema(description = "文件对象") |
|||
@Data |
|||
public class FileInfo { |
|||
|
|||
@Schema(description = "文件名称") |
|||
private String name; |
|||
|
|||
@Schema(description = "文件URL") |
|||
private String url; |
|||
|
|||
} |
|||
@ -0,0 +1,30 @@ |
|||
package com.youlai.boot.file.service; |
|||
|
|||
import com.youlai.boot.file.model.FileInfo; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
/** |
|||
* 对象存储服务接口层 |
|||
* |
|||
* @author haoxr |
|||
* @since 2022/11/19 |
|||
*/ |
|||
public interface FileService { |
|||
|
|||
/** |
|||
* 上传文件 |
|||
* @param file 表单文件对象 |
|||
* @return 文件信息 |
|||
*/ |
|||
FileInfo uploadFile(MultipartFile file); |
|||
|
|||
/** |
|||
* 删除文件 |
|||
* |
|||
* @param filePath 文件完整URL |
|||
* @return 删除结果 |
|||
*/ |
|||
boolean deleteFile(String filePath); |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,100 @@ |
|||
package com.youlai.boot.file.service.impl; |
|||
|
|||
import cn.hutool.core.date.DateUtil; |
|||
import cn.hutool.core.io.FileUtil; |
|||
import cn.hutool.core.lang.Assert; |
|||
import cn.hutool.core.util.IdUtil; |
|||
import com.aliyun.oss.OSS; |
|||
import com.aliyun.oss.OSSClientBuilder; |
|||
import com.aliyun.oss.model.ObjectMetadata; |
|||
import com.aliyun.oss.model.PutObjectRequest; |
|||
import com.youlai.boot.file.service.FileService; |
|||
import com.youlai.boot.file.model.FileInfo; |
|||
import jakarta.annotation.PostConstruct; |
|||
import lombok.Data; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.SneakyThrows; |
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
|||
import org.springframework.boot.context.properties.ConfigurationProperties; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import java.io.InputStream; |
|||
import java.time.LocalDateTime; |
|||
|
|||
/** |
|||
* Aliyun 对象存储服务类 |
|||
* |
|||
* @author haoxr |
|||
* @since 2.3.0 |
|||
*/ |
|||
@Component |
|||
@ConditionalOnProperty(value = "oss.type", havingValue = "aliyun") |
|||
@ConfigurationProperties(prefix = "oss.aliyun") |
|||
@RequiredArgsConstructor |
|||
@Data |
|||
public class AliyunFileService implements FileService { |
|||
/** |
|||
* 服务Endpoint |
|||
*/ |
|||
private String endpoint; |
|||
/** |
|||
* 访问凭据 |
|||
*/ |
|||
private String accessKeyId; |
|||
/** |
|||
* 凭据密钥 |
|||
*/ |
|||
private String accessKeySecret; |
|||
/** |
|||
* 存储桶名称 |
|||
*/ |
|||
private String bucketName; |
|||
|
|||
private OSS aliyunOssClient; |
|||
|
|||
@PostConstruct |
|||
public void init() { |
|||
aliyunOssClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); |
|||
} |
|||
|
|||
@Override |
|||
@SneakyThrows |
|||
public FileInfo uploadFile(MultipartFile file) { |
|||
|
|||
// 获取文件名称
|
|||
String originalFilename = file.getOriginalFilename(); |
|||
// 生成文件名(日期文件夹)
|
|||
String suffix = FileUtil.getSuffix(originalFilename); |
|||
String uuid = IdUtil.simpleUUID(); |
|||
String fileName = DateUtil.format(LocalDateTime.now(), "yyyyMMdd") + "/" + uuid + "." + suffix; |
|||
// try-with-resource 语法糖自动释放流
|
|||
try (InputStream inputStream = file.getInputStream()) { |
|||
|
|||
// 设置上传文件的元信息,例如Content-Type
|
|||
ObjectMetadata metadata = new ObjectMetadata(); |
|||
metadata.setContentType(file.getContentType()); |
|||
// 创建PutObjectRequest对象,指定Bucket名称、对象名称和输入流
|
|||
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, fileName, inputStream, metadata); |
|||
// 上传文件
|
|||
aliyunOssClient.putObject(putObjectRequest); |
|||
} catch (Exception e) { |
|||
throw new RuntimeException("文件上传失败"); |
|||
} |
|||
// 获取文件访问路径
|
|||
String fileUrl = "https://" + bucketName + "." + endpoint + "/" + fileName; |
|||
FileInfo fileInfo = new FileInfo(); |
|||
fileInfo.setName(originalFilename); |
|||
fileInfo.setUrl(fileUrl); |
|||
return fileInfo; |
|||
} |
|||
|
|||
@Override |
|||
public boolean deleteFile(String filePath) { |
|||
Assert.notBlank(filePath, "删除文件路径不能为空"); |
|||
String fileHost = "https://" + bucketName + "." + endpoint; // 文件主机域名
|
|||
String fileName = filePath.substring(fileHost.length() + 1); // +1 是/占一个字符,截断左闭右开
|
|||
aliyunOssClient.deleteObject(bucketName, fileName); |
|||
return true; |
|||
} |
|||
} |
|||
@ -0,0 +1,92 @@ |
|||
package com.youlai.boot.file.service.impl; |
|||
|
|||
import cn.hutool.core.date.DatePattern; |
|||
import cn.hutool.core.date.DateUtil; |
|||
import cn.hutool.core.io.FileUtil; |
|||
import cn.hutool.core.util.IdUtil; |
|||
import com.youlai.boot.file.model.FileInfo; |
|||
import com.youlai.boot.file.service.FileService; |
|||
import lombok.Data; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
|||
import org.springframework.boot.context.properties.ConfigurationProperties; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import java.io.File; |
|||
import java.io.InputStream; |
|||
import java.time.LocalDateTime; |
|||
|
|||
/** |
|||
* 本地存储服务类 |
|||
* |
|||
* @author Theo |
|||
* @since 2024-12-09 17:11 |
|||
*/ |
|||
@Data |
|||
@Slf4j |
|||
@Component |
|||
@ConditionalOnProperty(value = "oss.type", havingValue = "local") |
|||
@ConfigurationProperties(prefix = "oss.local") |
|||
@RequiredArgsConstructor |
|||
public class LocalFileService implements FileService { |
|||
|
|||
@Value("${oss.local.storage-path}") |
|||
private String storagePath; |
|||
|
|||
/** |
|||
* 上传文件方法 |
|||
* |
|||
* @param file 表单文件对象 |
|||
* @return 文件信息 |
|||
*/ |
|||
@Override |
|||
public FileInfo uploadFile(MultipartFile file) { |
|||
// 获取文件名
|
|||
String originalFilename = file.getOriginalFilename(); |
|||
// 获取文件后缀
|
|||
String suffix = FileUtil.getSuffix(originalFilename); |
|||
// 生成uuid
|
|||
String fileName = IdUtil.simpleUUID()+ "." + suffix;; |
|||
// 生成文件名(日期文件夹)
|
|||
String folder = DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN); |
|||
String filePrefix = storagePath.endsWith(File.separator) ? storagePath : storagePath + File.separator; |
|||
// try-with-resource 语法糖自动释放流
|
|||
try (InputStream inputStream = file.getInputStream()) { |
|||
// 上传文件
|
|||
FileUtil.writeFromStream(inputStream, filePrefix + folder + File.separator + fileName); |
|||
} catch (Exception e) { |
|||
log.error("文件上传失败", e); |
|||
throw new RuntimeException("文件上传失败"); |
|||
} |
|||
// 获取文件访问路径,因为这里是本地存储,所以直接返回文件的相对路径,需要前端自行处理访问前缀
|
|||
String fileUrl = File.separator + folder + File.separator + fileName; |
|||
FileInfo fileInfo = new FileInfo(); |
|||
fileInfo.setName(originalFilename); |
|||
fileInfo.setUrl(fileUrl); |
|||
return fileInfo; |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 删除文件 |
|||
* @param filePath 文件完整URL |
|||
* @return 是否删除成功 |
|||
*/ |
|||
@Override |
|||
public boolean deleteFile(String filePath) { |
|||
//判断文件是否为空
|
|||
if (filePath == null || filePath.isEmpty()) { |
|||
return false; |
|||
} |
|||
// 判断filepath是否为文件夹
|
|||
if (FileUtil.isDirectory(storagePath + filePath)) { |
|||
// 禁止删除文件夹
|
|||
return false; |
|||
} |
|||
// 删除文件
|
|||
return FileUtil.del(storagePath + filePath); |
|||
} |
|||
} |
|||
@ -0,0 +1,210 @@ |
|||
package com.youlai.boot.file.service.impl; |
|||
|
|||
import cn.hutool.core.date.DateUtil; |
|||
import cn.hutool.core.io.FileUtil; |
|||
import cn.hutool.core.lang.Assert; |
|||
import cn.hutool.core.util.IdUtil; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.youlai.boot.common.exception.BusinessException; |
|||
import com.youlai.boot.common.result.ResultCode; |
|||
import com.youlai.boot.file.model.FileInfo; |
|||
import com.youlai.boot.file.service.FileService; |
|||
import io.minio.*; |
|||
import io.minio.http.Method; |
|||
import jakarta.annotation.PostConstruct; |
|||
import lombok.Data; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.SneakyThrows; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
|||
import org.springframework.boot.context.properties.ConfigurationProperties; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.web.multipart.MultipartFile; |
|||
|
|||
import java.io.InputStream; |
|||
import java.time.LocalDateTime; |
|||
|
|||
/** |
|||
* MinIO 文件上传服务类 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2023/6/2 |
|||
*/ |
|||
@Component |
|||
@ConditionalOnProperty(value = "oss.type", havingValue = "minio") |
|||
@ConfigurationProperties(prefix = "oss.minio") |
|||
@RequiredArgsConstructor |
|||
@Data |
|||
@Slf4j |
|||
public class MinioFileService implements FileService { |
|||
|
|||
/** |
|||
* 服务Endpoint |
|||
*/ |
|||
private String endpoint; |
|||
/** |
|||
* 访问凭据 |
|||
*/ |
|||
private String accessKey; |
|||
/** |
|||
* 凭据密钥 |
|||
*/ |
|||
private String secretKey; |
|||
/** |
|||
* 存储桶名称 |
|||
*/ |
|||
private String bucketName; |
|||
/** |
|||
* 自定义域名 |
|||
*/ |
|||
private String customDomain; |
|||
|
|||
private MinioClient minioClient; |
|||
|
|||
// 依赖注入完成之后执行初始化
|
|||
@PostConstruct |
|||
public void init() { |
|||
minioClient = MinioClient.builder() |
|||
.endpoint(endpoint) |
|||
.credentials(accessKey, secretKey) |
|||
.build(); |
|||
// 创建存储桶(存储桶不存在)
|
|||
// createBucketIfAbsent(bucketName);
|
|||
} |
|||
|
|||
|
|||
/** |
|||
* 上传文件 |
|||
* |
|||
* @param file 表单文件对象 |
|||
* @return 文件信息 |
|||
*/ |
|||
@Override |
|||
public FileInfo uploadFile(MultipartFile file) { |
|||
|
|||
// 创建存储桶(存储桶不存在),如果有搭建好的minio服务,建议放在init方法中
|
|||
createBucketIfAbsent(bucketName); |
|||
|
|||
// 文件原生名称
|
|||
String originalFilename = file.getOriginalFilename(); |
|||
// 文件后缀
|
|||
String suffix = FileUtil.getSuffix(originalFilename); |
|||
// 文件夹名称
|
|||
String dateFolder = DateUtil.format(LocalDateTime.now(), "yyyyMMdd"); |
|||
// 文件名称
|
|||
String fileName = IdUtil.simpleUUID() + "." + suffix; |
|||
|
|||
// try-with-resource 语法糖自动释放流
|
|||
try (InputStream inputStream = file.getInputStream()) { |
|||
// 文件上传
|
|||
PutObjectArgs putObjectArgs = PutObjectArgs.builder() |
|||
.bucket(bucketName) |
|||
.object(dateFolder + "/"+ fileName) |
|||
.contentType(file.getContentType()) |
|||
.stream(inputStream, inputStream.available(), -1) |
|||
.build(); |
|||
minioClient.putObject(putObjectArgs); |
|||
|
|||
// 返回文件路径
|
|||
String fileUrl; |
|||
// 未配置自定义域名
|
|||
if (StrUtil.isBlank(customDomain)) { |
|||
// 获取文件URL
|
|||
GetPresignedObjectUrlArgs getPresignedObjectUrlArgs = GetPresignedObjectUrlArgs.builder() |
|||
.bucket(bucketName) |
|||
.object(dateFolder + "/"+ fileName) |
|||
.method(Method.GET) |
|||
.build(); |
|||
|
|||
fileUrl = minioClient.getPresignedObjectUrl(getPresignedObjectUrlArgs); |
|||
fileUrl = fileUrl.substring(0, fileUrl.indexOf("?")); |
|||
} else { |
|||
// 配置自定义文件路径域名
|
|||
fileUrl = customDomain + "/"+ bucketName + "/"+ dateFolder + "/"+ fileName; |
|||
} |
|||
|
|||
FileInfo fileInfo = new FileInfo(); |
|||
fileInfo.setName(originalFilename); |
|||
fileInfo.setUrl(fileUrl); |
|||
return fileInfo; |
|||
} catch (Exception e) { |
|||
log.error("上传文件失败", e); |
|||
throw new BusinessException(ResultCode.UPLOAD_FILE_EXCEPTION, e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 删除文件 |
|||
* |
|||
* @param filePath 文件完整路径 |
|||
* @return 是否删除成功 |
|||
*/ |
|||
@Override |
|||
public boolean deleteFile(String filePath) { |
|||
Assert.notBlank(filePath, "删除文件路径不能为空"); |
|||
try { |
|||
String fileName; |
|||
if (StrUtil.isNotBlank(customDomain)) { |
|||
// https://oss.youlai.tech/default/20221120/test.jpg → 20221120/websocket.jpg
|
|||
fileName = filePath.substring(customDomain.length() + 1 + bucketName.length() + 1); // 两个/占了2个字符长度
|
|||
} else { |
|||
// http://localhost:9000/default/20221120/test.jpg → 20221120/websocket.jpg
|
|||
fileName = filePath.substring(endpoint.length() + 1 + bucketName.length() + 1); |
|||
} |
|||
RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder() |
|||
.bucket(bucketName) |
|||
.object(fileName) |
|||
.build(); |
|||
|
|||
minioClient.removeObject(removeObjectArgs); |
|||
return true; |
|||
} catch (Exception e) { |
|||
log.error("删除文件失败", e); |
|||
throw new BusinessException(ResultCode.DELETE_FILE_EXCEPTION, e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
|
|||
/** |
|||
* PUBLIC桶策略 |
|||
* 如果不配置,则新建的存储桶默认是PRIVATE,则存储桶文件会拒绝访问 Access Denied |
|||
* |
|||
* @param bucketName 存储桶名称 |
|||
* @return 存储桶策略 |
|||
*/ |
|||
private static String publicBucketPolicy(String bucketName) { |
|||
// AWS的S3存储桶策略 JSON 格式 https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/example-bucket-policies.html
|
|||
return "{\"Version\":\"2012-10-17\"," |
|||
+ "\"Statement\":[{\"Effect\":\"Allow\"," |
|||
+ "\"Principal\":{\"AWS\":[\"*\"]}," |
|||
+ "\"Action\":[\"s3:ListBucketMultipartUploads\",\"s3:GetBucketLocation\",\"s3:ListBucket\"]," |
|||
+ "\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]}," |
|||
+ "{\"Effect\":\"Allow\"," + "\"Principal\":{\"AWS\":[\"*\"]}," |
|||
+ "\"Action\":[\"s3:ListMultipartUploadParts\",\"s3:PutObject\",\"s3:AbortMultipartUpload\",\"s3:DeleteObject\",\"s3:GetObject\"]," |
|||
+ "\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}]}"; |
|||
} |
|||
|
|||
/** |
|||
* 创建存储桶(存储桶不存在) |
|||
* |
|||
* @param bucketName 存储桶名称 |
|||
*/ |
|||
@SneakyThrows |
|||
private void createBucketIfAbsent(String bucketName) { |
|||
BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket(bucketName).build(); |
|||
if (!minioClient.bucketExists(bucketExistsArgs)) { |
|||
MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket(bucketName).build(); |
|||
|
|||
minioClient.makeBucket(makeBucketArgs); |
|||
|
|||
// 设置存储桶访问权限为PUBLIC, 如果不配置,则新建的存储桶默认是PRIVATE,则存储桶文件会拒绝访问 Access Denied
|
|||
SetBucketPolicyArgs setBucketPolicyArgs = SetBucketPolicyArgs |
|||
.builder() |
|||
.bucket(bucketName) |
|||
.config(publicBucketPolicy(bucketName)) |
|||
.build(); |
|||
minioClient.setBucketPolicy(setBucketPolicyArgs); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,150 @@ |
|||
package com.youlai.boot.framework.apidoc; |
|||
|
|||
|
|||
import com.github.xiaoymin.knife4j.annotations.ApiSupport; |
|||
import com.github.xiaoymin.knife4j.core.conf.ExtensionsConstants; |
|||
import com.github.xiaoymin.knife4j.core.conf.GlobalConstants; |
|||
import com.github.xiaoymin.knife4j.spring.configuration.Knife4jProperties; |
|||
import com.github.xiaoymin.knife4j.spring.configuration.Knife4jSetting; |
|||
import com.github.xiaoymin.knife4j.spring.extension.OpenApiExtensionResolver; |
|||
import io.swagger.v3.oas.annotations.tags.Tag; |
|||
import io.swagger.v3.oas.models.OpenAPI; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.lang3.ArrayUtils; |
|||
import org.springdoc.core.customizers.GlobalOpenApiCustomizer; |
|||
import org.springdoc.core.properties.SpringDocConfigProperties; |
|||
import org.springframework.beans.factory.config.BeanDefinition; |
|||
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.context.annotation.Primary; |
|||
import org.springframework.core.type.filter.AnnotationTypeFilter; |
|||
import org.springframework.util.CollectionUtils; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
import java.lang.annotation.Annotation; |
|||
import java.util.*; |
|||
import java.util.stream.Collectors; |
|||
|
|||
/** |
|||
* 增强扩展属性支持 |
|||
* @since 4.1.0 |
|||
* @author <a href="xiaoymin@foxmail.com">xiaoymin@foxmail.com</a> |
|||
* 2022/12/11 22:40 |
|||
*/ |
|||
@Primary |
|||
@Configuration |
|||
@Slf4j |
|||
public class Knife4jOpenApiCustomizer extends com.github.xiaoymin.knife4j.spring.extension.Knife4jOpenApiCustomizer implements GlobalOpenApiCustomizer { |
|||
final Knife4jProperties knife4jProperties; |
|||
final SpringDocConfigProperties properties; |
|||
public Knife4jOpenApiCustomizer(Knife4jProperties knife4jProperties, SpringDocConfigProperties properties) { |
|||
super(knife4jProperties,properties); |
|||
this.knife4jProperties = knife4jProperties; |
|||
this.properties = properties; |
|||
} |
|||
|
|||
@Override |
|||
public void customise(OpenAPI openApi) { |
|||
log.debug("Knife4j OpenApiCustomizer"); |
|||
if (knife4jProperties.isEnable()) { |
|||
Knife4jSetting setting = knife4jProperties.getSetting(); |
|||
OpenApiExtensionResolver openApiExtensionResolver = new OpenApiExtensionResolver(setting, knife4jProperties.getDocuments()); |
|||
// 解析初始化
|
|||
openApiExtensionResolver.start(); |
|||
Map<String, Object> objectMap = new HashMap<>(); |
|||
objectMap.put(GlobalConstants.EXTENSION_OPEN_SETTING_NAME, setting); |
|||
objectMap.put(GlobalConstants.EXTENSION_OPEN_MARKDOWN_NAME, openApiExtensionResolver.getMarkdownFiles()); |
|||
openApi.addExtension(GlobalConstants.EXTENSION_OPEN_API_NAME, objectMap); |
|||
addOrderExtension(openApi); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 往OpenAPI内tags字段添加x-order属性 |
|||
* |
|||
* @param openApi openApi |
|||
*/ |
|||
|
|||
private void addOrderExtension(OpenAPI openApi) { |
|||
if (CollectionUtils.isEmpty(properties.getGroupConfigs())) { |
|||
return; |
|||
} |
|||
// 获取包扫描路径
|
|||
Set<String> packagesToScan = |
|||
properties.getGroupConfigs().stream() |
|||
.map(SpringDocConfigProperties.GroupConfig::getPackagesToScan) |
|||
.filter(toScan -> !CollectionUtils.isEmpty(toScan)) |
|||
.flatMap(List::stream) |
|||
.collect(Collectors.toSet()); |
|||
if (CollectionUtils.isEmpty(packagesToScan)) { |
|||
return; |
|||
} |
|||
// 扫描包下被ApiSupport注解的RestController Class
|
|||
Set<Class<?>> classes = |
|||
packagesToScan.stream() |
|||
.map(packageToScan -> scanPackageByAnnotation(packageToScan, RestController.class)) |
|||
.flatMap(Set::stream) |
|||
.filter(clazz -> clazz.isAnnotationPresent(ApiSupport.class)) |
|||
.collect(Collectors.toSet()); |
|||
if (!CollectionUtils.isEmpty(classes)) { |
|||
// ApiSupport oder值存入tagSortMap<Tag.name,ApiSupport.order>
|
|||
Map<String, Integer> tagOrderMap = new HashMap<>(); |
|||
classes.forEach( |
|||
clazz -> { |
|||
Tag tag = getTag(clazz); |
|||
if (Objects.nonNull(tag)) { |
|||
ApiSupport apiSupport = clazz.getAnnotation(ApiSupport.class); |
|||
tagOrderMap.putIfAbsent(tag.name(), apiSupport.order()); |
|||
} |
|||
}); |
|||
// 往openApi tags字段添加x-order增强属性
|
|||
if (openApi.getTags() != null) { |
|||
openApi |
|||
.getTags() |
|||
.forEach( |
|||
tag -> { |
|||
if (tagOrderMap.containsKey(tag.getName())) { |
|||
tag.addExtension( |
|||
ExtensionsConstants.EXTENSION_ORDER, tagOrderMap.get(tag.getName())); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private Tag getTag(Class<?> clazz) { |
|||
// 从类上获取
|
|||
Tag tag = clazz.getAnnotation(Tag.class); |
|||
if (Objects.isNull(tag)) { |
|||
// 从接口上获取
|
|||
Class<?>[] interfaces = clazz.getInterfaces(); |
|||
if (ArrayUtils.isNotEmpty(interfaces)) { |
|||
for (Class<?> interfaceClazz : interfaces) { |
|||
Tag anno = interfaceClazz.getAnnotation(Tag.class); |
|||
if (Objects.nonNull(anno)) { |
|||
tag = anno; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
return tag; |
|||
} |
|||
|
|||
private Set<Class<?>> scanPackageByAnnotation( |
|||
String packageName, final Class<? extends Annotation> annotationClass) { |
|||
ClassPathScanningCandidateComponentProvider scanner = |
|||
new ClassPathScanningCandidateComponentProvider(false); |
|||
scanner.addIncludeFilter(new AnnotationTypeFilter(annotationClass)); |
|||
Set<Class<?>> classes = new HashSet<>(); |
|||
for (BeanDefinition beanDefinition : scanner.findCandidateComponents(packageName)) { |
|||
try { |
|||
Class<?> clazz = Class.forName(beanDefinition.getBeanClassName()); |
|||
classes.add(clazz); |
|||
} catch (ClassNotFoundException ignore) { |
|||
|
|||
} |
|||
} |
|||
return classes; |
|||
} |
|||
} |
|||
@ -0,0 +1,106 @@ |
|||
package com.youlai.boot.framework.apidoc; |
|||
|
|||
import cn.hutool.core.util.ArrayUtil; |
|||
import com.youlai.boot.framework.security.config.SecurityProperties; |
|||
import io.swagger.v3.oas.models.Components; |
|||
import io.swagger.v3.oas.models.OpenAPI; |
|||
import io.swagger.v3.oas.models.info.Contact; |
|||
import io.swagger.v3.oas.models.info.Info; |
|||
import io.swagger.v3.oas.models.info.License; |
|||
import io.swagger.v3.oas.models.security.SecurityRequirement; |
|||
import io.swagger.v3.oas.models.security.SecurityScheme; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springdoc.core.customizers.GlobalOpenApiCustomizer; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.core.env.Environment; |
|||
import org.springframework.http.HttpHeaders; |
|||
import org.springframework.util.AntPathMatcher; |
|||
|
|||
import java.util.stream.Stream; |
|||
|
|||
/** |
|||
* OpenAPI 接口文档配置 |
|||
* |
|||
* @author Ray.Hao |
|||
* @see <a href="https://doc.xiaominfo.com/docs/quick-start">knife4j 快速开始</a> |
|||
* @since 2023/2/17 |
|||
*/ |
|||
@Configuration |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class OpenApiConfig { |
|||
|
|||
private final Environment environment; |
|||
|
|||
private final SecurityProperties securityProperties; |
|||
|
|||
/** |
|||
* 接口文档信息 |
|||
*/ |
|||
@Bean |
|||
public OpenAPI openApi() { |
|||
|
|||
String appVersion = environment.getProperty("project.version", "1.0.0"); |
|||
|
|||
return new OpenAPI() |
|||
.info(new Info() |
|||
.title("管理系统 API 文档") |
|||
.description("本文档涵盖管理系统的所有API接口,包括登录认证、用户管理、角色管理、部门管理等功能模块,提供详细的接口说明和使用指南。") |
|||
.version(appVersion) |
|||
.license(new License() |
|||
.name("Apache License 2.0") |
|||
.url("http://www.apache.org/licenses/LICENSE-2.0") |
|||
) |
|||
.contact(new Contact() |
|||
.name("youlai") |
|||
.email("youlaitech@163.com") |
|||
.url("https://www.youlai.tech") |
|||
) |
|||
) |
|||
// 配置全局鉴权参数-Authorize
|
|||
.components(new Components() |
|||
.addSecuritySchemes(HttpHeaders.AUTHORIZATION, |
|||
new SecurityScheme() |
|||
.name(HttpHeaders.AUTHORIZATION) |
|||
.type(SecurityScheme.Type.APIKEY) |
|||
.in(SecurityScheme.In.HEADER) |
|||
.scheme("Bearer") |
|||
.bearerFormat("JWT") |
|||
) |
|||
); |
|||
} |
|||
|
|||
|
|||
/** |
|||
* 全局自定义扩展 |
|||
*/ |
|||
@Bean |
|||
public GlobalOpenApiCustomizer globalOpenApiCustomizer() { |
|||
return openApi -> { |
|||
// 全局添加Authorization
|
|||
if (openApi.getPaths() != null) { |
|||
openApi.getPaths().forEach((path, pathItem) -> { |
|||
|
|||
// 忽略认证的请求无需携带 Authorization
|
|||
String[] ignoreUrls = securityProperties.getIgnoreUrls(); |
|||
if (ArrayUtil.isNotEmpty(ignoreUrls)) { |
|||
// Ant 匹配忽略的路径,不添加Authorization
|
|||
AntPathMatcher antPathMatcher = new AntPathMatcher(); |
|||
if (Stream.of(ignoreUrls).anyMatch(ignoreUrl -> antPathMatcher.match(ignoreUrl, path))) { |
|||
return; |
|||
} |
|||
} |
|||
|
|||
// 其他接口统一添加Authorization
|
|||
pathItem.readOperations() |
|||
.forEach(operation -> |
|||
operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) |
|||
); |
|||
}); |
|||
} |
|||
}; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
package com.youlai.boot.framework.cache; |
|||
|
|||
import com.github.benmanes.caffeine.cache.Caffeine; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Value; |
|||
import org.springframework.cache.CacheManager; |
|||
import org.springframework.cache.caffeine.CaffeineCacheManager; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
|
|||
/** |
|||
* caffeine缓存配置 |
|||
* |
|||
* @author Theo |
|||
* @since 2025-01-22 17:40:23 |
|||
*/ |
|||
@Slf4j |
|||
@Configuration |
|||
public class CaffeineConfig { |
|||
|
|||
@Value("${spring.cache.caffeine.spec}") |
|||
private String caffeineSpec; |
|||
|
|||
/** |
|||
* 缓存管理器 |
|||
* |
|||
* @return CacheManager 缓存管理器 |
|||
*/ |
|||
@Bean |
|||
public CacheManager cacheManager() { |
|||
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); |
|||
Caffeine<Object, Object> caffeineBuilder = Caffeine.from(caffeineSpec); |
|||
caffeineCacheManager.setCaffeine(caffeineBuilder); |
|||
return caffeineCacheManager; |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,74 @@ |
|||
package com.youlai.boot.framework.cache; |
|||
|
|||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
|||
import org.springframework.boot.cache.autoconfigure.CacheProperties; |
|||
import org.springframework.boot.context.properties.EnableConfigurationProperties; |
|||
import org.springframework.cache.annotation.EnableCaching; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.data.redis.cache.RedisCacheConfiguration; |
|||
import org.springframework.data.redis.cache.RedisCacheManager; |
|||
import org.springframework.data.redis.cache.RedisCacheWriter; |
|||
import org.springframework.data.redis.connection.RedisConnectionFactory; |
|||
import org.springframework.data.redis.serializer.RedisSerializationContext; |
|||
import org.springframework.data.redis.serializer.RedisSerializer; |
|||
|
|||
/** |
|||
* Redis 缓存配置 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2023/12/4 |
|||
*/ |
|||
@EnableCaching |
|||
@EnableConfigurationProperties(CacheProperties.class) |
|||
@Configuration |
|||
@ConditionalOnProperty(name = "spring.cache.enabled") // xxl.job.enabled = true 才会自动装配
|
|||
public class RedisCacheConfig { |
|||
|
|||
/** |
|||
* 自定义 RedisCacheManager |
|||
* <p> |
|||
* 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer |
|||
* |
|||
* @param redisConnectionFactory {@link RedisConnectionFactory} |
|||
* @param cacheProperties {@link CacheProperties} |
|||
* @return {@link RedisCacheManager} |
|||
*/ |
|||
@Bean |
|||
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory, CacheProperties cacheProperties){ |
|||
return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)) |
|||
.cacheDefaults(redisCacheConfiguration(cacheProperties)) |
|||
.build(); |
|||
} |
|||
|
|||
/** |
|||
* 自定义 RedisCacheConfiguration |
|||
* |
|||
* @param cacheProperties {@link CacheProperties} |
|||
* @return {@link RedisCacheConfiguration} |
|||
*/ |
|||
@Bean |
|||
RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { |
|||
|
|||
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); |
|||
|
|||
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string())); |
|||
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json())); |
|||
|
|||
CacheProperties.Redis redisProperties = cacheProperties.getRedis(); |
|||
|
|||
if (redisProperties.getTimeToLive() != null) { |
|||
config = config.entryTtl(redisProperties.getTimeToLive()); |
|||
} |
|||
if (!redisProperties.isCacheNullValues()) { |
|||
config = config.disableCachingNullValues(); |
|||
} |
|||
if (!redisProperties.isUseKeyPrefix()) { |
|||
config = config.disableKeyPrefix(); |
|||
} |
|||
// 覆盖默认key双冒号 CacheKeyPrefix#prefixed
|
|||
config = config.computePrefixWith(name -> name + ":"); |
|||
return config; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
package com.youlai.boot.framework.cache; |
|||
|
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import com.fasterxml.jackson.databind.SerializationFeature; |
|||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.data.redis.connection.RedisConnectionFactory; |
|||
import org.springframework.data.redis.core.RedisTemplate; |
|||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; |
|||
import org.springframework.data.redis.serializer.RedisSerializer; |
|||
|
|||
/** |
|||
* Redis 配置 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2023/5/15 |
|||
*/ |
|||
@Configuration |
|||
public class RedisConfig { |
|||
|
|||
/** |
|||
* 自定义 RedisTemplate |
|||
* <p> |
|||
* 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer |
|||
* |
|||
* @param redisConnectionFactory {@link RedisConnectionFactory} |
|||
* @return {@link RedisTemplate} |
|||
*/ |
|||
@Bean |
|||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { |
|||
|
|||
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); |
|||
redisTemplate.setConnectionFactory(redisConnectionFactory); |
|||
|
|||
// Key 使用 String 序列化
|
|||
redisTemplate.setKeySerializer(RedisSerializer.string()); |
|||
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(); |
|||
|
|||
Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, Object.class); |
|||
|
|||
redisTemplate.setValueSerializer(jsonSerializer); |
|||
redisTemplate.setHashValueSerializer(jsonSerializer); |
|||
|
|||
redisTemplate.afterPropertiesSet(); |
|||
return redisTemplate; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
package com.youlai.boot.framework.captcha.config; |
|||
|
|||
import cn.hutool.captcha.generator.CodeGenerator; |
|||
import cn.hutool.captcha.generator.MathGenerator; |
|||
import cn.hutool.captcha.generator.RandomGenerator; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
|
|||
import java.awt.*; |
|||
|
|||
/** |
|||
* 验证码自动装配配置 |
|||
* |
|||
* @author haoxr |
|||
* @since 2023/11/24 |
|||
*/ |
|||
@Configuration |
|||
public class CaptchaConfig { |
|||
|
|||
@Autowired |
|||
private CaptchaProperties captchaProperties; |
|||
|
|||
/** |
|||
* 验证码文字生成器 |
|||
* |
|||
* @return CodeGenerator |
|||
*/ |
|||
@Bean |
|||
public CodeGenerator codeGenerator() { |
|||
String codeType = captchaProperties.getCode().getType(); |
|||
int codeLength = captchaProperties.getCode().getLength(); |
|||
if ("math".equalsIgnoreCase(codeType)) { |
|||
return new MathGenerator(codeLength, false); |
|||
} else if ("random".equalsIgnoreCase(codeType)) { |
|||
return new RandomGenerator(codeLength); |
|||
} else { |
|||
throw new IllegalArgumentException("Invalid captcha codegen type: " + codeType); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 验证码字体 |
|||
*/ |
|||
@Bean |
|||
public Font captchaFont() { |
|||
String fontName = captchaProperties.getFont().getName(); |
|||
int fontSize = captchaProperties.getFont().getSize(); |
|||
int fontWight = captchaProperties.getFont().getWeight(); |
|||
return new Font(fontName, fontWight, fontSize); |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,92 @@ |
|||
package com.youlai.boot.framework.captcha.config; |
|||
|
|||
import lombok.Data; |
|||
import org.springframework.boot.context.properties.ConfigurationProperties; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
/** |
|||
* 验证码 属性配置 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2023/11/24 |
|||
*/ |
|||
@Component |
|||
@ConfigurationProperties(prefix = "captcha") |
|||
@Data |
|||
public class CaptchaProperties { |
|||
|
|||
/** |
|||
* 验证码类型 circle-圆圈干扰验证码|gif-Gif验证码|line-干扰线验证码|shear-扭曲干扰验证码 |
|||
*/ |
|||
private String type; |
|||
|
|||
/** |
|||
* 验证码图片宽度 |
|||
*/ |
|||
private int width; |
|||
/** |
|||
* 验证码图片高度 |
|||
*/ |
|||
private int height; |
|||
|
|||
/** |
|||
* 干扰线数量 |
|||
*/ |
|||
private int interfereCount; |
|||
|
|||
/** |
|||
* 文本透明度 |
|||
*/ |
|||
private Float textAlpha; |
|||
|
|||
/** |
|||
* 验证码过期时间,单位:秒 |
|||
*/ |
|||
private Long expireSeconds; |
|||
|
|||
/** |
|||
* 验证码字符配置 |
|||
*/ |
|||
private CodeProperties code; |
|||
|
|||
/** |
|||
* 验证码字体 |
|||
*/ |
|||
private FontProperties font; |
|||
|
|||
/** |
|||
* 验证码字符配置 |
|||
*/ |
|||
@Data |
|||
public static class CodeProperties { |
|||
/** |
|||
* 验证码字符类型 math-算术|random-随机字符串 |
|||
*/ |
|||
private String type; |
|||
/** |
|||
* 验证码字符长度,type=算术时,表示运算位数(1:个位数 2:十位数);type=随机字符时,表示字符个数 |
|||
*/ |
|||
private int length; |
|||
} |
|||
|
|||
/** |
|||
* 验证码字体配置 |
|||
*/ |
|||
@Data |
|||
public static class FontProperties { |
|||
/** |
|||
* 字体名称 |
|||
*/ |
|||
private String name; |
|||
/** |
|||
* 字体样式 0-普通|1-粗体|2-斜体 |
|||
*/ |
|||
private int weight; |
|||
/** |
|||
* 字体大小 |
|||
*/ |
|||
private int size; |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
package com.youlai.boot.framework.captcha.exception; |
|||
|
|||
import com.youlai.boot.common.result.ResultCode; |
|||
import lombok.Getter; |
|||
|
|||
/** |
|||
* 验证码异常 |
|||
*/ |
|||
@Getter |
|||
public class CaptchaException extends RuntimeException { |
|||
|
|||
private final ResultCode resultCode; |
|||
|
|||
public CaptchaException(ResultCode resultCode) { |
|||
super(resultCode.getMsg()); |
|||
this.resultCode = resultCode; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
package com.youlai.boot.framework.captcha.model; |
|||
|
|||
import io.swagger.v3.oas.annotations.media.Schema; |
|||
import lombok.AllArgsConstructor; |
|||
import lombok.Builder; |
|||
import lombok.Data; |
|||
import lombok.NoArgsConstructor; |
|||
|
|||
/** |
|||
* 验证码信息 |
|||
*/ |
|||
@Data |
|||
@Builder |
|||
@NoArgsConstructor |
|||
@AllArgsConstructor |
|||
@Schema(description = "验证码信息") |
|||
public class CaptchaInfo { |
|||
|
|||
@Schema(description = "验证码缓存ID") |
|||
private String captchaId; |
|||
|
|||
@Schema(description = "验证码图片Base64字符串") |
|||
private String captchaBase64; |
|||
|
|||
} |
|||
@ -0,0 +1,102 @@ |
|||
package com.youlai.boot.framework.captcha.service; |
|||
|
|||
import cn.hutool.captcha.AbstractCaptcha; |
|||
import cn.hutool.captcha.CaptchaUtil; |
|||
import cn.hutool.captcha.generator.CodeGenerator; |
|||
import cn.hutool.core.util.IdUtil; |
|||
import cn.hutool.core.util.StrUtil; |
|||
import com.youlai.boot.common.constant.RedisConstants; |
|||
import com.youlai.boot.common.enums.CaptchaTypeEnum; |
|||
import com.youlai.boot.common.result.ResultCode; |
|||
import com.youlai.boot.framework.captcha.config.CaptchaProperties; |
|||
import com.youlai.boot.framework.captcha.exception.CaptchaException; |
|||
import com.youlai.boot.framework.captcha.model.CaptchaInfo; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.data.redis.core.RedisTemplate; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.awt.Font; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
/** |
|||
* 验证码服务 |
|||
*/ |
|||
@Service |
|||
@RequiredArgsConstructor |
|||
public class CaptchaService { |
|||
|
|||
private final RedisTemplate<String, Object> redisTemplate; |
|||
private final CaptchaProperties captchaProperties; |
|||
private final CodeGenerator codeGenerator; |
|||
private final Font captchaFont; |
|||
|
|||
/** |
|||
* 生成验证码 |
|||
*/ |
|||
public CaptchaInfo generate() { |
|||
String captchaType = captchaProperties.getType(); |
|||
int width = captchaProperties.getWidth(); |
|||
int height = captchaProperties.getHeight(); |
|||
int interfereCount = captchaProperties.getInterfereCount(); |
|||
int codeLength = captchaProperties.getCode().getLength(); |
|||
|
|||
AbstractCaptcha captcha; |
|||
if (CaptchaTypeEnum.CIRCLE.name().equalsIgnoreCase(captchaType)) { |
|||
captcha = CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount); |
|||
} else if (CaptchaTypeEnum.GIF.name().equalsIgnoreCase(captchaType)) { |
|||
captcha = CaptchaUtil.createGifCaptcha(width, height, codeLength); |
|||
} else if (CaptchaTypeEnum.LINE.name().equalsIgnoreCase(captchaType)) { |
|||
captcha = CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount); |
|||
} else if (CaptchaTypeEnum.SHEAR.name().equalsIgnoreCase(captchaType)) { |
|||
captcha = CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount); |
|||
} else { |
|||
throw new IllegalArgumentException("Invalid captcha type: " + captchaType); |
|||
} |
|||
|
|||
captcha.setGenerator(codeGenerator); |
|||
captcha.setTextAlpha(captchaProperties.getTextAlpha()); |
|||
captcha.setFont(captchaFont); |
|||
|
|||
String captchaCode = captcha.getCode(); |
|||
String imageBase64Data = captcha.getImageBase64Data(); |
|||
|
|||
String captchaId = IdUtil.fastSimpleUUID(); |
|||
redisTemplate.opsForValue().set( |
|||
StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId), |
|||
captchaCode, |
|||
captchaProperties.getExpireSeconds(), |
|||
TimeUnit.SECONDS |
|||
); |
|||
|
|||
return CaptchaInfo.builder() |
|||
.captchaId(captchaId) |
|||
.captchaBase64(imageBase64Data) |
|||
.build(); |
|||
} |
|||
|
|||
/** |
|||
* 校验验证码,失败抛异常 |
|||
* |
|||
* @param captchaId 验证码ID |
|||
* @param captchaCode 用户输入的验证码 |
|||
* @throws CaptchaException 验证码错误或过期 |
|||
*/ |
|||
public void validate(String captchaId, String captchaCode) { |
|||
if (StrUtil.isBlank(captchaId) || StrUtil.isBlank(captchaCode)) { |
|||
throw new CaptchaException(ResultCode.USER_VERIFICATION_CODE_ERROR); |
|||
} |
|||
|
|||
String cacheKey = StrUtil.format(RedisConstants.Captcha.IMAGE_CODE, captchaId); |
|||
String cachedCode = (String) redisTemplate.opsForValue().get(cacheKey); |
|||
if (cachedCode == null) { |
|||
throw new CaptchaException(ResultCode.USER_VERIFICATION_CODE_EXPIRED); |
|||
} |
|||
|
|||
if (!codeGenerator.verify(cachedCode, captchaCode)) { |
|||
throw new CaptchaException(ResultCode.USER_VERIFICATION_CODE_ERROR); |
|||
} |
|||
|
|||
redisTemplate.delete(cacheKey); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
package com.youlai.boot.framework.integration.mail.config; |
|||
|
|||
import org.springframework.boot.context.properties.EnableConfigurationProperties; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.mail.javamail.JavaMailSender; |
|||
import org.springframework.mail.javamail.JavaMailSenderImpl; |
|||
|
|||
import java.util.Properties; |
|||
|
|||
/** |
|||
* MailConfig 配置类,用于手动配置和注入 JavaMailSender。 |
|||
* 通过读取 MailProperties 类中配置的邮件相关属性来初始化 JavaMailSender。 |
|||
* <p> |
|||
* 手动注入的原因是为了避免在使用 application-dev.yml 或其他非 application.yml 配置文件时, |
|||
* IDEA 提示无法找到 JavaMailSender 的 bean。 |
|||
* |
|||
* @author Ray |
|||
* @since 2024/8/17 |
|||
*/ |
|||
@Configuration |
|||
@EnableConfigurationProperties(MailProperties.class) |
|||
public class MailConfig { |
|||
|
|||
private final MailProperties mailProperties; |
|||
|
|||
public MailConfig(MailProperties mailProperties) { |
|||
this.mailProperties = mailProperties; |
|||
} |
|||
|
|||
/** |
|||
* 创建并配置 JavaMailSender bean。 |
|||
* |
|||
* @return 配置好的 JavaMailSender 实例 |
|||
*/ |
|||
@Bean |
|||
public JavaMailSender javaMailSender() { |
|||
JavaMailSenderImpl mailSender = new JavaMailSenderImpl(); |
|||
mailSender.setHost(mailProperties.getHost()); |
|||
mailSender.setPort(mailProperties.getPort()); |
|||
mailSender.setUsername(mailProperties.getUsername()); |
|||
mailSender.setPassword(mailProperties.getPassword()); |
|||
|
|||
Properties properties = mailSender.getJavaMailProperties(); |
|||
properties.put("mail.smtp.auth", mailProperties.getProperties().getSmtp().isAuth()); |
|||
properties.put("mail.smtp.starttls.enable", mailProperties.getProperties().getSmtp().getStarttls().isEnable()); |
|||
|
|||
return mailSender; |
|||
} |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
package com.youlai.boot.framework.integration.mail.config; |
|||
|
|||
import lombok.Data; |
|||
import org.springframework.boot.context.properties.ConfigurationProperties; |
|||
|
|||
/** |
|||
* 邮件配置类,用于接收和存储邮件相关的配置属性。 |
|||
* |
|||
* @author Ray |
|||
* @since 2024/8/17 |
|||
*/ |
|||
@ConfigurationProperties(prefix = "spring.mail") |
|||
@Data |
|||
public class MailProperties { |
|||
|
|||
/** |
|||
* 邮件服务器主机名或 IP 地址。 |
|||
* 例如:smtp.example.com |
|||
*/ |
|||
private String host; |
|||
|
|||
/** |
|||
* 邮件服务器端口号。 |
|||
* 例如:587 |
|||
*/ |
|||
private int port; |
|||
|
|||
/** |
|||
* 用于连接邮件服务器的用户名。 |
|||
* 例如:your_email@example.com |
|||
*/ |
|||
private String username; |
|||
|
|||
/** |
|||
* 用于连接邮件服务器的密码。 |
|||
* 该密码应安全存储,不应在代码中硬编码。 |
|||
*/ |
|||
private String password; |
|||
|
|||
/** |
|||
* 邮件发送者地址。 |
|||
*/ |
|||
private String from; |
|||
|
|||
/** |
|||
* 邮件服务器的其他属性配置。 |
|||
* 这些配置通常用于进一步定制邮件发送行为。 |
|||
*/ |
|||
private Properties properties = new Properties(); |
|||
|
|||
/** |
|||
* 内部类,用于封装邮件服务器的详细配置。 |
|||
* 包含 SMTP 相关的配置选项。 |
|||
*/ |
|||
@Data |
|||
public static class Properties { |
|||
|
|||
/** |
|||
* SMTP 配置选项类。 |
|||
* 包含认证、加密等与 SMTP 协议相关的配置。 |
|||
*/ |
|||
private Smtp smtp = new Smtp(); |
|||
|
|||
@Data |
|||
public static class Smtp { |
|||
|
|||
/** |
|||
* 是否启用 SMTP 认证。 |
|||
* 如果为 `true`,则需要提供有效的用户名和密码进行认证。 |
|||
*/ |
|||
private boolean auth; |
|||
|
|||
/** |
|||
* STARTTLS 加密配置选项。 |
|||
*/ |
|||
private StartTls starttls = new StartTls(); |
|||
|
|||
@Data |
|||
public static class StartTls { |
|||
|
|||
/** |
|||
* 是否启用 STARTTLS 加密。 |
|||
* 如果为 `true`,在发送邮件时将启用 STARTTLS 协议进行加密传输。 |
|||
*/ |
|||
private boolean enable; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
package com.youlai.boot.framework.integration.mail.service; |
|||
|
|||
import com.youlai.boot.framework.integration.mail.config.MailProperties; |
|||
import jakarta.mail.MessagingException; |
|||
import jakarta.mail.internet.MimeMessage; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.core.io.FileSystemResource; |
|||
import org.springframework.mail.SimpleMailMessage; |
|||
import org.springframework.mail.javamail.JavaMailSender; |
|||
import org.springframework.mail.javamail.MimeMessageHelper; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.io.File; |
|||
|
|||
/** |
|||
* 邮件服务 |
|||
* |
|||
* @author Ray.Hao |
|||
* @since 2024/8/17 |
|||
*/ |
|||
@Service |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class MailService { |
|||
|
|||
private final JavaMailSender mailSender; |
|||
|
|||
private final MailProperties mailProperties; |
|||
|
|||
/** |
|||
* 发送简单文本邮件 |
|||
* |
|||
* @param to 收件人地址 |
|||
* @param subject 邮件主题 |
|||
* @param text 邮件内容 |
|||
*/ |
|||
public void sendMail(String to, String subject, String text) { |
|||
try { |
|||
SimpleMailMessage message = new SimpleMailMessage(); |
|||
message.setFrom(mailProperties.getFrom()); |
|||
message.setTo(to); |
|||
message.setSubject(subject); |
|||
message.setText(text); |
|||
mailSender.send(message); |
|||
} catch (Exception e) { |
|||
log.error("发送邮件失败{}", e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 发送带附件的邮件 |
|||
* |
|||
* @param to 收件人地址 |
|||
* @param subject 邮件主题 |
|||
* @param text 邮件内容 |
|||
* @param filePath 附件路径 |
|||
*/ |
|||
public void sendMailWithAttachment(String to, String subject, String text, String filePath) { |
|||
MimeMessage message = mailSender.createMimeMessage(); |
|||
try { |
|||
MimeMessageHelper helper = new MimeMessageHelper(message, true); |
|||
helper.setFrom(mailProperties.getFrom()); |
|||
helper.setTo(to); |
|||
helper.setSubject(subject); |
|||
helper.setText(text, true); // true 表示支持HTML内容
|
|||
|
|||
FileSystemResource file = new FileSystemResource(new File(filePath)); |
|||
helper.addAttachment(file.getFilename(), file); |
|||
|
|||
mailSender.send(message); |
|||
} catch (MessagingException e) { |
|||
log.error("发送带附件的邮件失败{}", e.getMessage()); |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue