Browse Source

初始化

master
review512jwy@163.com 2 months ago
commit
efed257f35
  1. 35
      .editorconfig
  2. 14
      .gitattributes
  3. 19
      .gitignore
  4. 23
      Dockerfile
  5. 201
      LICENSE
  6. 195
      README.md
  7. 69
      docker/docker-compose.yml
  8. 0
      docker/minio/README.md
  9. 20
      docker/mysql/conf/my.cnf
  10. 2297
      docker/redis/config/redis.conf
  11. 16
      docker/run.md
  12. 0
      docker/xxljob/README.md
  13. 319
      pom.xml
  14. 586
      sql/mysql/youlai_admin.sql
  15. 524
      sql/mysql/youlai_admin_template.sql
  16. 19
      src/main/java/com/youlai/boot/YouLaiBootApplication.java
  17. 86
      src/main/java/com/youlai/boot/auth/controller/AuthController.java
  18. 85
      src/main/java/com/youlai/boot/auth/controller/WxMaAuthController.java
  19. 31
      src/main/java/com/youlai/boot/auth/model/LoginReq.java
  20. 28
      src/main/java/com/youlai/boot/auth/model/WxMaBindMobileReq.java
  21. 42
      src/main/java/com/youlai/boot/auth/model/WxMaLoginResp.java
  22. 24
      src/main/java/com/youlai/boot/auth/model/WxMaPhoneLoginReq.java
  23. 56
      src/main/java/com/youlai/boot/auth/service/AuthService.java
  24. 53
      src/main/java/com/youlai/boot/auth/service/WxMaAuthService.java
  25. 151
      src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java
  26. 245
      src/main/java/com/youlai/boot/auth/service/impl/WxMaAuthServiceImpl.java
  27. 98
      src/main/java/com/youlai/boot/codegen/config/CodegenProperties.java
  28. 112
      src/main/java/com/youlai/boot/codegen/controller/CodegenController.java
  29. 41
      src/main/java/com/youlai/boot/codegen/converter/CodegenConverter.java
  30. 89
      src/main/java/com/youlai/boot/codegen/enums/FormTypeEnum.java
  31. 108
      src/main/java/com/youlai/boot/codegen/enums/JavaTypeEnum.java
  32. 73
      src/main/java/com/youlai/boot/codegen/enums/QueryTypeEnum.java
  33. 41
      src/main/java/com/youlai/boot/codegen/mapper/DatabaseMapper.java
  34. 20
      src/main/java/com/youlai/boot/codegen/mapper/GenTableColumnMapper.java
  35. 20
      src/main/java/com/youlai/boot/codegen/mapper/GenTableMapper.java
  36. 65
      src/main/java/com/youlai/boot/codegen/model/entity/GenTable.java
  37. 107
      src/main/java/com/youlai/boot/codegen/model/entity/GenTableColumn.java
  38. 109
      src/main/java/com/youlai/boot/codegen/model/form/GenConfigForm.java
  39. 31
      src/main/java/com/youlai/boot/codegen/model/query/TablePageQuery.java
  40. 31
      src/main/java/com/youlai/boot/codegen/model/query/TableQuery.java
  41. 25
      src/main/java/com/youlai/boot/codegen/model/vo/CodegenPreviewVO.java
  42. 26
      src/main/java/com/youlai/boot/codegen/model/vo/ColumnMetaVO.java
  43. 26
      src/main/java/com/youlai/boot/codegen/model/vo/TableMetaVO.java
  44. 32
      src/main/java/com/youlai/boot/codegen/model/vo/TablePageVO.java
  45. 40
      src/main/java/com/youlai/boot/codegen/service/CodegenService.java
  46. 14
      src/main/java/com/youlai/boot/codegen/service/GenTableColumnService.java
  47. 39
      src/main/java/com/youlai/boot/codegen/service/GenTableService.java
  48. 427
      src/main/java/com/youlai/boot/codegen/service/impl/CodegenServiceImpl.java
  49. 21
      src/main/java/com/youlai/boot/codegen/service/impl/GenTableColumnServiceImpl.java
  50. 227
      src/main/java/com/youlai/boot/codegen/service/impl/GenTableServiceImpl.java
  51. 28
      src/main/java/com/youlai/boot/common/annotation/DataPermission.java
  52. 47
      src/main/java/com/youlai/boot/common/annotation/Log.java
  53. 27
      src/main/java/com/youlai/boot/common/annotation/RepeatSubmit.java
  54. 35
      src/main/java/com/youlai/boot/common/annotation/ValidField.java
  55. 144
      src/main/java/com/youlai/boot/common/aspect/LogAspect.java
  56. 102
      src/main/java/com/youlai/boot/common/aspect/RepeatSubmitAspect.java
  57. 48
      src/main/java/com/youlai/boot/common/base/BaseEntity.java
  58. 33
      src/main/java/com/youlai/boot/common/base/BaseQuery.java
  59. 88
      src/main/java/com/youlai/boot/common/base/IBaseEnum.java
  60. 48
      src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java
  61. 63
      src/main/java/com/youlai/boot/common/constant/RedisConstants.java
  62. 25
      src/main/java/com/youlai/boot/common/constant/SecurityConstants.java
  63. 32
      src/main/java/com/youlai/boot/common/constant/SystemConstants.java
  64. 56
      src/main/java/com/youlai/boot/common/enums/ActionTypeEnum.java
  65. 27
      src/main/java/com/youlai/boot/common/enums/CaptchaTypeEnum.java
  66. 81
      src/main/java/com/youlai/boot/common/enums/DataScopeEnum.java
  67. 26
      src/main/java/com/youlai/boot/common/enums/EnvEnum.java
  68. 52
      src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java
  69. 27
      src/main/java/com/youlai/boot/common/enums/StatusEnum.java
  70. 45
      src/main/java/com/youlai/boot/common/exception/BusinessException.java
  71. 30
      src/main/java/com/youlai/boot/common/model/KeyValue.java
  72. 53
      src/main/java/com/youlai/boot/common/model/Option.java
  73. 43
      src/main/java/com/youlai/boot/common/result/ExcelResult.java
  74. 15
      src/main/java/com/youlai/boot/common/result/IResultCode.java
  75. 71
      src/main/java/com/youlai/boot/common/result/PageResult.java
  76. 122
      src/main/java/com/youlai/boot/common/result/ResponseWriter.java
  77. 79
      src/main/java/com/youlai/boot/common/result/Result.java
  78. 155
      src/main/java/com/youlai/boot/common/result/ResultCode.java
  79. 19
      src/main/java/com/youlai/boot/common/util/ExcelUtils.java
  80. 139
      src/main/java/com/youlai/boot/common/util/IPUtils.java
  81. 31
      src/main/java/com/youlai/boot/common/validator/FieldValidator.java
  82. 55
      src/main/java/com/youlai/boot/file/controller/FileController.java
  83. 23
      src/main/java/com/youlai/boot/file/model/FileInfo.java
  84. 30
      src/main/java/com/youlai/boot/file/service/FileService.java
  85. 100
      src/main/java/com/youlai/boot/file/service/impl/AliyunFileService.java
  86. 92
      src/main/java/com/youlai/boot/file/service/impl/LocalFileService.java
  87. 210
      src/main/java/com/youlai/boot/file/service/impl/MinioFileService.java
  88. 150
      src/main/java/com/youlai/boot/framework/apidoc/Knife4jOpenApiCustomizer.java
  89. 106
      src/main/java/com/youlai/boot/framework/apidoc/OpenApiConfig.java
  90. 37
      src/main/java/com/youlai/boot/framework/cache/CaffeineConfig.java
  91. 74
      src/main/java/com/youlai/boot/framework/cache/RedisCacheConfig.java
  92. 56
      src/main/java/com/youlai/boot/framework/cache/RedisConfig.java
  93. 54
      src/main/java/com/youlai/boot/framework/captcha/config/CaptchaConfig.java
  94. 92
      src/main/java/com/youlai/boot/framework/captcha/config/CaptchaProperties.java
  95. 19
      src/main/java/com/youlai/boot/framework/captcha/exception/CaptchaException.java
  96. 25
      src/main/java/com/youlai/boot/framework/captcha/model/CaptchaInfo.java
  97. 102
      src/main/java/com/youlai/boot/framework/captcha/service/CaptchaService.java
  98. 50
      src/main/java/com/youlai/boot/framework/integration/mail/config/MailConfig.java
  99. 89
      src/main/java/com/youlai/boot/framework/integration/mail/config/MailProperties.java
  100. 76
      src/main/java/com/youlai/boot/framework/integration/mail/service/MailService.java

35
.editorconfig

@ -0,0 +1,35 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# Java files
[*.java]
indent_style = space
indent_size = 4
# XML files
[*.xml]
indent_style = space
indent_size = 4
# YAML files
[*.{yml,yaml}]
indent_style = space
indent_size = 2
# Properties files
[*.properties]
indent_style = space
indent_size = 4
# Markdown files
[*.md]
trim_trailing_whitespace = false

14
.gitattributes

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

19
.gitignore

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

23
Dockerfile

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

201
LICENSE

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

195
README.md

@ -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` |
## 📊 项目统计
![Repobeats](https://repobeats.axiom.co/api/embed/544c5c0b5b3611a6c4d5ef0faa243a9066b89659.svg)
## 🤝 参与贡献
欢迎 Issue、PR 和 Star!详见 [贡献指南](https://www.youlai.tech/docs/admin/faq/help)。
[![Contributors](https://contrib.rocks/image?repo=haoxianrui/youlai-boot)](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>

69
docker/docker-compose.yml

@ -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
docker/minio/README.md

20
docker/mysql/conf/my.cnf

@ -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 命令行工具字符集

2297
docker/redis/config/redis.conf

File diff suppressed because it is too large

16
docker/run.md

@ -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
docker/xxljob/README.md

319
pom.xml

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

586
sql/mysql/youlai_admin.sql

@ -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='用户第三方账号绑定表';

524
sql/mysql/youlai_admin_template.sql

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

19
src/main/java/com/youlai/boot/YouLaiBootApplication.java

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

86
src/main/java/com/youlai/boot/auth/controller/AuthController.java

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

85
src/main/java/com/youlai/boot/auth/controller/WxMaAuthController.java

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

31
src/main/java/com/youlai/boot/auth/model/LoginReq.java

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

28
src/main/java/com/youlai/boot/auth/model/WxMaBindMobileReq.java

@ -0,0 +1,28 @@
package com.youlai.boot.auth.model;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
/**
* 微信小程序绑定手机号请求
*
* @author Ray.Hao
* @since 3.0.0
*/
@Schema(description = "微信小程序绑定手机号请求")
@Data
public class WxMaBindMobileReq {
@NotBlank(message = "openid 不能为空")
@Schema(description = "微信用户唯一标识(静默登录返回)", example = "oVBkZ0aYgDMDIywRdgPW8-joxXc4")
private String openid;
@NotBlank(message = "手机号不能为空")
@Schema(description = "手机号码", example = "18888888888")
private String mobile;
@NotBlank(message = "短信验证码不能为空")
@Schema(description = "短信验证码", example = "123456")
private String smsCode;
}

42
src/main/java/com/youlai/boot/auth/model/WxMaLoginResp.java

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

24
src/main/java/com/youlai/boot/auth/model/WxMaPhoneLoginReq.java

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

56
src/main/java/com/youlai/boot/auth/service/AuthService.java

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

53
src/main/java/com/youlai/boot/auth/service/WxMaAuthService.java

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

151
src/main/java/com/youlai/boot/auth/service/impl/AuthServiceImpl.java

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

245
src/main/java/com/youlai/boot/auth/service/impl/WxMaAuthServiceImpl.java

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

98
src/main/java/com/youlai/boot/codegen/config/CodegenProperties.java

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

112
src/main/java/com/youlai/boot/codegen/controller/CodegenController.java

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

41
src/main/java/com/youlai/boot/codegen/converter/CodegenConverter.java

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

89
src/main/java/com/youlai/boot/codegen/enums/FormTypeEnum.java

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

108
src/main/java/com/youlai/boot/codegen/enums/JavaTypeEnum.java

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

73
src/main/java/com/youlai/boot/codegen/enums/QueryTypeEnum.java

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

41
src/main/java/com/youlai/boot/codegen/mapper/DatabaseMapper.java

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

20
src/main/java/com/youlai/boot/codegen/mapper/GenTableColumnMapper.java

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

20
src/main/java/com/youlai/boot/codegen/mapper/GenTableMapper.java

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

65
src/main/java/com/youlai/boot/codegen/model/entity/GenTable.java

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

107
src/main/java/com/youlai/boot/codegen/model/entity/GenTableColumn.java

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

109
src/main/java/com/youlai/boot/codegen/model/form/GenConfigForm.java

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

31
src/main/java/com/youlai/boot/codegen/model/query/TablePageQuery.java

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

31
src/main/java/com/youlai/boot/codegen/model/query/TableQuery.java

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

25
src/main/java/com/youlai/boot/codegen/model/vo/CodegenPreviewVO.java

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

26
src/main/java/com/youlai/boot/codegen/model/vo/ColumnMetaVO.java

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

26
src/main/java/com/youlai/boot/codegen/model/vo/TableMetaVO.java

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

32
src/main/java/com/youlai/boot/codegen/model/vo/TablePageVO.java

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

40
src/main/java/com/youlai/boot/codegen/service/CodegenService.java

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

14
src/main/java/com/youlai/boot/codegen/service/GenTableColumnService.java

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

39
src/main/java/com/youlai/boot/codegen/service/GenTableService.java

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

427
src/main/java/com/youlai/boot/codegen/service/impl/CodegenServiceImpl.java

@ -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 模板名例如 EntityControllerAPI
* @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 子包名例如 controllerservice.implapiviews
* @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);
}
}
}
}

21
src/main/java/com/youlai/boot/codegen/service/impl/GenTableColumnServiceImpl.java

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

227
src/main/java/com/youlai/boot/codegen/service/impl/GenTableServiceImpl.java

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

28
src/main/java/com/youlai/boot/common/annotation/DataPermission.java

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

47
src/main/java/com/youlai/boot/common/annotation/Log.java

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

27
src/main/java/com/youlai/boot/common/annotation/RepeatSubmit.java

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

35
src/main/java/com/youlai/boot/common/annotation/ValidField.java

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

144
src/main/java/com/youlai/boot/common/aspect/LogAspect.java

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

102
src/main/java/com/youlai/boot/common/aspect/RepeatSubmitAspect.java

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

48
src/main/java/com/youlai/boot/common/base/BaseEntity.java

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

33
src/main/java/com/youlai/boot/common/base/BaseQuery.java

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

88
src/main/java/com/youlai/boot/common/base/IBaseEnum.java

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

48
src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java

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

63
src/main/java/com/youlai/boot/common/constant/RedisConstants.java

@ -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"; // 系统角色和权限映射
}
}

25
src/main/java/com/youlai/boot/common/constant/SecurityConstants.java

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

32
src/main/java/com/youlai/boot/common/constant/SystemConstants.java

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

56
src/main/java/com/youlai/boot/common/enums/ActionTypeEnum.java

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

27
src/main/java/com/youlai/boot/common/enums/CaptchaTypeEnum.java

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

81
src/main/java/com/youlai/boot/common/enums/DataScopeEnum.java

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

26
src/main/java/com/youlai/boot/common/enums/EnvEnum.java

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

52
src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java

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

27
src/main/java/com/youlai/boot/common/enums/StatusEnum.java

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

45
src/main/java/com/youlai/boot/common/exception/BusinessException.java

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

30
src/main/java/com/youlai/boot/common/model/KeyValue.java

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

53
src/main/java/com/youlai/boot/common/model/Option.java

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

43
src/main/java/com/youlai/boot/common/result/ExcelResult.java

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

15
src/main/java/com/youlai/boot/common/result/IResultCode.java

@ -0,0 +1,15 @@
package com.youlai.boot.common.result;
/**
* 响应码接口
*
* @author Ray.Hao
* @since 1.0.0
**/
public interface IResultCode {
String getCode();
String getMsg();
}

71
src/main/java/com/youlai/boot/common/result/PageResult.java

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

122
src/main/java/com/youlai/boot/common/result/ResponseWriter.java

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

79
src/main/java/com/youlai/boot/common/result/Result.java

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

155
src/main/java/com/youlai/boot/common/result/ResultCode.java

@ -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 预留号段 A0200A0300A0400
* - 错误码后三位编号与 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; // 默认系统执行错误
}
}

19
src/main/java/com/youlai/boot/common/util/ExcelUtils.java

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

139
src/main/java/com/youlai/boot/common/util/IPUtils.java

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

31
src/main/java/com/youlai/boot/common/validator/FieldValidator.java

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

55
src/main/java/com/youlai/boot/file/controller/FileController.java

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

23
src/main/java/com/youlai/boot/file/model/FileInfo.java

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

30
src/main/java/com/youlai/boot/file/service/FileService.java

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

100
src/main/java/com/youlai/boot/file/service/impl/AliyunFileService.java

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

92
src/main/java/com/youlai/boot/file/service/impl/LocalFileService.java

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

210
src/main/java/com/youlai/boot/file/service/impl/MinioFileService.java

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

150
src/main/java/com/youlai/boot/framework/apidoc/Knife4jOpenApiCustomizer.java

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

106
src/main/java/com/youlai/boot/framework/apidoc/OpenApiConfig.java

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

37
src/main/java/com/youlai/boot/framework/cache/CaffeineConfig.java

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

74
src/main/java/com/youlai/boot/framework/cache/RedisCacheConfig.java

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

56
src/main/java/com/youlai/boot/framework/cache/RedisConfig.java

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

54
src/main/java/com/youlai/boot/framework/captcha/config/CaptchaConfig.java

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

92
src/main/java/com/youlai/boot/framework/captcha/config/CaptchaProperties.java

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

19
src/main/java/com/youlai/boot/framework/captcha/exception/CaptchaException.java

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

25
src/main/java/com/youlai/boot/framework/captcha/model/CaptchaInfo.java

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

102
src/main/java/com/youlai/boot/framework/captcha/service/CaptchaService.java

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

50
src/main/java/com/youlai/boot/framework/integration/mail/config/MailConfig.java

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

89
src/main/java/com/youlai/boot/framework/integration/mail/config/MailProperties.java

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

76
src/main/java/com/youlai/boot/framework/integration/mail/service/MailService.java

@ -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…
Cancel
Save