ky hai 3 días
achega
f4b9646984
Modificáronse 100 ficheiros con 7361 adicións e 0 borrados
  1. 13 0
      .gitignore
  2. 60 0
      CHANGELOG.md
  3. 24 0
      Dockerfile
  4. 201 0
      LICENSE
  5. 0 0
      README.md
  6. 294 0
      pom.xml
  7. 549 0
      sql/mysql5/youlai_boot.sql
  8. 550 0
      sql/mysql8/youlai_boot.sql
  9. 34 0
      src/main/java/com/youlai/boot/CheckJarPid.java
  10. 50 0
      src/main/java/com/youlai/boot/DirectoryTreeBuilder.java
  11. 23 0
      src/main/java/com/youlai/boot/YouLaiApplication.java
  12. 28 0
      src/main/java/com/youlai/boot/common/annotation/DataPermission.java
  13. 23 0
      src/main/java/com/youlai/boot/common/annotation/Log.java
  14. 28 0
      src/main/java/com/youlai/boot/common/annotation/RepeatSubmit.java
  15. 15 0
      src/main/java/com/youlai/boot/common/base/BaseAnalysisEventListener.java
  16. 48 0
      src/main/java/com/youlai/boot/common/base/BaseEntity.java
  17. 28 0
      src/main/java/com/youlai/boot/common/base/BasePageQuery.java
  18. 21 0
      src/main/java/com/youlai/boot/common/base/BaseVO.java
  19. 88 0
      src/main/java/com/youlai/boot/common/base/IBaseEnum.java
  20. 33 0
      src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java
  21. 45 0
      src/main/java/com/youlai/boot/common/constant/RedisConstants.java
  22. 46 0
      src/main/java/com/youlai/boot/common/constant/SecurityConstants.java
  23. 120 0
      src/main/java/com/youlai/boot/common/constant/SymbolConstant.java
  24. 43 0
      src/main/java/com/youlai/boot/common/constant/SystemConstants.java
  25. 31 0
      src/main/java/com/youlai/boot/common/enums/DataScopeEnum.java
  26. 26 0
      src/main/java/com/youlai/boot/common/enums/EnvEnum.java
  27. 34 0
      src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java
  28. 27 0
      src/main/java/com/youlai/boot/common/enums/StatusEnum.java
  29. 38 0
      src/main/java/com/youlai/boot/common/exception/BusinessException.java
  30. 219 0
      src/main/java/com/youlai/boot/common/exception/GlobalExceptionHandler.java
  31. 32 0
      src/main/java/com/youlai/boot/common/model/KeyValue.java
  32. 53 0
      src/main/java/com/youlai/boot/common/model/Option.java
  33. 15 0
      src/main/java/com/youlai/boot/common/result/IResultCode.java
  34. 46 0
      src/main/java/com/youlai/boot/common/result/PageResult.java
  35. 73 0
      src/main/java/com/youlai/boot/common/result/Result.java
  36. 116 0
      src/main/java/com/youlai/boot/common/result/ResultCode.java
  37. 61 0
      src/main/java/com/youlai/boot/common/util/DateUtils.java
  38. 20 0
      src/main/java/com/youlai/boot/common/util/ExcelUtils.java
  39. 304 0
      src/main/java/com/youlai/boot/common/util/FtpUtil.java
  40. 139 0
      src/main/java/com/youlai/boot/common/util/IPUtils.java
  41. 51 0
      src/main/java/com/youlai/boot/common/util/ResponseUtils.java
  42. 61 0
      src/main/java/com/youlai/boot/common/util/SFTPChannel.java
  43. 10 0
      src/main/java/com/youlai/boot/common/util/SFTPConstants.java
  44. 41 0
      src/main/java/com/youlai/boot/common/util/SFTPTest.java
  45. 98 0
      src/main/java/com/youlai/boot/common/util/SystemUtil.java
  46. 14 0
      src/main/java/com/youlai/boot/config/AppConfig.java
  47. 55 0
      src/main/java/com/youlai/boot/config/CaptchaConfig.java
  48. 42 0
      src/main/java/com/youlai/boot/config/CorsConfig.java
  49. 51 0
      src/main/java/com/youlai/boot/config/MailConfig.java
  50. 48 0
      src/main/java/com/youlai/boot/config/MybatisConfig.java
  51. 74 0
      src/main/java/com/youlai/boot/config/RedisCacheConfig.java
  52. 42 0
      src/main/java/com/youlai/boot/config/RedisConfig.java
  53. 111 0
      src/main/java/com/youlai/boot/config/SecurityConfig.java
  54. 84 0
      src/main/java/com/youlai/boot/config/SwaggerConfig.java
  55. 25 0
      src/main/java/com/youlai/boot/config/ThreadPoolConfig.java
  56. 67 0
      src/main/java/com/youlai/boot/config/WebMvcConfig.java
  57. 107 0
      src/main/java/com/youlai/boot/config/WebSocketConfig.java
  58. 61 0
      src/main/java/com/youlai/boot/config/XxlJobConfig.java
  59. 50 0
      src/main/java/com/youlai/boot/config/property/AliyunSmsProperties.java
  60. 92 0
      src/main/java/com/youlai/boot/config/property/CaptchaProperties.java
  61. 96 0
      src/main/java/com/youlai/boot/config/property/CodegenProperties.java
  62. 89 0
      src/main/java/com/youlai/boot/config/property/MailProperties.java
  63. 44 0
      src/main/java/com/youlai/boot/config/property/SecurityProperties.java
  64. 95 0
      src/main/java/com/youlai/boot/core/aspect/LogAspect.java
  65. 82 0
      src/main/java/com/youlai/boot/core/aspect/RepeatSubmitAspect.java
  66. 80 0
      src/main/java/com/youlai/boot/core/filter/RateLimiterFilter.java
  67. 38 0
      src/main/java/com/youlai/boot/core/filter/RequestLogFilter.java
  68. 100 0
      src/main/java/com/youlai/boot/core/handler/MyDataPermissionHandler.java
  69. 39 0
      src/main/java/com/youlai/boot/core/handler/MyMetaObjectHandler.java
  70. 25 0
      src/main/java/com/youlai/boot/core/security/exception/MyAccessDeniedHandler.java
  71. 40 0
      src/main/java/com/youlai/boot/core/security/exception/MyAuthenticationEntryPoint.java
  72. 75 0
      src/main/java/com/youlai/boot/core/security/filter/CaptchaValidationFilter.java
  73. 83 0
      src/main/java/com/youlai/boot/core/security/filter/JwtValidationFilter.java
  74. 85 0
      src/main/java/com/youlai/boot/core/security/model/SysUserDetails.java
  75. 97 0
      src/main/java/com/youlai/boot/core/security/service/PermissionService.java
  76. 49 0
      src/main/java/com/youlai/boot/core/security/service/SysUserDetailsService.java
  77. 110 0
      src/main/java/com/youlai/boot/core/security/util/JwtUtils.java
  78. 109 0
      src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java
  79. 56 0
      src/main/java/com/youlai/boot/module/auth/controller/AuthController.java
  80. 27 0
      src/main/java/com/youlai/boot/module/auth/enums/CaptchaTypeEnum.java
  81. 34 0
      src/main/java/com/youlai/boot/module/auth/service/AuthService.java
  82. 153 0
      src/main/java/com/youlai/boot/module/auth/service/impl/AuthServiceImpl.java
  83. 110 0
      src/main/java/com/youlai/boot/module/codegen/controller/CodegenController.java
  84. 39 0
      src/main/java/com/youlai/boot/module/codegen/converter/CodegenConverter.java
  85. 84 0
      src/main/java/com/youlai/boot/module/codegen/enums/FormTypeEnum.java
  86. 84 0
      src/main/java/com/youlai/boot/module/codegen/enums/JavaTypeEnum.java
  87. 73 0
      src/main/java/com/youlai/boot/module/codegen/enums/QueryTypeEnum.java
  88. 23 0
      src/main/java/com/youlai/boot/module/codegen/mapper/DatabaseMapper.java
  89. 20 0
      src/main/java/com/youlai/boot/module/codegen/mapper/GenConfigMapper.java
  90. 20 0
      src/main/java/com/youlai/boot/module/codegen/mapper/GenFieldConfigMapper.java
  91. 50 0
      src/main/java/com/youlai/boot/module/codegen/model/bo/ColumnMetaData.java
  92. 45 0
      src/main/java/com/youlai/boot/module/codegen/model/bo/TableMetaData.java
  93. 54 0
      src/main/java/com/youlai/boot/module/codegen/model/entity/GenConfig.java
  94. 106 0
      src/main/java/com/youlai/boot/module/codegen/model/entity/GenFieldConfig.java
  95. 103 0
      src/main/java/com/youlai/boot/module/codegen/model/form/GenConfigForm.java
  96. 31 0
      src/main/java/com/youlai/boot/module/codegen/model/query/TablePageQuery.java
  97. 19 0
      src/main/java/com/youlai/boot/module/codegen/model/vo/CodegenPreviewVO.java
  98. 32 0
      src/main/java/com/youlai/boot/module/codegen/model/vo/TablePageVO.java
  99. 41 0
      src/main/java/com/youlai/boot/module/codegen/service/CodegenService.java
  100. 39 0
      src/main/java/com/youlai/boot/module/codegen/service/GenConfigService.java

+ 13 - 0
.gitignore

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

+ 60 - 0
CHANGELOG.md

@@ -0,0 +1,60 @@
+# 2.7.1 (2024/4/18)
+### 🐛 fix
+- 修复用户名或者密码错误时,返回的错误信息不正确问题
+### 🛠️ refactor
+- JWT 解析和验证代码优化重构
+- 优化代码结构和完善注释,提高代码可读性
+
+# 2.7.0 (2024/4/13)
+### ✨ feat
+- 集成 Mybatis-Plus generator 代码生成器
+
+# 2.6.0 (2024/3/6)
+
+### ✨ feat
+- 黑名单方式实现 JWT 主动注销过期
+### 🛠️ refactor
+- 角色权限重构
+
+
+# 2.5.0 (2023/12/6)
+### ✨ feat
+- [集成 Spring Cache 和 Redis 缓存,路由缓存](https://blog.csdn.net/u013737132/article/details/134789862)
+### 🛠️ refactor
+- 权限判断逻辑调整,用户绑定权限调整为角色绑定权限
+### fix
+- [接口无请求权限,Spring Security 自定义异常无效问题修复](https://youlai.blog.csdn.net/article/details/134718249)
+
+
+# 2.4.1 (2023/11/7)
+### ✂️ refactor
+- 项目目录结构优化
+### ⬆️ chore
+- 升级 SpringBoot 版本 `3.1.4` → `3.1.5`
+
+
+# 2.2.1 (2023/5/25)
+
+### 🐛 fix
+
+- 修复多级路由的组件路径错误导致页面404问题
+
+# 2.2.0 (2023/5/21)
+
+### ✨ feat
+- 菜单、角色、字典、部门添加接口权限控制
+
+### 🐛 fix
+
+- 用户登录权限缓存键值不一致导致获取用户数据权限错误问题修复
+
+### ✂️ refactor
+
+- 递归获取菜单、部门属性列表代码重构优化
+
+### ⬆️ chore
+- 升级 SpringBoot 版本 `3.0.6` → `3.1.0`
+
+### 📝 docs
+- SQL 脚本更新,sys_menu 新增 `tree_path` 字段  (升级需更新SQL脚本)
+

+ 24 - 0
Dockerfile

@@ -0,0 +1,24 @@
+# 基础镜像
+FROM openjdk:17-jdk-alpine
+
+# 维护者信息
+MAINTAINER youlai <youlaitech@163.com>
+
+# 设置国内镜像源(中国科技大学镜像源),修改容器时区(alpine镜像需安装tzdata来设置时区),安装字体库(验证码)
+RUN echo -e https://mirrors.ustc.edu.cn/alpine/v3.7/main/ > /etc/apk/repositories  \
+    && apk --no-cache add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo "Asia/Shanghai" > /etc/timezone \
+    && apk --no-cache add ttf-dejavu fontconfig
+
+# 在运行时自动挂载 /tmp 目录为匿名卷,提高可移植性
+VOLUME /tmp
+
+# 将构建的 Spring Boot 可执行 JAR 复制到容器中,重命名为 app.jar
+ADD target/youlai-boot.jar app.jar
+
+# 指定容器启动时执行的命令
+CMD java \
+    -Djava.security.egd=file:/dev/./urandom \
+    -jar /app.jar
+
+# 暴露容器的端口
+EXPOSE 8989

+ 201 - 0
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 有来开源组织
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 0 - 0
README.md


+ 294 - 0
pom.xml

@@ -0,0 +1,294 @@
+<?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.ventanaly</groupId>
+    <artifactId>vpm-boot</artifactId>
+    <version>2.12.2</version>
+    <description>通风软件包管理系统</description>
+
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>3.3.2</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.27</hutool.version>
+
+        <mysql.version>8.0.28</mysql.version>
+        <druid.version>1.2.23</druid.version>
+        <mybatis-plus.version>3.5.5</mybatis-plus.version>
+
+        <knife4j.version>4.5.0</knife4j.version>
+
+        <mapstruct.version>1.5.5.Final</mapstruct.version>
+        <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
+
+        <xxl-job.version>2.4.1</xxl-job.version>
+
+        <easyexcel.version>3.2.1</easyexcel.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>3.30.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.6.4</aliyun.java.sdk.core.version>
+        <aliyun.java.sdk.dysmsapi.version>2.2.1</aliyun.java.sdk.dysmsapi.version>
+        <outDir>${user.dir}/target/dist</outDir>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <!--编译测试环境,不打包在lib-->
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-net</groupId>
+            <artifactId>commons-net</artifactId>
+            <version>3.9.0</version>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>${hutool.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>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+            <version>${mysql.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+            <version>${druid.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
+            <version>${mybatis-plus.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
+            <version>${knife4j.version}</version>
+        </dependency>
+
+        <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>
+
+        <dependency>
+            <groupId>com.xuxueli</groupId>
+            <artifactId>xxl-job-core</artifactId>
+            <version>${xxl-job.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>${easyexcel.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-validation</artifactId>
+        </dependency>
+
+        <!-- MinIO 对象存储 -->
+        <dependency>
+            <groupId>io.minio</groupId>
+            <artifactId>minio</artifactId>
+            <version>${minio.version}</version>
+        </dependency>
+
+        <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>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-generator</artifactId>
+            <version>${mybatis-plus-generator.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.velocity</groupId>
+            <artifactId>velocity-engine-core</artifactId>
+            <version>${velocity.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.lionsoul</groupId>
+            <artifactId>ip2region</artifactId>
+            <version>${ip2region.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-mail</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.jcraft</groupId>
+            <artifactId>jsch</artifactId>
+            <version>0.1.55</version>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.47</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>
+
+    </dependencies>
+
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <outputDirectory>${outDir}</outputDirectory>
+                    <includes>
+                        <include>
+                            <groupId>non-exists</groupId>
+                            <artifactId>non-exists</artifactId>
+                        </include>
+                    </includes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-dependencies</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>copy-dependencies</goal>
+                        </goals>
+                        <configuration>
+                            <!-- lib依赖包输出目录,打包的时候不打进jar包里 -->
+                            <outputDirectory>${outDir}/lib</outputDirectory>
+                            <excludeTransitive>false</excludeTransitive>
+                            <stripVersion>false</stripVersion>
+                            <includeScope>runtime</includeScope>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>3.3.0</version>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <addClasspath>true</addClasspath>
+                            <classpathPrefix>lib/</classpathPrefix>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+
+        </plugins>
+    </build>
+
+</project>

+ 549 - 0
sql/mysql5/youlai_boot.sql

@@ -0,0 +1,549 @@
+/*
+* youlai_boot 权限系统数据库(MySQL5.x)
+* @author youlai
+* @date 2024/06/24
+*/
+
+-- ----------------------------
+-- 1. 创建数据库
+-- ----------------------------
+CREATE DATABASE IF NOT EXISTS youlai_boot DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;
+
+-- ----------------------------
+-- 2. 创建表 && 数据初始化
+-- ----------------------------
+use youlai_boot;
+
+SET NAMES utf8;
+SET FOREIGN_KEY_CHECKS = 0;
+
+      -- 开启事务
+START TRANSACTION;
+
+-- ----------------------------
+-- Table structure for sys_config
+-- ----------------------------
+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(200) DEFAULT NULL COMMENT '描述、备注',
+                              `create_time` datetime NOT NULL COMMENT '创建时间',
+                              `create_by` bigint NOT NULL COMMENT '创建人ID',
+                              `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+                              `update_by` bigint DEFAULT NULL COMMENT '更新人ID',
+                              `is_deleted` tinyint(1) NOT NULL COMMENT '逻辑删除标识(0-未删除 1-已删除)',
+                              PRIMARY KEY (`id`)
+) ENGINE=InnoDB COMMENT='系统配置';
+
+-- ----------------------------
+-- 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) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '部门名称',
+                             `code` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '部门编号',
+                             `parent_id` bigint NOT NULL DEFAULT 0 COMMENT '父节点id',
+                             `tree_path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '父节点id路径',
+                             `sort` smallint NULL DEFAULT 0 COMMENT '显示顺序',
+                             `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(1-正常 0-禁用)',
+                             `create_by` bigint NULL DEFAULT NULL COMMENT '创建人ID',
+                             `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+                             `update_by` bigint NULL DEFAULT NULL COMMENT '修改人ID',
+                             `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+                             `is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '逻辑删除标识(1-已删除 0-未删除)',
+                             PRIMARY KEY (`id`) USING BTREE,
+                             UNIQUE INDEX `uk_code`(`code` ASC) USING BTREE COMMENT '部门编号唯一索引'
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '部门表' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of sys_dept
+-- ----------------------------
+INSERT INTO `sys_dept` VALUES (1, '有来技术', 'YOULAI', 0, '0', 1, 1, 1, NULL, 1, '2024-06-24 23:48:59', 0);
+INSERT INTO `sys_dept` VALUES (2, '研发部门', 'RD001', 1, '0,1', 1, 1, 2, NULL, 2, '2022-04-19 12:46:37', 0);
+INSERT INTO `sys_dept` VALUES (3, '测试部门', 'QA001', 1, '0,1', 1, 1, 2, NULL, 2, '2022-04-19 12:46:37', 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) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '字典编码',
+                            `name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '类型编码',
+                            `status` tinyint(1) DEFAULT '0' COMMENT '状态(0:正常,1:禁用)',
+                            `remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '备注',
+                            `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+                            `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+                            `is_deleted` tinyint DEFAULT '0' COMMENT '是否删除(0:未删除,1:已删除)',
+                            PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci ROW_FORMAT=DYNAMIC COMMENT='系统字典表';
+
+-- ----------------------------
+-- Records of sys_dict
+-- ----------------------------
+INSERT INTO `sys_dict` VALUES (1, 'gender', '性别', 1, NULL, now() , now(), 0);
+INSERT INTO `sys_dict` VALUES (2, 'notice_type', '通知类型', 1, NULL, now(), now(), 0);
+INSERT INTO `sys_dict` VALUES (3, 'notice_level', '通知级别', 1, NULL, now(), now(), 0);
+
+
+-- ----------------------------
+-- Table structure for sys_dict_data
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_dict_data`;
+CREATE TABLE `sys_dict_data` (
+                                 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+                                 `dict_code` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '关联字典编码,与sys_dict表中的dict_code对应',
+                                 `value` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '字典项值',
+                                 `label` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '字典项标签',
+                                 `tag_type` varchar(50) COLLATE utf8_general_ci DEFAULT NULL COMMENT '标签类型,用于前端样式展示(如success、warning等)',
+                                 `status` tinyint DEFAULT '0' COMMENT '状态(1-正常,0-禁用)',
+                                 `sort` int DEFAULT '0' COMMENT '排序',
+                                 `remark` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '备注',
+                                 `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+                                 `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+                                 PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci ROW_FORMAT=DYNAMIC COMMENT='字典数据表';
+
+-- ----------------------------
+-- Records of sys_dict_data
+-- ----------------------------
+INSERT INTO `sys_dict_data` VALUES (1, 'gender', '1', '男', 'primary', 1, 1, NULL, now(), now());
+INSERT INTO `sys_dict_data` VALUES (2, 'gender', '2', '女', 'danger', 1, 2, NULL, now(), now());
+INSERT INTO `sys_dict_data` VALUES (3, 'gender', '0', '保密', 'info', 1, 3, NULL, now(), now());
+INSERT INTO `sys_dict_data` VALUES (4, 'notice_type', '1', '系统升级', 'success', 1, 1, '', now(), now());
+INSERT INTO `sys_dict_data` VALUES (5, 'notice_type', '2', '系统维护', 'primary', 1, 2, '', now(), now());
+INSERT INTO `sys_dict_data` VALUES (6, 'notice_type', '3', '安全警告', 'danger', 1, 3, '', now(), now());
+INSERT INTO `sys_dict_data` VALUES (7, 'notice_type', '4', '假期通知', 'success', 1, 4, '', now(), now());
+INSERT INTO `sys_dict_data` VALUES (8, 'notice_type', '5', '公司新闻', 'primary', 1, 5, '', now(), now());
+INSERT INTO `sys_dict_data` VALUES (9, 'notice_type', '99', '其他', 'info', 1, 99, '', now(), now());
+INSERT INTO `sys_dict_data` VALUES (10, 'notice_level', 'L', '低', 'info', 1, 1, '', now(), now());
+INSERT INTO `sys_dict_data` VALUES (11, 'notice_level', 'M', '中', 'warning', 1, 2, '', now(), now());
+INSERT INTO `sys_dict_data` VALUES (12, 'notice_level', 'H', '高', 'danger', 1, 3, '', now(), now());
+
+-- ----------------------------
+-- Table structure for sys_log
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_log`;
+CREATE TABLE `sys_log`  (
+                            `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+                            `type` tinyint NULL DEFAULT NULL COMMENT '日志类型(1-操作日志 2-登录日志)',
+                            `title` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '日志标题',
+                            `ip` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'IP地址',
+                            `content` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '日志内容',
+                            `create_by` bigint NULL DEFAULT NULL COMMENT '创建人ID',
+                            `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+                            `update_by` bigint NULL DEFAULT NULL COMMENT '修改人ID',
+                            `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+                            `is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '逻辑删除标识(1-已删除 0-未删除)',
+                            PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '系统日志' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of sys_log
+-- ----------------------------
+
+-- ----------------------------
+-- 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) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '父节点ID路径',
+                             `name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '菜单名称',
+                             `type` tinyint NOT NULL COMMENT '菜单类型(1-菜单 2-目录 3-外链 4-按钮)',
+                             `route_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '路由名称(Vue Router 中用于命名路由)',
+                             `route_path` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '路由路径(Vue Router 中定义的 URL 路径)',
+                             `component` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '组件路径(组件页面完整路径,相对于 src/views/,缺省后缀 .vue)',
+                             `perm` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '【按钮】权限标识',
+                             `always_show` tinyint NULL DEFAULT 0 COMMENT '【目录】只有一个子路由是否始终显示(1-是 0-否)',
+                             `keep_alive` tinyint NULL DEFAULT 0 COMMENT '【菜单】是否开启页面缓存(1-是 0-否)',
+                             `visible` tinyint(1) NOT NULL DEFAULT 1 COMMENT '显示状态(1-显示 0-隐藏)',
+                             `sort` int NULL DEFAULT 0 COMMENT '排序',
+                             `icon` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '菜单图标',
+                             `redirect` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '跳转路径',
+                             `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+                             `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+                             `params` json NULL COMMENT '路由参数',
+                             PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '菜单管理' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of sys_menu
+-- ----------------------------
+INSERT INTO `sys_menu` VALUES (1, 0, '0', '系统管理', 2, '', '/system', 'Layout', NULL, NULL, NULL, 1, 1, 'system', '/system/user', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (2, 1, '0,1', '用户管理', 1, 'User', 'user', 'system/user/index', NULL, NULL, 1, 1, 1, 'el-icon-User', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (3, 1, '0,1', '角色管理', 1, 'Role', 'role', 'system/role/index', NULL, NULL, 1, 1, 2, 'role', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (4, 1, '0,1', '菜单管理', 1, 'Menu', 'menu', 'system/menu/index', NULL, NULL, 1, 1, 3, 'menu', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (5, 1, '0,1', '部门管理', 1, 'Dept', 'dept', 'system/dept/index', NULL, NULL, 1, 1, 4, 'tree', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (6, 1, '0,1', '字典管理', 1, 'Dict', 'dict', 'system/dict/index', NULL, NULL, 1, 1, 5, 'dict', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (20, 0, '0', '多级菜单', 2, NULL, '/multi-level', 'Layout', NULL, 1, NULL, 1, 9, 'cascader', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (21, 20, '0,20', '菜单一级', 1, NULL, 'multi-level1', 'demo/multi-level/level1', NULL, 1, NULL, 1, 1, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (22, 21, '0,20,21', '菜单二级', 1, NULL, 'multi-level2', 'demo/multi-level/children/level2', NULL, 0, NULL, 1, 1, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (23, 22, '0,20,21,22', '菜单三级-1', 1, NULL, 'multi-level3-1', 'demo/multi-level/children/children/level3-1', NULL, 0, 1, 1, 1, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (24, 22, '0,20,21,22', '菜单三级-2', 1, NULL, 'multi-level3-2', 'demo/multi-level/children/children/level3-2', NULL, 0, 1, 1, 2, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (26, 0, '0', '平台文档', 2, NULL, '/doc', 'Layout', NULL, NULL, NULL, 1, 8, 'document', 'https://juejin.cn/post/7228990409909108793', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (30, 26, '0,26', '平台文档(外链)', 3, NULL, 'https://juejin.cn/post/7228990409909108793', '', NULL, NULL, NULL, 1, 2, 'link', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (31, 2, '0,1,2', '用户新增', 4, NULL, '', NULL, 'sys:user:add', NULL, NULL, 1, 1, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (32, 2, '0,1,2', '用户编辑', 4, NULL, '', NULL, 'sys:user:edit', NULL, NULL, 1, 2, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (33, 2, '0,1,2', '用户删除', 4, NULL, '', NULL, 'sys:user:delete', NULL, NULL, 1, 3, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (36, 0, '0', '组件封装', 2, NULL, '/component', 'Layout', NULL, NULL, NULL, 1, 10, 'menu', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (37, 36, '0,36', '富文本编辑器', 1, NULL, 'wang-editor', 'demo/wang-editor', NULL, NULL, 1, 1, 2, '', '', NULL, NULL, NULL);
+INSERT INTO `sys_menu` VALUES (38, 36, '0,36', '图片上传', 1, NULL, 'upload', 'demo/upload', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (39, 36, '0,36', '图标选择器', 1, NULL, 'icon-selector', 'demo/icon-selector', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (40, 0, '0', '接口文档', 2, NULL, '/api', 'Layout', NULL, 1, NULL, 1, 7, 'api', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (41, 40, '0,40', 'Apifox', 1, NULL, 'apifox', 'demo/api/apifox', NULL, NULL, 1, 1, 1, 'api', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (70, 3, '0,1,3', '角色新增', 4, NULL, '', NULL, 'sys:role:add', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (71, 3, '0,1,3', '角色编辑', 4, NULL, '', NULL, 'sys:role:edit', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (72, 3, '0,1,3', '角色删除', 4, NULL, '', NULL, 'sys:role:delete', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (73, 4, '0,1,4', '菜单新增', 4, NULL, '', NULL, 'sys:menu:add', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (74, 4, '0,1,4', '菜单编辑', 4, NULL, '', NULL, 'sys:menu:edit', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (75, 4, '0,1,4', '菜单删除', 4, NULL, '', NULL, 'sys:menu:delete', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (76, 5, '0,1,5', '部门新增', 4, NULL, '', NULL, 'sys:dept:add', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (77, 5, '0,1,5', '部门编辑', 4, NULL, '', NULL, 'sys:dept:edit', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (78, 5, '0,1,5', '部门删除', 4, NULL, '', NULL, 'sys:dept:delete', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (79, 6, '0,1,6', '字典新增', 4, NULL, '', NULL, 'sys:dict:add', NULL, NULL, 1, 1, '', NULL,now(),now(), NULL);
+INSERT INTO `sys_menu` VALUES (81, 6, '0,1,6', '字典编辑', 4, NULL, '', NULL, 'sys:dict:edit', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (84, 6, '0,1,6', '字典删除', 4, NULL, '', NULL, 'sys:dict:delete', NULL, NULL, 1, 3, '', NULL,now(),now(), NULL);
+INSERT INTO `sys_menu` VALUES (88, 2, '0,1,2', '重置密码', 4, NULL, '', NULL, 'sys:user:password:reset', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (89, 0, '0', '功能演示', 2, NULL, '/function', 'Layout', NULL, NULL, NULL, 1, 12, 'menu', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (90, 89, '0,89', 'Websocket', 1, NULL, '/function/websocket', 'demo/websocket', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (91, 89, '0,89', '敬请期待...', 2, NULL, 'other/:id', 'demo/other', NULL, NULL, NULL, 1, 4, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (95, 36, '0,36', '字典组件', 1, NULL, 'dict-demo', 'demo/dict', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (97, 89, '0,89', 'Icons', 1, NULL, 'icon-demo', 'demo/icons', NULL, NULL, 1, 1, 2, 'el-icon-Notification', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (102, 26, '0,26', '平台文档(内嵌)', 3, NULL, 'internal-doc', 'demo/internal-doc', NULL, NULL, NULL, 1, 1, 'document', '', now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (105, 2, '0,1,2', '用户查询', 4, NULL, '', NULL, 'sys:user:query', 0, 0, 1, 0, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (106, 2, '0,1,2', '用户导入', 4, NULL, '', NULL, 'sys:user:import', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (107, 2, '0,1,2', '用户导出', 4, NULL, '', NULL, 'sys:user:export', NULL, NULL, 1, 6, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (108, 36, '0,36', '增删改查', 1, NULL, 'curd', 'demo/curd/index', NULL, NULL, 1, 1, 0, '', '', NULL, NULL, NULL);
+INSERT INTO `sys_menu` VALUES (109, 36, '0,36', '列表选择器', 1, NULL, 'table-select', 'demo/table-select/index', NULL, NULL, 1, 1, 1, '', '', NULL, NULL, NULL);
+INSERT INTO `sys_menu` VALUES (110, 0, '0', '路由参数', 2, NULL, '/route-param', 'Layout', NULL, 1, 1, 1, 11, 'el-icon-ElementPlus', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (111, 110, '0,110', '参数(type=1)', 1, NULL, 'route-param-type1', 'demo/route-param', NULL, 0, 1, 1, 1, 'el-icon-Star', NULL, now(), now(), '{\"type\": \"1\"}');
+INSERT INTO `sys_menu` VALUES (112, 110, '0,110', '参数(type=2)', 1, NULL, 'route-param-type2', 'demo/route-param', NULL, 0, 1, 1, 2, 'el-icon-StarFilled', NULL, now(), now(), '{\"type\": \"2\"}');
+INSERT INTO `sys_menu` VALUES (117, 1, '0,1', '系统日志', 1, 'Log', 'log', 'system/log/index', NULL, 0, 1, 1, 6, 'document', NULL, now(), now(),NULL);
+INSERT INTO `sys_menu` VALUES (118, 0, '0', '系统工具', 2, NULL, '/codegen', 'Layout', NULL, 0, 1, 1, 2, 'menu', NULL,now(),now(), NULL);
+INSERT INTO `sys_menu` VALUES (119, 118, '0,118', '代码生成', 1, 'Codegen', 'codegen', 'codegen/index', NULL, 0, 1, 1, 1, 'code', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (120,1,'0,1','系统配置',1,'Config','config','system/config/index',NULL,0,1,1,7,'setting',NULL,now(),now(),NULL);
+INSERT INTO `sys_menu` VALUES (121,120,'0,1,120','查询系统配置',4,NULL,'',NULL,'sys:config:query',0,1,1,1,'',NULL,now(),now(),NULL);
+INSERT INTO `sys_menu` VALUES (122,120,'0,1,120','新增系统配置',4,NULL,'',NULL,'sys:config:add',0,1,1,2,'',NULL,now(),now(),NULL);
+INSERT INTO `sys_menu` VALUES (123,120,'0,1,120','修改系统配置',4,NULL,'',NULL,'sys:config:update',0,1,1,3,'',NULL,now(),now(),NULL);
+INSERT INTO `sys_menu` VALUES (124,120,'0,1,120','删除系统配置',4,NULL,'',NULL,'sys:config:delete',0,1,1,4,'',NULL,now(),now(),NULL);
+INSERT INTO `sys_menu` VALUES (125,120,'0,1,120','刷新系统配置',4,NULL,'',NULL,'sys:config:refresh',0,1,1,5,'',NULL,now(),now(),NULL);
+INSERT INTO `sys_menu` VALUES (126,1,'0,1','通知公告',1,'Notice','notice','system/notice/index',NULL,NULL,NULL,1,9,'',NULL,now(),now(),NULL);
+INSERT INTO `sys_menu` VALUES (127,126,'0,1,126','查询',4,NULL,'',NULL,'sys:notice:query',NULL,NULL,1,1,'',NULL,now(),now(),NULL);
+INSERT INTO `sys_menu` VALUES (128,126,'0,1,126','新增',4,NULL,'',NULL,'sys:notice:add',NULL,NULL,1,2,'',NULL,now(),now(),NULL);
+INSERT INTO `sys_menu` VALUES (129,126,'0,1,126','编辑',4,NULL,'',NULL,'sys:notice:edit',NULL,NULL,1,3,'',NULL,now(),now(),NULL);
+INSERT INTO `sys_menu` VALUES (130,126,'0,1,126','删除',4,NULL,'',NULL,'sys:notice:delete',NULL,NULL,1,4,'',NULL,now(),now(),NULL);
+INSERT INTO `sys_menu` VALUES (133,126,'0,1,126','发布',4,NULL,'',NULL,'sys:notice:publish',0,1,1,5,'',NULL,now(),now(),NULL);
+INSERT INTO `sys_menu` VALUES (134,126,'0,1,126','撤回',4,NULL,'',NULL,'sys:notice:revoke',0,1,1,6,'',NULL,now(),now(),NULL);
+INSERT INTO `sys_menu` VALUES (135, 1, '0,1', '字典数据', 1, 'DictData', 'dict-data', 'system/dict/data', NULL, 0, 1, 0, 6, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (136, 135, '0,1,135', '字典数据新增', 4, NULL, '', NULL, 'sys:dict:add', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (137, 135, '0,1,135', '字典数据编辑', 4, NULL, '', NULL, 'sys:dict:edit', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL);
+INSERT INTO `sys_menu` VALUES (138, 135, '0,1,135', '字典数据删除', 4, NULL, '', NULL, 'sys:dict:delete', NULL, NULL, 1, 6, '', NULL, now(), now(), NULL);
+
+-- ----------------------------
+-- Table structure for sys_message
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_message`;
+CREATE TABLE `sys_message`  (
+                                `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+                                `create_by` bigint NULL DEFAULT NULL COMMENT '创建人ID',
+                                `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+                                `update_by` bigint NULL DEFAULT NULL COMMENT '修改人ID',
+                                `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+                                `is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '逻辑删除标识(1-已删除 0-未删除)',
+                                PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '系统消息' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of sys_message
+-- ----------------------------
+
+-- ----------------------------
+-- Table structure for sys_role
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_role`;
+CREATE TABLE `sys_role`  (
+                             `id` bigint NOT NULL AUTO_INCREMENT,
+                             `name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '角色名称',
+                             `code` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '角色编码',
+                             `sort` int NULL DEFAULT NULL COMMENT '显示顺序',
+                             `status` tinyint(1) NULL DEFAULT 1 COMMENT '角色状态(1-正常 0-停用)',
+                             `data_scope` tinyint NULL DEFAULT NULL COMMENT '数据权限(0-所有数据 1-部门及子部门数据 2-本部门数据3-本人数据)',
+                             `create_by` bigint NULL DEFAULT NULL COMMENT '创建人 ID',
+                             `create_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+                             `update_by` bigint NULL DEFAULT NULL COMMENT '更新人ID',
+                             `update_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+                             `is_deleted` tinyint(1) NOT NULL 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 = utf8 COLLATE = utf8_general_ci COMMENT = '角色表' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of sys_role
+-- ----------------------------
+INSERT INTO `sys_role` VALUES (1, '超级管理员', 'ROOT', 1, 1, 0, NULL, '2021-05-21 14:56:51', NULL, '2018-12-23 16:00:00', 0);
+INSERT INTO `sys_role` VALUES (2, '系统管理员', 'ADMIN', 2, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (3, '访问游客', 'GUEST', 3, 1, 2, NULL, '2021-05-26 15:49:05', NULL, '2019-05-05 16:00:00', 0);
+INSERT INTO `sys_role` VALUES (4, '系统管理员1', 'ADMIN1', 4, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (5, '系统管理员2', 'ADMIN2', 5, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (6, '系统管理员3', 'ADMIN3', 6, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (7, '系统管理员4', 'ADMIN4', 7, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (8, '系统管理员5', 'ADMIN5', 8, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (9, '系统管理员6', 'ADMIN6', 9, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (10, '系统管理员7', 'ADMIN7', 10, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (11, '系统管理员8', 'ADMIN8', 11, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+INSERT INTO `sys_role` VALUES (12, '系统管理员9', 'ADMIN9', 12, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 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 = utf8 COLLATE = utf8_general_ci COMMENT = '角色和菜单关联表' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- Records of sys_role_menu
+-- ----------------------------
+INSERT INTO `sys_role_menu` VALUES (2, 1);
+INSERT INTO `sys_role_menu` VALUES (2, 2);
+INSERT INTO `sys_role_menu` VALUES (2, 3);
+INSERT INTO `sys_role_menu` VALUES (2, 4);
+INSERT INTO `sys_role_menu` VALUES (2, 5);
+INSERT INTO `sys_role_menu` VALUES (2, 6);
+INSERT INTO `sys_role_menu` VALUES (2, 20);
+INSERT INTO `sys_role_menu` VALUES (2, 21);
+INSERT INTO `sys_role_menu` VALUES (2, 22);
+INSERT INTO `sys_role_menu` VALUES (2, 23);
+INSERT INTO `sys_role_menu` VALUES (2, 24);
+INSERT INTO `sys_role_menu` VALUES (2, 26);
+INSERT INTO `sys_role_menu` VALUES (2, 30);
+INSERT INTO `sys_role_menu` VALUES (2, 31);
+INSERT INTO `sys_role_menu` VALUES (2, 32);
+INSERT INTO `sys_role_menu` VALUES (2, 33);
+INSERT INTO `sys_role_menu` VALUES (2, 36);
+INSERT INTO `sys_role_menu` VALUES (2, 37);
+INSERT INTO `sys_role_menu` VALUES (2, 38);
+INSERT INTO `sys_role_menu` VALUES (2, 39);
+INSERT INTO `sys_role_menu` VALUES (2, 40);
+INSERT INTO `sys_role_menu` VALUES (2, 41);
+INSERT INTO `sys_role_menu` VALUES (2, 70);
+INSERT INTO `sys_role_menu` VALUES (2, 71);
+INSERT INTO `sys_role_menu` VALUES (2, 72);
+INSERT INTO `sys_role_menu` VALUES (2, 73);
+INSERT INTO `sys_role_menu` VALUES (2, 74);
+INSERT INTO `sys_role_menu` VALUES (2, 75);
+INSERT INTO `sys_role_menu` VALUES (2, 76);
+INSERT INTO `sys_role_menu` VALUES (2, 77);
+INSERT INTO `sys_role_menu` VALUES (2, 78);
+INSERT INTO `sys_role_menu` VALUES (2, 79);
+INSERT INTO `sys_role_menu` VALUES (2, 81);
+INSERT INTO `sys_role_menu` VALUES (2, 84);
+INSERT INTO `sys_role_menu` VALUES (2, 85);
+INSERT INTO `sys_role_menu` VALUES (2, 86);
+INSERT INTO `sys_role_menu` VALUES (2, 87);
+INSERT INTO `sys_role_menu` VALUES (2, 88);
+INSERT INTO `sys_role_menu` VALUES (2, 89);
+INSERT INTO `sys_role_menu` VALUES (2, 90);
+INSERT INTO `sys_role_menu` VALUES (2, 91);
+INSERT INTO `sys_role_menu` VALUES (2, 95);
+INSERT INTO `sys_role_menu` VALUES (2, 97);
+INSERT INTO `sys_role_menu` VALUES (2, 102);
+INSERT INTO `sys_role_menu` VALUES (2, 105);
+INSERT INTO `sys_role_menu` VALUES (2, 106);
+INSERT INTO `sys_role_menu` VALUES (2, 107);
+INSERT INTO `sys_role_menu` VALUES (2, 108);
+INSERT INTO `sys_role_menu` VALUES (2, 109);
+INSERT INTO `sys_role_menu` VALUES (2, 110);
+INSERT INTO `sys_role_menu` VALUES (2, 111);
+INSERT INTO `sys_role_menu` VALUES (2, 112);
+INSERT INTO `sys_role_menu` VALUES (2, 114);
+INSERT INTO `sys_role_menu` VALUES (2, 115);
+INSERT INTO `sys_role_menu` VALUES (2, 116);
+INSERT INTO `sys_role_menu` VALUES (2, 117);
+INSERT INTO `sys_role_menu` VALUES (2, 118);
+INSERT INTO `sys_role_menu` VALUES (2, 119);
+INSERT INTO `sys_role_menu` VALUES (2, 120);
+INSERT INTO `sys_role_menu` VALUES (2, 121);
+INSERT INTO `sys_role_menu` VALUES (2, 122);
+INSERT INTO `sys_role_menu` VALUES (2, 123);
+INSERT INTO `sys_role_menu` VALUES (2, 124);
+INSERT INTO `sys_role_menu` VALUES (2, 125);
+INSERT INTO `sys_role_menu` VALUES (2, 126);
+INSERT INTO `sys_role_menu` VALUES (2, 127);
+INSERT INTO `sys_role_menu` VALUES (2, 128);
+INSERT INTO `sys_role_menu` VALUES (2, 129);
+INSERT INTO `sys_role_menu` VALUES (2, 130);
+INSERT INTO `sys_role_menu` VALUES (2, 131);
+INSERT INTO `sys_role_menu` VALUES (2, 132);
+INSERT INTO `sys_role_menu` VALUES (2, 133);
+INSERT INTO `sys_role_menu` VALUES (2, 134);
+INSERT INTO `sys_role_menu` VALUES (2, 135);
+INSERT INTO `sys_role_menu` VALUES (2, 136);
+INSERT INTO `sys_role_menu` VALUES (2, 137);
+INSERT INTO `sys_role_menu` VALUES (2, 138);
+-- ----------------------------
+-- Table structure for sys_user
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_user`;
+CREATE TABLE `sys_user`  (
+                             `id` int NOT NULL AUTO_INCREMENT,
+                             `username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户名',
+                             `nickname` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',
+                             `gender` tinyint(1) NULL DEFAULT 1 COMMENT '性别((1-男 2-女 0-保密)',
+                             `password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
+                             `dept_id` int NULL DEFAULT NULL COMMENT '部门ID',
+                             `avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '用户头像',
+                             `mobile` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '联系方式',
+                             `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态((1-正常 0-禁用)',
+                             `email` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户邮箱',
+                             `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+                             `create_by` bigint NULL DEFAULT NULL COMMENT '创建人ID',
+                             `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+                             `update_by` bigint NULL DEFAULT NULL COMMENT '修改人ID',
+                             `is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)',
+                             PRIMARY KEY (`id`) USING BTREE,
+                             UNIQUE INDEX `login_name`(`username` ASC) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户信息表' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- 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', '18866668888', 1, 'youlaitech@163.com', NULL, NULL, NULL, 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', '18866668887', 1, '', '2019-10-10 13:41:22', NULL, '2022-07-31 12:39:30', NULL, 0);
+INSERT INTO `sys_user` VALUES (3, 'websocket', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668886', 1, 'youlaitech@163.com', '2021-06-05 01:31:29', NULL, '2021-06-05 01:31:29', 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,
+                                  UNIQUE INDEX `uk_userid_roleid`(`user_id` ASC, `role_id` ASC) USING BTREE COMMENT '用户角色唯一索引'
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户和角色关联表' ROW_FORMAT = DYNAMIC;
+
+-- ----------------------------
+-- 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);
+
+
+-- ----------------------------
+-- Table structure for sys_log
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_log`;
+CREATE TABLE `sys_log` (
+                           `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+                           `module` enum('LOGIN','USER','ROLE','DEPT','MENU','DICT','OTHER') CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '日志模块',
+                           `content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '日志内容',
+                           `request_uri` varchar(255) COLLATE utf8_general_ci DEFAULT NULL COMMENT '请求路径',
+                           `ip` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'IP地址',
+                           `province` varchar(100) COLLATE utf8_general_ci DEFAULT NULL COMMENT '省份',
+                           `city` varchar(100) COLLATE utf8_general_ci DEFAULT NULL COMMENT '城市',
+                           `execution_time` bigint DEFAULT NULL COMMENT '执行时间(ms)',
+                           `browser` varchar(100) COLLATE utf8_general_ci DEFAULT NULL COMMENT '浏览器',
+                           `browser_version` varchar(100) COLLATE utf8_general_ci DEFAULT NULL COMMENT '浏览器版本',
+                           `os` varchar(100) COLLATE utf8_general_ci DEFAULT NULL COMMENT '终端系统',
+                           `create_by` bigint DEFAULT NULL COMMENT '创建人ID',
+                           `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+                           `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除标识(1-已删除 0-未删除)',
+                           PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci ROW_FORMAT=DYNAMIC COMMENT='系统日志表';
+
+
+-- ----------------------------
+-- Table structure for gen_config
+-- ----------------------------
+DROP TABLE IF EXISTS `gen_config`;
+CREATE TABLE `gen_config` (
+                              `id` bigint NOT NULL AUTO_INCREMENT,
+                              `table_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_0900_ai_ci NOT NULL COMMENT '表名',
+                              `module_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_0900_ai_ci DEFAULT NULL COMMENT '模块名',
+                              `package_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_0900_ai_ci NOT NULL COMMENT '包名',
+                              `business_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_0900_ai_ci NOT NULL COMMENT '业务名',
+                              `entity_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_0900_ai_ci NOT NULL COMMENT '实体类名',
+                              `author` varchar(50) CHARACTER SET utf8 COLLATE utf8_0900_ai_ci NOT NULL COMMENT '作者',
+                              `parent_menu_id` bigint DEFAULT NULL COMMENT '上级菜单ID,对应sys_menu的id ',
+                              `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+                              `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+                              `is_deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+                              PRIMARY KEY (`id`),
+                              UNIQUE KEY `uk_tablename` (`table_name`)
+) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='代码生成基础配置表';
+
+-- ----------------------------
+-- Table structure for gen_field_config
+-- ----------------------------
+DROP TABLE IF EXISTS `gen_field_config`;
+CREATE TABLE `gen_field_config` (
+                                    `id` bigint NOT NULL AUTO_INCREMENT,
+                                    `config_id` bigint NOT NULL COMMENT '关联的配置ID',
+                                    `column_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_0900_ai_ci DEFAULT NULL,
+                                    `column_type` varchar(50) CHARACTER SET utf8 COLLATE utf8_0900_ai_ci DEFAULT NULL,
+                                    `column_length` int DEFAULT NULL,
+                                    `field_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_0900_ai_ci NOT NULL COMMENT '字段名称',
+                                    `field_type` varchar(100) CHARACTER SET utf8 COLLATE utf8_0900_ai_ci DEFAULT NULL COMMENT '字段类型',
+                                    `field_sort` int DEFAULT NULL COMMENT '字段排序',
+                                    `field_comment` varchar(255) CHARACTER SET utf8 COLLATE utf8_0900_ai_ci DEFAULT NULL COMMENT '字段描述',
+                                    `max_length` int NULL DEFAULT NULL,
+                                    `is_required` tinyint(1) DEFAULT NULL 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 DEFAULT NULL COMMENT '查询方式',
+                                    `form_type` tinyint DEFAULT NULL COMMENT '表单类型',
+                                    `dict_type` varchar(50) CHARACTER SET utf8 COLLATE utf8_0900_ai_ci NULL DEFAULT NULL COMMENT '字典类型',
+                                    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+                                    `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+                                    PRIMARY KEY (`id`),
+                                    KEY `config_id` (`config_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='代码生成字段配置表';
+
+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 NOT NULL DEFAULT '0' COMMENT '读取状态(0未读;1已读)',
+                                   `read_time` datetime DEFAULT NULL COMMENT '阅读时间',
+                                   `create_time` datetime NOT NULL COMMENT '创建时间',
+                                   `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+                                   `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除(1-已删除;0-未删除)',
+                                   PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='用户通知公告表';
+
+CREATE TABLE `sys_notice` (
+                              `id` bigint NOT NULL AUTO_INCREMENT,
+                              `title` varchar(50) CHARACTER SET utf8 COLLATE utf8_0900_ai_ci DEFAULT NULL COMMENT '通知标题',
+                              `content` text CHARACTER SET utf8 COLLATE utf8_0900_ai_ci COMMENT '通知内容',
+                              `type` tinyint NOT NULL COMMENT '通知类型(字典code:notice_type)',
+                              `level` varchar(5) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '通知等级(字典code:notice_level)',
+                              `target_type` tinyint NOT NULL COMMENT '目标类型(1: 全体, 2: 指定)',
+                              `target_user_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_0900_ai_ci DEFAULT NULL COMMENT '目标人ID集合(多个使用英文逗号,分割)',
+                              `publisher_id` bigint DEFAULT NULL COMMENT '发布人ID',
+                              `publish_status` tinyint NOT NULL DEFAULT '0' COMMENT '发布状态(0: 未发布, 1: 已发布, -1: 已撤回)',
+                              `publish_time` datetime DEFAULT NULL COMMENT '发布时间',
+                              `revoke_time` datetime DEFAULT NULL COMMENT '撤回时间',
+                              `create_by` bigint NOT NULL COMMENT '创建人ID',
+                              `create_time` datetime NOT NULL COMMENT '创建时间',
+                              `update_by` bigint DEFAULT NULL COMMENT '更新人ID',
+                              `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+                              `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除(0: 未删除, 1: 已删除)',
+                              PRIMARY KEY (`id`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci COMMENT='通知公告表';
+
+SET FOREIGN_KEY_CHECKS = 1;
+
+     -- 提交事务
+COMMIT;

+ 550 - 0
sql/mysql8/youlai_boot.sql

@@ -0,0 +1,550 @@
+    /*
+    * youlai_boot 权限系统数据库(MySQL8.x)
+    * @author youlai
+    * @date 2024/06/24
+    */
+
+    -- ----------------------------
+    -- 1. 创建数据库
+    -- ----------------------------
+    CREATE DATABASE IF NOT EXISTS youlai_boot DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;
+
+
+    -- ----------------------------
+    -- 2. 创建表 && 数据初始化
+    -- ----------------------------
+    use youlai_boot;
+
+    SET NAMES utf8mb4;
+    SET FOREIGN_KEY_CHECKS = 0;
+
+      -- 开启事务
+    START TRANSACTION;
+
+    -- ----------------------------
+    -- Table structure for sys_config
+    -- ----------------------------
+    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(200) DEFAULT NULL COMMENT '描述、备注',
+          `create_time` datetime NOT NULL COMMENT '创建时间',
+          `create_by` bigint NOT NULL COMMENT '创建人ID',
+          `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+          `update_by` bigint DEFAULT NULL COMMENT '更新人ID',
+          `is_deleted` tinyint(1) NOT NULL COMMENT '逻辑删除标识(0-未删除 1-已删除)',
+          PRIMARY KEY (`id`)
+    ) ENGINE=InnoDB COMMENT='系统配置';
+
+    -- ----------------------------
+    -- 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) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '部门名称',
+                                 `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '部门编号',
+                                 `parent_id` bigint NOT NULL DEFAULT 0 COMMENT '父节点id',
+                                 `tree_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '父节点id路径',
+                                 `sort` smallint NULL DEFAULT 0 COMMENT '显示顺序',
+                                 `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态(1-正常 0-禁用)',
+                                 `create_by` bigint NULL DEFAULT NULL COMMENT '创建人ID',
+                                 `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+                                 `update_by` bigint NULL DEFAULT NULL COMMENT '修改人ID',
+                                 `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+                                 `is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '逻辑删除标识(1-已删除 0-未删除)',
+                                 PRIMARY KEY (`id`) USING BTREE,
+                                 UNIQUE INDEX `uk_code`(`code` ASC) USING BTREE COMMENT '部门编号唯一索引'
+    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '部门表' ROW_FORMAT = DYNAMIC;
+
+    -- ----------------------------
+    -- Records of sys_dept
+    -- ----------------------------
+    INSERT INTO `sys_dept` VALUES (1, '有来技术', 'YOULAI', 0, '0', 1, 1, 1, NULL, 1, '2024-06-24 23:48:59', 0);
+    INSERT INTO `sys_dept` VALUES (2, '研发部门', 'RD001', 1, '0,1', 1, 1, 2, NULL, 2, '2022-04-19 12:46:37', 0);
+    INSERT INTO `sys_dept` VALUES (3, '测试部门', 'QA001', 1, '0,1', 1, 1, 2, NULL, 2, '2022-04-19 12:46:37', 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) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '字典编码',
+                                `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '类型编码',
+                                `status` tinyint(1) DEFAULT '0' COMMENT '状态(0:正常,1:禁用)',
+                                `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注',
+                                `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+                                `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+                                `is_deleted` tinyint DEFAULT '0' COMMENT '是否删除(0:未删除,1:已删除)',
+                                PRIMARY KEY (`id`) USING BTREE
+    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='系统字典表';
+
+    -- ----------------------------
+    -- Records of sys_dict
+    -- ----------------------------
+    INSERT INTO `sys_dict` VALUES (1, 'gender', '性别', 1, NULL, now() , now(), 0);
+    INSERT INTO `sys_dict` VALUES (2, 'notice_type', '通知类型', 1, NULL, now(), now(), 0);
+    INSERT INTO `sys_dict` VALUES (3, 'notice_level', '通知级别', 1, NULL, now(), now(), 0);
+
+
+    -- ----------------------------
+    -- Table structure for sys_dict_data
+    -- ----------------------------
+    DROP TABLE IF EXISTS `sys_dict_data`;
+    CREATE TABLE `sys_dict_data` (
+                                     `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+                                     `dict_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '关联字典编码,与sys_dict表中的dict_code对应',
+                                     `value` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '字典项值',
+                                     `label` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '字典项标签',
+                                     `tag_type` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '标签类型,用于前端样式展示(如success、warning等)',
+                                     `status` tinyint DEFAULT '0' COMMENT '状态(1-正常,0-禁用)',
+                                     `sort` int DEFAULT '0' COMMENT '排序',
+                                     `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '备注',
+                                     `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+                                     `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+                                     PRIMARY KEY (`id`) USING BTREE
+    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='字典数据表';
+
+    -- ----------------------------
+    -- Records of sys_dict_data
+    -- ----------------------------
+    INSERT INTO `sys_dict_data` VALUES (1, 'gender', '1', '男', 'primary', 1, 1, NULL, now(), now());
+    INSERT INTO `sys_dict_data` VALUES (2, 'gender', '2', '女', 'danger', 1, 2, NULL, now(), now());
+    INSERT INTO `sys_dict_data` VALUES (3, 'gender', '0', '保密', 'info', 1, 3, NULL, now(), now());
+    INSERT INTO `sys_dict_data` VALUES (4, 'notice_type', '1', '系统升级', 'success', 1, 1, '', now(), now());
+    INSERT INTO `sys_dict_data` VALUES (5, 'notice_type', '2', '系统维护', 'primary', 1, 2, '', now(), now());
+    INSERT INTO `sys_dict_data` VALUES (6, 'notice_type', '3', '安全警告', 'danger', 1, 3, '', now(), now());
+    INSERT INTO `sys_dict_data` VALUES (7, 'notice_type', '4', '假期通知', 'success', 1, 4, '', now(), now());
+    INSERT INTO `sys_dict_data` VALUES (8, 'notice_type', '5', '公司新闻', 'primary', 1, 5, '', now(), now());
+    INSERT INTO `sys_dict_data` VALUES (9, 'notice_type', '99', '其他', 'info', 1, 99, '', now(), now());
+    INSERT INTO `sys_dict_data` VALUES (10, 'notice_level', 'L', '低', 'info', 1, 1, '', now(), now());
+    INSERT INTO `sys_dict_data` VALUES (11, 'notice_level', 'M', '中', 'warning', 1, 2, '', now(), now());
+    INSERT INTO `sys_dict_data` VALUES (12, 'notice_level', 'H', '高', 'danger', 1, 3, '', now(), now());
+
+    -- ----------------------------
+    -- Table structure for sys_log
+    -- ----------------------------
+    DROP TABLE IF EXISTS `sys_log`;
+    CREATE TABLE `sys_log`  (
+                                `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+                                `type` tinyint NULL DEFAULT NULL COMMENT '日志类型(1-操作日志 2-登录日志)',
+                                `title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '日志标题',
+                                `ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'IP地址',
+                                `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '日志内容',
+                                `create_by` bigint NULL DEFAULT NULL COMMENT '创建人ID',
+                                `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+                                `update_by` bigint NULL DEFAULT NULL COMMENT '修改人ID',
+                                `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+                                `is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '逻辑删除标识(1-已删除 0-未删除)',
+                                PRIMARY KEY (`id`) USING BTREE
+    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统日志' ROW_FORMAT = DYNAMIC;
+
+    -- ----------------------------
+    -- Records of sys_log
+    -- ----------------------------
+
+    -- ----------------------------
+    -- 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) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '父节点ID路径',
+                                 `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '菜单名称',
+                                 `type` tinyint NOT NULL COMMENT '菜单类型(1-菜单 2-目录 3-外链 4-按钮)',
+                                 `route_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '路由名称(Vue Router 中用于命名路由)',
+                                 `route_path` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '路由路径(Vue Router 中定义的 URL 路径)',
+                                 `component` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组件路径(组件页面完整路径,相对于 src/views/,缺省后缀 .vue)',
+                                 `perm` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '【按钮】权限标识',
+                                 `always_show` tinyint NULL DEFAULT 0 COMMENT '【目录】只有一个子路由是否始终显示(1-是 0-否)',
+                                 `keep_alive` tinyint NULL DEFAULT 0 COMMENT '【菜单】是否开启页面缓存(1-是 0-否)',
+                                 `visible` tinyint(1) NOT NULL DEFAULT 1 COMMENT '显示状态(1-显示 0-隐藏)',
+                                 `sort` int NULL DEFAULT 0 COMMENT '排序',
+                                 `icon` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '菜单图标',
+                                 `redirect` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '跳转路径',
+                                 `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+                                 `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+                                 `params` json NULL COMMENT '路由参数',
+                                 PRIMARY KEY (`id`) USING BTREE
+    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '菜单管理' ROW_FORMAT = DYNAMIC;
+
+    -- ----------------------------
+    -- Records of sys_menu
+    -- ----------------------------
+    INSERT INTO `sys_menu` VALUES (1, 0, '0', '系统管理', 2, '', '/system', 'Layout', NULL, NULL, NULL, 1, 1, 'system', '/system/user', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (2, 1, '0,1', '用户管理', 1, 'User', 'user', 'system/user/index', NULL, NULL, 1, 1, 1, 'el-icon-User', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (3, 1, '0,1', '角色管理', 1, 'Role', 'role', 'system/role/index', NULL, NULL, 1, 1, 2, 'role', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (4, 1, '0,1', '菜单管理', 1, 'Menu', 'menu', 'system/menu/index', NULL, NULL, 1, 1, 3, 'menu', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (5, 1, '0,1', '部门管理', 1, 'Dept', 'dept', 'system/dept/index', NULL, NULL, 1, 1, 4, 'tree', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (6, 1, '0,1', '字典管理', 1, 'Dict', 'dict', 'system/dict/index', NULL, NULL, 1, 1, 5, 'dict', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (20, 0, '0', '多级菜单', 2, NULL, '/multi-level', 'Layout', NULL, 1, NULL, 1, 9, 'cascader', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (21, 20, '0,20', '菜单一级', 1, NULL, 'multi-level1', 'demo/multi-level/level1', NULL, 1, NULL, 1, 1, '', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (22, 21, '0,20,21', '菜单二级', 1, NULL, 'multi-level2', 'demo/multi-level/children/level2', NULL, 0, NULL, 1, 1, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (23, 22, '0,20,21,22', '菜单三级-1', 1, NULL, 'multi-level3-1', 'demo/multi-level/children/children/level3-1', NULL, 0, 1, 1, 1, '', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (24, 22, '0,20,21,22', '菜单三级-2', 1, NULL, 'multi-level3-2', 'demo/multi-level/children/children/level3-2', NULL, 0, 1, 1, 2, '', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (26, 0, '0', '平台文档', 2, NULL, '/doc', 'Layout', NULL, NULL, NULL, 1, 8, 'document', 'https://juejin.cn/post/7228990409909108793', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (30, 26, '0,26', '平台文档(外链)', 3, NULL, 'https://juejin.cn/post/7228990409909108793', '', NULL, NULL, NULL, 1, 2, 'link', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (31, 2, '0,1,2', '用户新增', 4, NULL, '', NULL, 'sys:user:add', NULL, NULL, 1, 1, '', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (32, 2, '0,1,2', '用户编辑', 4, NULL, '', NULL, 'sys:user:edit', NULL, NULL, 1, 2, '', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (33, 2, '0,1,2', '用户删除', 4, NULL, '', NULL, 'sys:user:delete', NULL, NULL, 1, 3, '', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (36, 0, '0', '组件封装', 2, NULL, '/component', 'Layout', NULL, NULL, NULL, 1, 10, 'menu', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (37, 36, '0,36', '富文本编辑器', 1, NULL, 'wang-editor', 'demo/wang-editor', NULL, NULL, 1, 1, 2, '', '', NULL, NULL, NULL);
+    INSERT INTO `sys_menu` VALUES (38, 36, '0,36', '图片上传', 1, NULL, 'upload', 'demo/upload', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (39, 36, '0,36', '图标选择器', 1, NULL, 'icon-selector', 'demo/icon-selector', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (40, 0, '0', '接口文档', 2, NULL, '/api', 'Layout', NULL, 1, NULL, 1, 7, 'api', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (41, 40, '0,40', 'Apifox', 1, NULL, 'apifox', 'demo/api/apifox', NULL, NULL, 1, 1, 1, 'api', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (70, 3, '0,1,3', '角色新增', 4, NULL, '', NULL, 'sys:role:add', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (71, 3, '0,1,3', '角色编辑', 4, NULL, '', NULL, 'sys:role:edit', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (72, 3, '0,1,3', '角色删除', 4, NULL, '', NULL, 'sys:role:delete', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (73, 4, '0,1,4', '菜单新增', 4, NULL, '', NULL, 'sys:menu:add', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (74, 4, '0,1,4', '菜单编辑', 4, NULL, '', NULL, 'sys:menu:edit', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (75, 4, '0,1,4', '菜单删除', 4, NULL, '', NULL, 'sys:menu:delete', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (76, 5, '0,1,5', '部门新增', 4, NULL, '', NULL, 'sys:dept:add', NULL, NULL, 1, 1, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (77, 5, '0,1,5', '部门编辑', 4, NULL, '', NULL, 'sys:dept:edit', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (78, 5, '0,1,5', '部门删除', 4, NULL, '', NULL, 'sys:dept:delete', NULL, NULL, 1, 3, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (79, 6, '0,1,6', '字典新增', 4, NULL, '', NULL, 'sys:dict:add', NULL, NULL, 1, 1, '', NULL,now(),now(), NULL);
+    INSERT INTO `sys_menu` VALUES (81, 6, '0,1,6', '字典编辑', 4, NULL, '', NULL, 'sys:dict:edit', NULL, NULL, 1, 2, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (84, 6, '0,1,6', '字典删除', 4, NULL, '', NULL, 'sys:dict:delete', NULL, NULL, 1, 3, '', NULL,now(),now(), NULL);
+    INSERT INTO `sys_menu` VALUES (88, 2, '0,1,2', '重置密码', 4, NULL, '', NULL, 'sys:user:password:reset', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (89, 0, '0', '功能演示', 2, NULL, '/function', 'Layout', NULL, NULL, NULL, 1, 12, 'menu', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (90, 89, '0,89', 'Websocket', 1, NULL, '/function/websocket', 'demo/websocket', NULL, NULL, 1, 1, 3, '', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (91, 89, '0,89', '敬请期待...', 2, NULL, 'other/:id', 'demo/other', NULL, NULL, NULL, 1, 4, '', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (95, 36, '0,36', '字典组件', 1, NULL, 'dict-demo', 'demo/dict', NULL, NULL, 1, 1, 4, '', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (97, 89, '0,89', 'Icons', 1, NULL, 'icon-demo', 'demo/icons', NULL, NULL, 1, 1, 2, 'el-icon-Notification', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (102, 26, '0,26', '平台文档(内嵌)', 3, NULL, 'internal-doc', 'demo/internal-doc', NULL, NULL, NULL, 1, 1, 'document', '', now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (105, 2, '0,1,2', '用户查询', 4, NULL, '', NULL, 'sys:user:query', 0, 0, 1, 0, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (106, 2, '0,1,2', '用户导入', 4, NULL, '', NULL, 'sys:user:import', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (107, 2, '0,1,2', '用户导出', 4, NULL, '', NULL, 'sys:user:export', NULL, NULL, 1, 6, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (108, 36, '0,36', '增删改查', 1, NULL, 'curd', 'demo/curd/index', NULL, NULL, 1, 1, 0, '', '', NULL, NULL, NULL);
+    INSERT INTO `sys_menu` VALUES (109, 36, '0,36', '列表选择器', 1, NULL, 'table-select', 'demo/table-select/index', NULL, NULL, 1, 1, 1, '', '', NULL, NULL, NULL);
+    INSERT INTO `sys_menu` VALUES (110, 0, '0', '路由参数', 2, NULL, '/route-param', 'Layout', NULL, 1, 1, 1, 11, 'el-icon-ElementPlus', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (111, 110, '0,110', '参数(type=1)', 1, NULL, 'route-param-type1', 'demo/route-param', NULL, 0, 1, 1, 1, 'el-icon-Star', NULL, now(), now(), '{\"type\": \"1\"}');
+    INSERT INTO `sys_menu` VALUES (112, 110, '0,110', '参数(type=2)', 1, NULL, 'route-param-type2', 'demo/route-param', NULL, 0, 1, 1, 2, 'el-icon-StarFilled', NULL, now(), now(), '{\"type\": \"2\"}');
+    INSERT INTO `sys_menu` VALUES (117, 1, '0,1', '系统日志', 1, 'Log', 'log', 'system/log/index', NULL, 0, 1, 1, 6, 'document', NULL, now(), now(),NULL);
+    INSERT INTO `sys_menu` VALUES (118, 0, '0', '系统工具', 2, NULL, '/codegen', 'Layout', NULL, 0, 1, 1, 2, 'menu', NULL,now(),now(), NULL);
+    INSERT INTO `sys_menu` VALUES (119, 118, '0,118', '代码生成', 1, 'Codegen', 'codegen', 'codegen/index', NULL, 0, 1, 1, 1, 'code', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (120,1,'0,1','系统配置',1,'Config','config','system/config/index',NULL,0,1,1,7,'setting',NULL,now(),now(),NULL);
+    INSERT INTO `sys_menu` VALUES (121,120,'0,1,120','查询系统配置',4,NULL,'',NULL,'sys:config:query',0,1,1,1,'',NULL,now(),now(),NULL);
+    INSERT INTO `sys_menu` VALUES (122,120,'0,1,120','新增系统配置',4,NULL,'',NULL,'sys:config:add',0,1,1,2,'',NULL,now(),now(),NULL);
+    INSERT INTO `sys_menu` VALUES (123,120,'0,1,120','修改系统配置',4,NULL,'',NULL,'sys:config:update',0,1,1,3,'',NULL,now(),now(),NULL);
+    INSERT INTO `sys_menu` VALUES (124,120,'0,1,120','删除系统配置',4,NULL,'',NULL,'sys:config:delete',0,1,1,4,'',NULL,now(),now(),NULL);
+    INSERT INTO `sys_menu` VALUES (125,120,'0,1,120','刷新系统配置',4,NULL,'',NULL,'sys:config:refresh',0,1,1,5,'',NULL,now(),now(),NULL);
+    INSERT INTO `sys_menu` VALUES (126,1,'0,1','通知公告',1,'Notice','notice','system/notice/index',NULL,NULL,NULL,1,9,'',NULL,now(),now(),NULL);
+    INSERT INTO `sys_menu` VALUES (127,126,'0,1,126','查询',4,NULL,'',NULL,'sys:notice:query',NULL,NULL,1,1,'',NULL,now(),now(),NULL);
+    INSERT INTO `sys_menu` VALUES (128,126,'0,1,126','新增',4,NULL,'',NULL,'sys:notice:add',NULL,NULL,1,2,'',NULL,now(),now(),NULL);
+    INSERT INTO `sys_menu` VALUES (129,126,'0,1,126','编辑',4,NULL,'',NULL,'sys:notice:edit',NULL,NULL,1,3,'',NULL,now(),now(),NULL);
+    INSERT INTO `sys_menu` VALUES (130,126,'0,1,126','删除',4,NULL,'',NULL,'sys:notice:delete',NULL,NULL,1,4,'',NULL,now(),now(),NULL);
+    INSERT INTO `sys_menu` VALUES (133,126,'0,1,126','发布',4,NULL,'',NULL,'sys:notice:publish',0,1,1,5,'',NULL,now(),now(),NULL);
+    INSERT INTO `sys_menu` VALUES (134,126,'0,1,126','撤回',4,NULL,'',NULL,'sys:notice:revoke',0,1,1,6,'',NULL,now(),now(),NULL);
+    INSERT INTO `sys_menu` VALUES (135, 1, '0,1', '字典数据', 1, 'DictData', 'dict-data', 'system/dict/data', NULL, 0, 1, 0, 6, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (136, 135, '0,1,135', '字典数据新增', 4, NULL, '', NULL, 'sys:dict:add', NULL, NULL, 1, 4, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (137, 135, '0,1,135', '字典数据编辑', 4, NULL, '', NULL, 'sys:dict:edit', NULL, NULL, 1, 5, '', NULL, now(), now(), NULL);
+    INSERT INTO `sys_menu` VALUES (138, 135, '0,1,135', '字典数据删除', 4, NULL, '', NULL, 'sys:dict:delete', NULL, NULL, 1, 6, '', NULL, now(), now(), NULL);
+
+    -- ----------------------------
+    -- Table structure for sys_message
+    -- ----------------------------
+    DROP TABLE IF EXISTS `sys_message`;
+    CREATE TABLE `sys_message`  (
+                                    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+                                    `create_by` bigint NULL DEFAULT NULL COMMENT '创建人ID',
+                                    `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+                                    `update_by` bigint NULL DEFAULT NULL COMMENT '修改人ID',
+                                    `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+                                    `is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '逻辑删除标识(1-已删除 0-未删除)',
+                                    PRIMARY KEY (`id`) USING BTREE
+    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统消息' ROW_FORMAT = DYNAMIC;
+
+    -- ----------------------------
+    -- Records of sys_message
+    -- ----------------------------
+
+    -- ----------------------------
+    -- Table structure for sys_role
+    -- ----------------------------
+    DROP TABLE IF EXISTS `sys_role`;
+    CREATE TABLE `sys_role`  (
+                                 `id` bigint NOT NULL AUTO_INCREMENT,
+                                 `name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '角色名称',
+                                 `code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色编码',
+                                 `sort` int NULL DEFAULT NULL COMMENT '显示顺序',
+                                 `status` tinyint(1) NULL DEFAULT 1 COMMENT '角色状态(1-正常 0-停用)',
+                                 `data_scope` tinyint NULL DEFAULT NULL COMMENT '数据权限(0-所有数据 1-部门及子部门数据 2-本部门数据3-本人数据)',
+                                 `create_by` bigint NULL DEFAULT NULL COMMENT '创建人 ID',
+                                 `create_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+                                 `update_by` bigint NULL DEFAULT NULL COMMENT '更新人ID',
+                                 `update_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+                                 `is_deleted` tinyint(1) NOT NULL 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 COLLATE = utf8mb4_general_ci COMMENT = '角色表' ROW_FORMAT = DYNAMIC;
+
+    -- ----------------------------
+    -- Records of sys_role
+    -- ----------------------------
+    INSERT INTO `sys_role` VALUES (1, '超级管理员', 'ROOT', 1, 1, 0, NULL, '2021-05-21 14:56:51', NULL, '2018-12-23 16:00:00', 0);
+    INSERT INTO `sys_role` VALUES (2, '系统管理员', 'ADMIN', 2, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+    INSERT INTO `sys_role` VALUES (3, '访问游客', 'GUEST', 3, 1, 2, NULL, '2021-05-26 15:49:05', NULL, '2019-05-05 16:00:00', 0);
+    INSERT INTO `sys_role` VALUES (4, '系统管理员1', 'ADMIN1', 4, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+    INSERT INTO `sys_role` VALUES (5, '系统管理员2', 'ADMIN2', 5, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+    INSERT INTO `sys_role` VALUES (6, '系统管理员3', 'ADMIN3', 6, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+    INSERT INTO `sys_role` VALUES (7, '系统管理员4', 'ADMIN4', 7, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+    INSERT INTO `sys_role` VALUES (8, '系统管理员5', 'ADMIN5', 8, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+    INSERT INTO `sys_role` VALUES (9, '系统管理员6', 'ADMIN6', 9, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+    INSERT INTO `sys_role` VALUES (10, '系统管理员7', 'ADMIN7', 10, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+    INSERT INTO `sys_role` VALUES (11, '系统管理员8', 'ADMIN8', 11, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 0);
+    INSERT INTO `sys_role` VALUES (12, '系统管理员9', 'ADMIN9', 12, 1, 1, NULL, '2021-03-25 12:39:54', NULL, NULL, 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 COLLATE = utf8mb4_general_ci COMMENT = '角色和菜单关联表' ROW_FORMAT = DYNAMIC;
+
+    -- ----------------------------
+    -- Records of sys_role_menu
+    -- ----------------------------
+    INSERT INTO `sys_role_menu` VALUES (2, 1);
+    INSERT INTO `sys_role_menu` VALUES (2, 2);
+    INSERT INTO `sys_role_menu` VALUES (2, 3);
+    INSERT INTO `sys_role_menu` VALUES (2, 4);
+    INSERT INTO `sys_role_menu` VALUES (2, 5);
+    INSERT INTO `sys_role_menu` VALUES (2, 6);
+    INSERT INTO `sys_role_menu` VALUES (2, 20);
+    INSERT INTO `sys_role_menu` VALUES (2, 21);
+    INSERT INTO `sys_role_menu` VALUES (2, 22);
+    INSERT INTO `sys_role_menu` VALUES (2, 23);
+    INSERT INTO `sys_role_menu` VALUES (2, 24);
+    INSERT INTO `sys_role_menu` VALUES (2, 26);
+    INSERT INTO `sys_role_menu` VALUES (2, 30);
+    INSERT INTO `sys_role_menu` VALUES (2, 31);
+    INSERT INTO `sys_role_menu` VALUES (2, 32);
+    INSERT INTO `sys_role_menu` VALUES (2, 33);
+    INSERT INTO `sys_role_menu` VALUES (2, 36);
+    INSERT INTO `sys_role_menu` VALUES (2, 37);
+    INSERT INTO `sys_role_menu` VALUES (2, 38);
+    INSERT INTO `sys_role_menu` VALUES (2, 39);
+    INSERT INTO `sys_role_menu` VALUES (2, 40);
+    INSERT INTO `sys_role_menu` VALUES (2, 41);
+    INSERT INTO `sys_role_menu` VALUES (2, 70);
+    INSERT INTO `sys_role_menu` VALUES (2, 71);
+    INSERT INTO `sys_role_menu` VALUES (2, 72);
+    INSERT INTO `sys_role_menu` VALUES (2, 73);
+    INSERT INTO `sys_role_menu` VALUES (2, 74);
+    INSERT INTO `sys_role_menu` VALUES (2, 75);
+    INSERT INTO `sys_role_menu` VALUES (2, 76);
+    INSERT INTO `sys_role_menu` VALUES (2, 77);
+    INSERT INTO `sys_role_menu` VALUES (2, 78);
+    INSERT INTO `sys_role_menu` VALUES (2, 79);
+    INSERT INTO `sys_role_menu` VALUES (2, 81);
+    INSERT INTO `sys_role_menu` VALUES (2, 84);
+    INSERT INTO `sys_role_menu` VALUES (2, 85);
+    INSERT INTO `sys_role_menu` VALUES (2, 86);
+    INSERT INTO `sys_role_menu` VALUES (2, 87);
+    INSERT INTO `sys_role_menu` VALUES (2, 88);
+    INSERT INTO `sys_role_menu` VALUES (2, 89);
+    INSERT INTO `sys_role_menu` VALUES (2, 90);
+    INSERT INTO `sys_role_menu` VALUES (2, 91);
+    INSERT INTO `sys_role_menu` VALUES (2, 95);
+    INSERT INTO `sys_role_menu` VALUES (2, 97);
+    INSERT INTO `sys_role_menu` VALUES (2, 102);
+    INSERT INTO `sys_role_menu` VALUES (2, 105);
+    INSERT INTO `sys_role_menu` VALUES (2, 106);
+    INSERT INTO `sys_role_menu` VALUES (2, 107);
+    INSERT INTO `sys_role_menu` VALUES (2, 108);
+    INSERT INTO `sys_role_menu` VALUES (2, 109);
+    INSERT INTO `sys_role_menu` VALUES (2, 110);
+    INSERT INTO `sys_role_menu` VALUES (2, 111);
+    INSERT INTO `sys_role_menu` VALUES (2, 112);
+    INSERT INTO `sys_role_menu` VALUES (2, 114);
+    INSERT INTO `sys_role_menu` VALUES (2, 115);
+    INSERT INTO `sys_role_menu` VALUES (2, 116);
+    INSERT INTO `sys_role_menu` VALUES (2, 117);
+    INSERT INTO `sys_role_menu` VALUES (2, 118);
+    INSERT INTO `sys_role_menu` VALUES (2, 119);
+    INSERT INTO `sys_role_menu` VALUES (2, 120);
+    INSERT INTO `sys_role_menu` VALUES (2, 121);
+    INSERT INTO `sys_role_menu` VALUES (2, 122);
+    INSERT INTO `sys_role_menu` VALUES (2, 123);
+    INSERT INTO `sys_role_menu` VALUES (2, 124);
+    INSERT INTO `sys_role_menu` VALUES (2, 125);
+    INSERT INTO `sys_role_menu` VALUES (2, 126);
+    INSERT INTO `sys_role_menu` VALUES (2, 127);
+    INSERT INTO `sys_role_menu` VALUES (2, 128);
+    INSERT INTO `sys_role_menu` VALUES (2, 129);
+    INSERT INTO `sys_role_menu` VALUES (2, 130);
+    INSERT INTO `sys_role_menu` VALUES (2, 131);
+    INSERT INTO `sys_role_menu` VALUES (2, 132);
+    INSERT INTO `sys_role_menu` VALUES (2, 133);
+    INSERT INTO `sys_role_menu` VALUES (2, 134);
+    INSERT INTO `sys_role_menu` VALUES (2, 135);
+    INSERT INTO `sys_role_menu` VALUES (2, 136);
+    INSERT INTO `sys_role_menu` VALUES (2, 137);
+    INSERT INTO `sys_role_menu` VALUES (2, 138);
+    -- ----------------------------
+    -- Table structure for sys_user
+    -- ----------------------------
+    DROP TABLE IF EXISTS `sys_user`;
+    CREATE TABLE `sys_user`  (
+                                 `id` int NOT NULL AUTO_INCREMENT,
+                                 `username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名',
+                                 `nickname` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称',
+                                 `gender` tinyint(1) NULL DEFAULT 1 COMMENT '性别((1-男 2-女 0-保密)',
+                                 `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码',
+                                 `dept_id` int NULL DEFAULT NULL COMMENT '部门ID',
+                                 `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户头像',
+                                 `mobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联系方式',
+                                 `status` tinyint(1) NULL DEFAULT 1 COMMENT '状态((1-正常 0-禁用)',
+                                 `email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户邮箱',
+                                 `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
+                                 `create_by` bigint NULL DEFAULT NULL COMMENT '创建人ID',
+                                 `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
+                                 `update_by` bigint NULL DEFAULT NULL COMMENT '修改人ID',
+                                 `is_deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除标识(0-未删除 1-已删除)',
+                                 PRIMARY KEY (`id`) USING BTREE,
+                                 UNIQUE INDEX `login_name`(`username` ASC) USING BTREE
+    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户信息表' ROW_FORMAT = DYNAMIC;
+
+    -- ----------------------------
+    -- 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', '18866668888', 1, 'youlaitech@163.com', NULL, NULL, NULL, 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', '18866668887', 1, '', '2019-10-10 13:41:22', NULL, '2022-07-31 12:39:30', NULL, 0);
+    INSERT INTO `sys_user` VALUES (3, 'websocket', '测试小用户', 1, '$2a$10$xVWsNOhHrCxh5UbpCE7/HuJ.PAOKcYAqRxD2CO2nVnJS.IAXkr5aq', 3, 'https://foruda.gitee.com/images/1723603502796844527/03cdca2a_716974.gif', '18866668886', 1, 'youlaitech@163.com', '2021-06-05 01:31:29', NULL, '2021-06-05 01:31:29', 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,
+                                      UNIQUE INDEX `uk_userid_roleid`(`user_id` ASC, `role_id` ASC) USING BTREE COMMENT '用户角色唯一索引'
+    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户和角色关联表' ROW_FORMAT = DYNAMIC;
+
+    -- ----------------------------
+    -- 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);
+
+
+    -- ----------------------------
+    -- Table structure for sys_log
+    -- ----------------------------
+    DROP TABLE IF EXISTS `sys_log`;
+    CREATE TABLE `sys_log` (
+                               `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
+                               `module` enum('LOGIN','USER','ROLE','DEPT','MENU','DICT','OTHER') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '日志模块',
+                               `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '日志内容',
+                               `request_uri` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '请求路径',
+                               `ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'IP地址',
+                               `province` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '省份',
+                               `city` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '城市',
+                               `execution_time` bigint DEFAULT NULL COMMENT '执行时间(ms)',
+                               `browser` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '浏览器',
+                               `browser_version` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '浏览器版本',
+                               `os` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '终端系统',
+                               `create_by` bigint DEFAULT NULL COMMENT '创建人ID',
+                               `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+                               `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除标识(1-已删除 0-未删除)',
+                               PRIMARY KEY (`id`) USING BTREE
+    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='系统日志表';
+
+
+    -- ----------------------------
+    -- Table structure for gen_config
+    -- ----------------------------
+    DROP TABLE IF EXISTS `gen_config`;
+    CREATE TABLE `gen_config` (
+                                  `id` bigint NOT NULL AUTO_INCREMENT,
+                                  `table_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '表名',
+                                  `module_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '模块名',
+                                  `package_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '包名',
+                                  `business_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '业务名',
+                                  `entity_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '实体类名',
+                                  `author` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '作者',
+                                  `parent_menu_id` bigint DEFAULT NULL COMMENT '上级菜单ID,对应sys_menu的id ',
+                                  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+                                  `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+                                  `is_deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+                                  PRIMARY KEY (`id`),
+                                  UNIQUE KEY `uk_tablename` (`table_name`)
+    ) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='代码生成基础配置表';
+
+    -- ----------------------------
+    -- Table structure for gen_field_config
+    -- ----------------------------
+    DROP TABLE IF EXISTS `gen_field_config`;
+    CREATE TABLE `gen_field_config` (
+                                        `id` bigint NOT NULL AUTO_INCREMENT,
+                                        `config_id` bigint NOT NULL COMMENT '关联的配置ID',
+                                        `column_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
+                                        `column_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
+                                        `column_length` int DEFAULT NULL,
+                                        `field_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '字段名称',
+                                        `field_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '字段类型',
+                                        `field_sort` int DEFAULT NULL COMMENT '字段排序',
+                                        `field_comment` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '字段描述',
+                                        `max_length` int NULL DEFAULT NULL,
+                                        `is_required` tinyint(1) DEFAULT NULL 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 DEFAULT NULL COMMENT '查询方式',
+                                        `form_type` tinyint DEFAULT NULL COMMENT '表单类型',
+                                        `dict_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '字典类型',
+                                        `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+                                        `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+                                        PRIMARY KEY (`id`),
+                                        KEY `config_id` (`config_id`)
+    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='代码生成字段配置表';
+
+    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 NOT NULL DEFAULT '0' COMMENT '读取状态(0未读;1已读)',
+                                       `read_time` datetime DEFAULT NULL COMMENT '阅读时间',
+                                       `create_time` datetime NOT NULL COMMENT '创建时间',
+                                       `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+                                       `is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除(1-已删除;0-未删除)',
+                                       PRIMARY KEY (`id`) USING BTREE
+    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户通知公告表';
+
+    CREATE TABLE `sys_notice` (
+                                  `id` bigint NOT NULL AUTO_INCREMENT,
+                                  `title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '通知标题',
+                                  `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '通知内容',
+                                  `type` tinyint NOT NULL COMMENT '通知类型(字典code:notice_type)',
+                                  `level` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '通知等级(字典code:notice_level)',
+                                  `target_type` tinyint NOT NULL COMMENT '目标类型(1: 全体, 2: 指定)',
+                                  `target_user_ids` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '目标人ID集合(多个使用英文逗号,分割)',
+                                  `publisher_id` bigint DEFAULT NULL COMMENT '发布人ID',
+                                  `publish_status` tinyint NOT NULL DEFAULT '0' COMMENT '发布状态(0: 未发布, 1: 已发布, -1: 已撤回)',
+                                  `publish_time` datetime DEFAULT NULL COMMENT '发布时间',
+                                  `revoke_time` datetime DEFAULT NULL COMMENT '撤回时间',
+                                  `create_by` bigint NOT NULL COMMENT '创建人ID',
+                                  `create_time` datetime NOT NULL COMMENT '创建时间',
+                                  `update_by` bigint DEFAULT NULL COMMENT '更新人ID',
+                                  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+                                  `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否删除(0: 未删除, 1: 已删除)',
+                                  PRIMARY KEY (`id`) USING BTREE
+    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='通知公告表';
+
+    SET FOREIGN_KEY_CHECKS = 1;
+
+     -- 提交事务
+    COMMIT;

+ 34 - 0
src/main/java/com/youlai/boot/CheckJarPid.java

@@ -0,0 +1,34 @@
+package com.youlai.boot;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;  
+  
+public class CheckJarPid {  
+    public static void main(String[] args) {  
+        String jarName = "jeecg-cloud-nacos-3.2.0.jar";  
+        String command = "cmd /c tasklist /FI \"IMAGENAME eq java.exe\" /FO CSV /NH | findstr \"" + jarName.replace(".", "\\.") + "\"";  
+  
+        try {  
+            Process process = Runtime.getRuntime().exec(command);  
+            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));  
+            String line;  
+  
+            // 读取命令输出  
+            while ((line = reader.readLine()) != null) {  
+                // 解析CSV格式的输出,第一列是PID  
+                String[] parts = line.split(",");  
+                if (parts.length > 0) {  
+                    String pid = parts[0].trim();  
+                    // 输出PID  
+                    System.out.println("PID: " + pid);  
+                    // 由于我们可能只关心第一个匹配项,所以找到后可以退出循环  
+                    break;  
+                }  
+            }  
+  
+            reader.close();  
+        } catch (Exception e) {  
+            e.printStackTrace();  
+        }  
+    }  
+}

+ 50 - 0
src/main/java/com/youlai/boot/DirectoryTreeBuilder.java

@@ -0,0 +1,50 @@
+//package com.youlai.boot;
+//
+//import com.youlai.boot.system.model.entity.DirectoryNode;
+//
+//import java.io.File;
+//import java.util.ArrayList;
+//import java.util.HashMap;
+//import java.util.List;
+//import java.util.Map;
+//
+//
+//public class DirectoryTreeBuilder {
+//    public static int currentId = 1; // 用于生成唯一的目录ID
+//    public static Map<String, DirectoryNode> directoryMap = new HashMap<>();
+//    public static Map<String, Integer> directoryIdMap = new HashMap<>(); // 存储目录路径到ID的映射
+//
+//    public static void main(String[] args) {
+//        File rootDir = new File("D:\\temp\\front\\pac");
+//        buildDirectoryTree(rootDir, -1); // -1表示根目录没有父目录
+////
+////        // 打印目录树(为了演示,这里仅打印根节点和其直接子节点)
+////        printDirectoryTree(directoryMap.get(rootDir.getAbsolutePath()));
+//    }
+//
+//    // 递归构建目录树
+//    public static int buildDirectoryTree(File dir, int parentId) {
+//        String dirPath = dir.getAbsolutePath();
+//        DirectoryNode currentNode = new DirectoryNode(dir.getName(), currentId, null);
+//        directoryMap.put(dirPath, currentNode);
+//        directoryIdMap.put(dirPath, currentId);
+//
+//        File[] files = dir.listFiles();
+//        if (files != null) {
+//            for (File file : files) {
+//                if (file.isDirectory()) {
+//                    currentId++;
+//                    int childId = buildDirectoryTree(file, parentId-1);
+//                    currentNode.addChild(directoryMap.get(file.getAbsolutePath()));
+//                }
+//                // 跳过文件
+//            }
+//        }
+//
+//        return currentId - 1; // 返回当前节点的ID
+//    }
+//
+//
+//
+//
+//}

+ 23 - 0
src/main/java/com/youlai/boot/YouLaiApplication.java

@@ -0,0 +1,23 @@
+package com.youlai.boot;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 应用启动类
+ *
+ * @author Ray
+ * @since 0.0.1
+ */
+@SpringBootApplication
+@ConfigurationPropertiesScan
+@EnableScheduling
+public class YouLaiApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(YouLaiApplication.class, args);
+    }
+
+}

+ 28 - 0
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";
+
+}
+

+ 23 - 0
src/main/java/com/youlai/boot/common/annotation/Log.java

@@ -0,0 +1,23 @@
+package com.youlai.boot.common.annotation;
+
+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 {
+
+    String value() default "";
+
+    LogModuleEnum module()  ;
+
+
+}

+ 28 - 0
src/main/java/com/youlai/boot/common/annotation/RepeatSubmit.java

@@ -0,0 +1,28 @@
+package com.youlai.boot.common.annotation;
+
+
+import java.lang.annotation.*;
+
+/**
+ * 防止重复提交注解
+ * <p>
+ * 该注解用于方法上,防止在指定时间内的重复提交。
+ * 默认时间为5秒。
+ *
+ * @author haoxr
+ * @since 2.3.0
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface RepeatSubmit {
+
+    /**
+     * 锁过期时间(秒)
+     * <p>
+     * 默认5秒内不允许重复提交
+     */
+    int expire() default 5;
+
+}

+ 15 - 0
src/main/java/com/youlai/boot/common/base/BaseAnalysisEventListener.java

@@ -0,0 +1,15 @@
+package com.youlai.boot.common.base;
+
+import com.alibaba.excel.event.AnalysisEventListener;
+
+/**
+ * 自定义解析结果监听器
+ *
+ * @author haoxr
+ * @since 2023/03/01
+ */
+public abstract class BaseAnalysisEventListener<T> extends AnalysisEventListener<T> {
+
+    private String msg;
+    public abstract String getMsg();
+}

+ 48 - 0
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;
+
+}

+ 28 - 0
src/main/java/com/youlai/boot/common/base/BasePageQuery.java

@@ -0,0 +1,28 @@
+package com.youlai.boot.common.base;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 基础分页请求对象
+ *
+ * @author haoxr
+ * @since 2021/2/28
+ */
+@Data
+@Schema
+public class BasePageQuery implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+
+    @Schema(description = "页码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    private int pageNum = 1;
+
+    @Schema(description = "每页记录数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
+    private int pageSize = 10;
+}

+ 21 - 0
src/main/java/com/youlai/boot/common/base/BaseVO.java

@@ -0,0 +1,21 @@
+package com.youlai.boot.common.base;
+
+import lombok.Data;
+import lombok.ToString;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 视图对象基类
+ *
+ * @author haoxr
+ * @since 2022/10/22
+ */
+@Data
+@ToString
+public class BaseVO implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+}

+ 88 - 0
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;
+    }
+
+
+}

+ 33 - 0
src/main/java/com/youlai/boot/common/constant/JwtClaimConstants.java

@@ -0,0 +1,33 @@
+package com.youlai.boot.common.constant;
+
+/**
+ * JWT Claims声明常量
+ * <p>
+ * JWT Claims 属于 Payload 的一部分,包含了一些实体(通常指的用户)的状态和额外的元数据。
+ *
+ * @author haoxr
+ * @since 2023/11/24
+ */
+public interface JwtClaimConstants {
+
+    /**
+     * 用户ID
+     */
+    String USER_ID = "userId";
+
+    /**
+     * 部门ID
+     */
+    String DEPT_ID = "deptId";
+
+    /**
+     * 数据权限
+     */
+    String DATA_SCOPE = "dataScope";
+
+    /**
+     * 权限(角色Code)集合
+     */
+    String AUTHORITIES = "authorities";
+
+}

+ 45 - 0
src/main/java/com/youlai/boot/common/constant/RedisConstants.java

@@ -0,0 +1,45 @@
+package com.youlai.boot.common.constant;
+
+/**
+ * Redis Key常量
+ *
+ * @author Theo
+ * @since 2024-7-29 11:46:08
+ */
+public interface RedisConstants {
+
+    /**
+     * 系统配置Redis-key
+     */
+    String SYSTEM_CONFIG_KEY = "system:config";
+
+    /**
+     * IP限流Redis-key
+     */
+    String IP_RATE_LIMITER_KEY = "ip:rate:limiter:";
+
+    /**
+     * 防重复提交Redis-key
+     */
+    String RESUBMIT_LOCK_PREFIX = "resubmit:lock:";
+
+    /**
+     * 单个IP请求的最大每秒查询数(QPS)阈值Key
+     */
+    String IP_QPS_THRESHOLD_LIMIT_KEY = "IP_QPS_THRESHOLD_LIMIT";
+
+    /**
+     * 手机验证码缓存前缀
+     */
+
+    String MOBILE_VERIFICATION_CODE_PREFIX = "VERIFICATION_CODE:MOBILE:";
+
+
+    /**
+     * 邮箱验证码缓存前缀
+     */
+    String EMAIL_VERIFICATION_CODE_PREFIX = "VERIFICATION_CODE:EMAIL:";
+
+    String FOLDER_PREFIX = "FOLDER:";
+
+}

+ 46 - 0
src/main/java/com/youlai/boot/common/constant/SecurityConstants.java

@@ -0,0 +1,46 @@
+package com.youlai.boot.common.constant;
+
+/**
+ * 缓存常量
+ *
+ * @author haoxr
+ * @since 2023/11/24
+ */
+public interface SecurityConstants {
+
+    /**
+     * 验证码缓存前缀
+     */
+    String CAPTCHA_CODE_PREFIX = "captcha_code:";
+
+    /**
+     * 角色和权限缓存前缀
+     */
+    String ROLE_PERMS_PREFIX = "role_perms:";
+
+    /**
+     * 黑名单Token缓存前缀
+     */
+    String BLACKLIST_TOKEN_PREFIX = "token:blacklist:";
+
+
+    /**
+     * 登录路径
+     */
+    String LOGIN_PATH = "/api/v1/auth/login";
+
+    String TEST_PATH = "/api/v1/deploy/**";
+
+    String DOWNLOAD_PATH = "/api/v1/demand-file/download/**";
+
+    String DOWNLOAD_PATH_2 = "/api/v1/deploy/download/**";
+
+    String SYNC_PATH = "/sync/**";
+
+    /**
+     * JWT Token 前缀
+     */
+    String JWT_TOKEN_PREFIX = "Bearer ";
+
+
+}

+ 120 - 0
src/main/java/com/youlai/boot/common/constant/SymbolConstant.java

@@ -0,0 +1,120 @@
+package com.youlai.boot.common.constant;
+
+/**
+ * 符号和特殊符号常用类
+ *
+ * @author Theo
+ * @since 2024-7-29 11:46:08
+ */
+public interface SymbolConstant {
+
+    /**
+     * 符号:点
+     */
+    String SPOT = ".";
+
+    /**
+     * 符号:双斜杠
+     */
+    String DOUBLE_BACKSLASH = "\\";
+
+    /**
+     * 符号:冒号
+     */
+    String COLON = ":";
+
+    /**
+     * 符号:逗号
+     */
+    String COMMA = ",";
+
+    /**
+     * 符号:左花括号 {
+     */
+    String LEFT_CURLY_BRACKET = "{";
+
+    /**
+     * 符号:右花括号 }
+     */
+    String RIGHT_CURLY_BRACKET = "}";
+
+    /**
+     * 符号:井号 #
+     */
+    String WELL_NUMBER = "#";
+
+    /**
+     * 符号:单斜杠
+     */
+    String SINGLE_SLASH = "/";
+
+    /**
+     * 符号:双斜杠
+     */
+    String DOUBLE_SLASH = "//";
+
+    /**
+     * 符号:感叹号
+     */
+    String EXCLAMATORY_MARK = "!";
+
+    /**
+     * 符号:下划线
+     */
+    String UNDERLINE = "_";
+
+    /**
+     * 符号:单引号
+     */
+    String SINGLE_QUOTATION_MARK = "'";
+
+    /**
+     * 符号:星号
+     */
+    String ASTERISK = "*";
+
+    /**
+     * 符号:百分号
+     */
+    String PERCENT_SIGN = "%";
+
+    /**
+     * 符号:美元 $
+     */
+    String DOLLAR = "$";
+
+    /**
+     * 符号:和 &
+     */
+    String AND = "&";
+
+    /**
+     * 符号:../
+     */
+    String SPOT_SINGLE_SLASH = "../";
+
+    /**
+     * 符号:..\\
+     */
+    String SPOT_DOUBLE_BACKSLASH = "..\\";
+
+    /**
+     * 系统变量前缀 #{
+     */
+    String SYS_VAR_PREFIX = "#{";
+
+    /**
+     * 符号 {{
+     */
+    String DOUBLE_LEFT_CURLY_BRACKET = "{{";
+
+    /**
+     * 符号:[
+     */
+    String SQUARE_BRACKETS_LEFT = "[";
+
+    /**
+     * 符号:]
+     */
+    String SQUARE_BRACKETS_RIGHT = "]";
+}

+ 43 - 0
src/main/java/com/youlai/boot/common/constant/SystemConstants.java

@@ -0,0 +1,43 @@
+package com.youlai.boot.common.constant;
+
+/**
+ * 系统常量
+ *
+ * @author haoxr
+ * @since 1.0.0
+ */
+public interface SystemConstants {
+
+    /**
+     * 根节点ID
+     */
+    Long ROOT_NODE_ID = 0L;
+
+    /**
+     * 系统默认密码
+     */
+    String DEFAULT_PASSWORD = "123456";
+
+    /**
+     * 超级管理员角色编码
+     */
+    String ROOT_ROLE_CODE = "ROOT";
+
+
+    String JAR_PATH_PREFIX = "JAR_PATH";
+
+    String DIST_PATH_PREFIX = "DIST_PATH";
+
+    String JAR_TYPE = "jar";
+
+    String DIST_TYPE = "dist";
+
+    String VIEW_PREFIX = "VIEW_PREFIX";
+
+    String UPLOAD_PATH = "UPLOAD_PATH";
+
+    String DEMAND_FOLDER = "demand";
+
+    String OM_FOLDER = "om";
+
+}

+ 31 - 0
src/main/java/com/youlai/boot/common/enums/DataScopeEnum.java

@@ -0,0 +1,31 @@
+package com.youlai.boot.common.enums;
+
+import com.youlai.boot.common.base.IBaseEnum;
+import lombok.Getter;
+
+/**
+ * 数据权限枚举
+ *
+ * @author haoxr
+ * @since 2.3.0
+ */
+@Getter
+public enum DataScopeEnum implements IBaseEnum<Integer> {
+
+    /**
+     * value 越小,数据权限范围越大
+     */
+    ALL(0, "所有数据"),
+    DEPT_AND_SUB(1, "部门及子部门数据"),
+    DEPT(2, "本部门数据"),
+    SELF(3, "本人数据");
+
+    private final Integer value;
+
+    private final String label;
+
+    DataScopeEnum(Integer value, String label) {
+        this.value = value;
+        this.label = label;
+    }
+}

+ 26 - 0
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;
+    }
+}

+ 34 - 0
src/main/java/com/youlai/boot/common/enums/LogModuleEnum.java

@@ -0,0 +1,34 @@
+package com.youlai.boot.common.enums;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+
+/**
+ * 日志模块枚举
+ *
+ * @author Ray
+ * @since 2.10.0
+ */
+@Schema(enumAsRef = true)
+@Getter
+public enum LogModuleEnum {
+
+
+    LOGIN("登录"),
+    USER("用户"),
+    DEPT("部门"),
+    ROLE("角色"),
+    MENU("菜单"),
+    DICT("字典"),
+    OTHER("其他"),
+    UPLOAD("上传")
+    ;
+
+    @JsonValue
+    private final String moduleName;
+
+    LogModuleEnum(String moduleName) {
+        this.moduleName = moduleName;
+    }
+}

+ 27 - 0
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;
+    }
+}

+ 38 - 0
src/main/java/com/youlai/boot/common/exception/BusinessException.java

@@ -0,0 +1,38 @@
+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(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();
+    }
+}

+ 219 - 0
src/main/java/com/youlai/boot/common/exception/GlobalExceptionHandler.java

@@ -0,0 +1,219 @@
+package com.youlai.boot.common.exception;
+
+import cn.hutool.core.util.StrUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.youlai.boot.common.result.Result;
+import com.youlai.boot.common.result.ResultCode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.TypeMismatchException;
+import org.springframework.context.support.DefaultMessageSourceResolvable;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.jdbc.BadSqlGrammarException;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.validation.BindException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+import org.springframework.web.servlet.NoHandlerFoundException;
+
+import jakarta.servlet.ServletException;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+
+import java.sql.SQLSyntaxErrorException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * 全局系统异常处理器
+ * <p>
+ * 调整异常处理的HTTP状态码,丰富异常处理类型
+ *
+ * @author Gadfly
+ * @since 2020-02-25 13:54
+ **/
+@RestControllerAdvice
+@Slf4j
+public class GlobalExceptionHandler {
+
+    @ExceptionHandler(BindException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(BindException e) {
+        log.error("BindException:{}", e.getMessage());
+        String msg = e.getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
+        return Result.failed(ResultCode.PARAM_ERROR, msg);
+    }
+
+    /**
+     * RequestParam参数的校验
+     *
+     * @param e
+     * @param <T>
+     * @return
+     */
+    @ExceptionHandler(ConstraintViolationException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(ConstraintViolationException e) {
+        log.error("ConstraintViolationException:{}", e.getMessage());
+        String msg = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";"));
+        return Result.failed(ResultCode.PARAM_ERROR, msg);
+    }
+
+    /**
+     * RequestBody参数的校验
+     *
+     * @param e
+     * @param <T>
+     * @return
+     */
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(MethodArgumentNotValidException e) {
+        log.error("MethodArgumentNotValidException:{}", e.getMessage());
+        String msg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining(";"));
+        return Result.failed(ResultCode.PARAM_ERROR, msg);
+    }
+
+    @ExceptionHandler(NoHandlerFoundException.class)
+    @ResponseStatus(HttpStatus.NOT_FOUND)
+    public <T> Result<T> processException(NoHandlerFoundException e) {
+        log.error(e.getMessage(), e);
+        return Result.failed(ResultCode.RESOURCE_NOT_FOUND);
+    }
+
+    /**
+     * MissingServletRequestParameterException
+     */
+    @ExceptionHandler(MissingServletRequestParameterException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(MissingServletRequestParameterException e) {
+        log.error(e.getMessage(), e);
+        return Result.failed(ResultCode.PARAM_IS_NULL);
+    }
+
+    /**
+     * MethodArgumentTypeMismatchException
+     */
+    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(MethodArgumentTypeMismatchException e) {
+        log.error(e.getMessage(), e);
+        return Result.failed(ResultCode.PARAM_ERROR, "类型错误");
+    }
+
+    /**
+     * ServletException
+     */
+    @ExceptionHandler(ServletException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(ServletException e) {
+        log.error(e.getMessage(), e);
+        return Result.failed(e.getMessage());
+    }
+
+    @ExceptionHandler(IllegalArgumentException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> handleIllegalArgumentException(IllegalArgumentException e) {
+        log.error("非法参数异常,异常原因:{}", e.getMessage(), e);
+        return Result.failed(e.getMessage());
+    }
+
+    @ExceptionHandler(JsonProcessingException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> handleJsonProcessingException(JsonProcessingException e) {
+        log.error("Json转换异常,异常原因:{}", e.getMessage(), e);
+        return Result.failed(e.getMessage());
+    }
+
+    /**
+     * HttpMessageNotReadableException
+     */
+    @ExceptionHandler(HttpMessageNotReadableException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(HttpMessageNotReadableException e) {
+        log.error(e.getMessage(), e);
+        String errorMessage = "请求体不可为空";
+        Throwable cause = e.getCause();
+        if (cause != null) {
+            errorMessage = convertMessage(cause);
+        }
+        return Result.failed(errorMessage);
+    }
+
+    @ExceptionHandler(TypeMismatchException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> processException(TypeMismatchException e) {
+        log.error(e.getMessage(), e);
+        return Result.failed(e.getMessage());
+    }
+
+    @ExceptionHandler(BadSqlGrammarException.class)
+    @ResponseStatus(HttpStatus.FORBIDDEN)
+    public <T> Result<T> handleBadSqlGrammarException(BadSqlGrammarException e) {
+        log.error(e.getMessage(), e);
+        String errorMsg = e.getMessage();
+        if (StrUtil.isNotBlank(errorMsg) && errorMsg.contains("denied to user")) {
+            return Result.failed(ResultCode.FORBIDDEN_OPERATION);
+        } else {
+            return Result.failed(e.getMessage());
+        }
+    }
+
+    @ExceptionHandler(SQLSyntaxErrorException.class)
+    @ResponseStatus(HttpStatus.FORBIDDEN)
+    public <T> Result<T> processSQLSyntaxErrorException(SQLSyntaxErrorException e) {
+        log.error(e.getMessage(), e);
+        return Result.failed(e.getMessage());
+    }
+
+
+    @ExceptionHandler(BusinessException.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> handleBizException(BusinessException e) {
+        log.error("biz exception: {}", e.getMessage());
+        if (e.getResultCode() != null) {
+            return Result.failed(e.getResultCode());
+        }
+        return Result.failed(e.getMessage());
+    }
+
+    @ExceptionHandler(Exception.class)
+    @ResponseStatus(HttpStatus.BAD_REQUEST)
+    public <T> Result<T> handleException(Exception e) throws Exception{
+        // 将 Spring Security 异常继续抛出,以便交给自定义处理器处理
+        if (e instanceof AccessDeniedException
+                || e instanceof AuthenticationException) {
+            throw e;
+        }
+        log.error("unknown exception: {}", e.getMessage());
+        e.printStackTrace();
+        return Result.failed(e.getLocalizedMessage());
+    }
+
+    /**
+     * 传参类型错误时,用于消息转换
+     *
+     * @param throwable 异常
+     * @return 错误信息
+     */
+    private String convertMessage(Throwable throwable) {
+        String error = throwable.toString();
+        String regulation = "\\[\"(.*?)\"]+";
+        Pattern pattern = Pattern.compile(regulation);
+        Matcher matcher = pattern.matcher(error);
+        String group = "";
+        if (matcher.find()) {
+            String matchString = matcher.group();
+            matchString = matchString.replace("[", "").replace("]", "");
+            matchString = "%s字段类型错误".formatted(matchString.replaceAll("\"", ""));
+            group += matchString;
+        }
+        return group;
+    }
+}

+ 32 - 0
src/main/java/com/youlai/boot/common/model/KeyValue.java

@@ -0,0 +1,32 @@
+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 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 - 0
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;
+
+}

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

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

+ 46 - 0
src/main/java/com/youlai/boot/common/result/PageResult.java

@@ -0,0 +1,46 @@
+package com.youlai.boot.common.result;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 分页响应结构体
+ *
+ * @author Ray
+ * @since 2022/2/18
+ */
+@Data
+public class PageResult<T> implements Serializable {
+
+    private String code;
+
+    private Data<T> data;
+
+    private String msg;
+
+    public static <T> PageResult<T> success(IPage<T> page) {
+        PageResult<T> result = new PageResult<>();
+        result.setCode(ResultCode.SUCCESS.getCode());
+
+        Data<T> data = new Data<>();
+        data.setList(page.getRecords());
+        data.setTotal(page.getTotal());
+
+        result.setData(data);
+        result.setMsg(ResultCode.SUCCESS.getMsg());
+        return result;
+    }
+
+    @lombok.Data
+    public static class Data<T> {
+
+        private List<T> list;
+
+        private long total;
+
+    }
+
+}

+ 73 - 0
src/main/java/com/youlai/boot/common/result/Result.java

@@ -0,0 +1,73 @@
+package com.youlai.boot.common.result;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 统一响应结构体
+ *
+ * @author Ray
+ * @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_EXECUTION_ERROR.getCode(), ResultCode.SYSTEM_EXECUTION_ERROR.getMsg(), null);
+    }
+
+    public static <T> Result<T> failed(String msg) {
+        return result(ResultCode.SYSTEM_EXECUTION_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(), msg, null);
+    }
+
+    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;
+    }
+
+    public static boolean isSuccess(Result<?> result) {
+        return result != null && ResultCode.SUCCESS.getCode().equals(result.getCode());
+    }
+}

+ 116 - 0
src/main/java/com/youlai/boot/common/result/ResultCode.java

@@ -0,0 +1,116 @@
+package com.youlai.boot.common.result;
+
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 响应码枚举
+ * <p>
+ * 参考阿里巴巴开发手册响应码规范
+ *
+ * @author Ray
+ * @since 2020/6/23
+ **/
+@AllArgsConstructor
+@NoArgsConstructor
+public enum ResultCode implements IResultCode, Serializable {
+
+    SUCCESS("00000", "一切ok"),
+
+    USER_ERROR("A0001", "用户端错误"),
+    REPEAT_SUBMIT_ERROR("A0002", "您的请求已提交,请不要重复提交或等待片刻再尝试。"),
+
+    USER_LOGIN_ERROR("A0200", "用户登录异常"),
+
+    USER_NOT_EXIST("A0201", "用户不存在"),
+    USER_ACCOUNT_LOCKED("A0202", "用户账户被冻结"),
+    USER_ACCOUNT_INVALID("A0203", "用户账户已作废"),
+
+    USERNAME_OR_PASSWORD_ERROR("A0210", "用户名或密码错误"),
+    PASSWORD_ENTER_EXCEED_LIMIT("A0211", "用户输入密码次数超限"),
+    CLIENT_AUTHENTICATION_FAILED("A0212", "客户端认证失败"),
+
+    VERIFY_CODE_TIMEOUT("A0213", "验证码已过期"),
+    VERIFY_CODE_ERROR("A0214", "验证码错误"),
+
+    TOKEN_INVALID("A0230", "token无效或已过期"),
+    TOKEN_ACCESS_FORBIDDEN("A0231", "token已被禁止访问"),
+
+    AUTHORIZED_ERROR("A0300", "访问权限异常"),
+    ACCESS_UNAUTHORIZED("A0301", "访问未授权"),
+    FORBIDDEN_OPERATION("A0302", "演示环境禁止新增、修改和删除数据,请本地部署后测试"),
+
+
+    PARAM_ERROR("A0400", "用户请求参数错误"),
+    RESOURCE_NOT_FOUND("A0401", "请求资源不存在"),
+    PARAM_IS_NULL("A0410", "请求必填参数为空"),
+
+    USER_UPLOAD_FILE_ERROR("A0700", "用户上传文件异常"),
+    USER_UPLOAD_FILE_TYPE_NOT_MATCH("A0701", "用户上传文件类型不匹配"),
+    USER_UPLOAD_FILE_SIZE_EXCEEDS("A0702", "用户上传文件太大"),
+    USER_UPLOAD_IMAGE_SIZE_EXCEEDS("A0703", "用户上传图片太大"),
+
+    SYSTEM_EXECUTION_ERROR("B0001", "系统执行出错"),
+    SYSTEM_EXECUTION_TIMEOUT("B0100", "系统执行超时"),
+    SYSTEM_ORDER_PROCESSING_TIMEOUT("B0100", "系统订单处理超时"),
+
+    SYSTEM_DISASTER_RECOVERY_TRIGGER("B0200", "系统容灾功能被触发"),
+    FLOW_LIMITING("B0210", "系统限流,请稍后再试"),
+    DEGRADATION("B0220", "系统功能降级"),
+
+    SYSTEM_RESOURCE_ERROR("B0300", "系统资源异常"),
+    SYSTEM_RESOURCE_EXHAUSTION("B0310", "系统资源耗尽"),
+    SYSTEM_RESOURCE_ACCESS_ERROR("B0320", "系统资源访问异常"),
+    SYSTEM_READ_DISK_FILE_ERROR("B0321", "系统读取磁盘文件失败"),
+
+    CALL_THIRD_PARTY_SERVICE_ERROR("C0001", "调用第三方服务出错"),
+    MIDDLEWARE_SERVICE_ERROR("C0100", "中间件服务出错"),
+    INTERFACE_NOT_EXIST("C0113", "接口不存在"),
+
+    MESSAGE_SERVICE_ERROR("C0120", "消息服务出错"),
+    MESSAGE_DELIVERY_ERROR("C0121", "消息投递出错"),
+    MESSAGE_CONSUMPTION_ERROR("C0122", "消息消费出错"),
+    MESSAGE_SUBSCRIPTION_ERROR("C0123", "消息订阅出错"),
+    MESSAGE_GROUP_NOT_FOUND("C0124", "消息分组未查到"),
+
+    DATABASE_ERROR("C0300", "数据库服务出错"),
+    DATABASE_TABLE_NOT_EXIST("C0311", "表不存在"),
+    DATABASE_COLUMN_NOT_EXIST("C0312", "列不存在"),
+    DATABASE_DUPLICATE_COLUMN_NAME("C0321", "多表关联中存在多个相同名称的列"),
+    DATABASE_DEADLOCK("C0331", "数据库死锁"),
+    DATABASE_PRIMARY_KEY_CONFLICT("C0341", "主键冲突");
+
+    @Override
+    public String getCode() {
+        return code;
+    }
+
+    @Override
+    public String getMsg() {
+        return msg;
+    }
+
+    private String code;
+
+    private String 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_EXECUTION_ERROR; // 默认系统执行错误
+    }
+}

+ 61 - 0
src/main/java/com/youlai/boot/common/util/DateUtils.java

@@ -0,0 +1,61 @@
+
+package com.youlai.boot.common.util;
+
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.ReflectUtil;
+import cn.hutool.core.util.StrUtil;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.lang.reflect.Field;
+
+/**
+ * 日期工具类
+ *
+ * @author haoxr
+ * @since 2.4.2
+ */
+public class DateUtils {
+
+    /**
+     * 区间日期格式化为数据库日期格式
+     * <p>
+     * eg:2021-01-01 → 2021-01-01 00:00:00
+     *
+     * @param obj                要处理的对象
+     * @param startTimeFieldName 起始时间字段名
+     * @param endTimeFieldName   结束时间字段名
+     */
+    public static void toDatabaseFormat(Object obj, String startTimeFieldName, String endTimeFieldName) {
+        Field startTimeField = ReflectUtil.getField(obj.getClass(), startTimeFieldName);
+        Field endTimeField = ReflectUtil.getField(obj.getClass(), endTimeFieldName);
+
+        if (startTimeField != null) {
+            processDateTimeField(obj, startTimeField, startTimeFieldName, "yyyy-MM-dd 00:00:00");
+        }
+
+        if (endTimeField != null) {
+            processDateTimeField(obj, endTimeField, endTimeFieldName, "yyyy-MM-dd 23:59:59");
+        }
+    }
+
+    /**
+     * 处理日期字段
+     *
+     * @param obj           要处理的对象
+     * @param field         字段
+     * @param fieldName     字段名
+     * @param targetPattern 目标数据库日期格式
+     */
+    private static void processDateTimeField(Object obj, Field field, String fieldName, String targetPattern) {
+        Object fieldValue = ReflectUtil.getFieldValue(obj, fieldName);
+        if (fieldValue != null) {
+            // 得到原始的日期格式
+            String pattern = field.isAnnotationPresent(DateTimeFormat.class) ? field.getAnnotation(DateTimeFormat.class).pattern() : "yyyy-MM-dd";
+            // 转换为日期对象
+            DateTime dateTime = DateUtil.parse(StrUtil.toString(fieldValue), pattern);
+            // 转换为目标数据库日期格式
+            ReflectUtil.setFieldValue(obj, fieldName, dateTime.toString(targetPattern));
+        }
+    }
+}

+ 20 - 0
src/main/java/com/youlai/boot/common/util/ExcelUtils.java

@@ -0,0 +1,20 @@
+package com.youlai.boot.common.util;
+
+import com.alibaba.excel.EasyExcel;
+import com.youlai.boot.common.base.BaseAnalysisEventListener;
+
+import java.io.InputStream;
+
+/**
+ * Excel 工具类
+ *
+ * @author haoxr
+ * @since 2023/03/01
+ */
+public class ExcelUtils {
+
+    public static <T> String importExcel(InputStream is, Class clazz, BaseAnalysisEventListener<T> listener) {
+        EasyExcel.read(is, clazz, listener).sheet().doRead();
+        return listener.getMsg();
+    }
+}

+ 304 - 0
src/main/java/com/youlai/boot/common/util/FtpUtil.java

@@ -0,0 +1,304 @@
+package com.youlai.boot.common.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.apache.commons.net.ftp.FTP;
+import org.apache.commons.net.ftp.FTPClient;
+import org.apache.commons.net.ftp.FTPClientConfig;
+import org.apache.commons.net.ftp.FTPFile;
+import org.apache.commons.net.ftp.FTPReply;
+
+public class FtpUtil {
+
+	private String Control_Encoding = "UTF-8";
+
+	private FTPClient client = null;
+
+	private String host = "182.92.126.35";
+	private int port = 21;
+	private String user = "root";
+	private String password = "Ccri123456";
+	private String ftpPath = "/";
+
+	private String absPath;
+	@SuppressWarnings("unused")
+	private FtpUtil() {
+	}
+
+	public FtpUtil(String absPath) {
+		this.absPath = absPath;
+	}
+
+	/**
+	 * 获取当前FTP所在目录位置
+	 * 
+	 * @return
+	 */
+	public String getHome() {
+		return this.ftpPath;
+	}
+
+	/**
+	 * 连接FTP Server
+	 * 
+	 * @throws IOException
+	 */
+	public static final String ANONYMOUS_USER = "anonymous";
+
+	public void connect() throws Exception {
+		if (client == null) {
+			client = new FTPClient();
+		}
+		// 已经连接
+		if (client.isConnected()) {
+			return;
+		}
+
+		// 编码
+		client.setControlEncoding(Control_Encoding);
+
+		try {
+			// 连接FTP Server
+			client.connect(this.host, this.port);
+			// 登陆
+			if (this.user == null || "".equals(this.user)) {
+				// 使用匿名登陆
+				client.login(ANONYMOUS_USER, ANONYMOUS_USER);
+			} else {
+				client.login(this.user, this.password);
+			}
+			// 设置文件格式
+			client.setFileType(FTPClient.BINARY_FILE_TYPE);
+			// 获取FTP Server 应答
+			int reply = client.getReplyCode();
+			if (!FTPReply.isPositiveCompletion(reply)) {
+				client.disconnect();
+				throw new Exception("connection FTP fail!");
+			}
+
+			FTPClientConfig config = new FTPClientConfig(client.getSystemType().split(" ")[0]);
+			config.setServerLanguageCode("zh");
+			client.configure(config);
+			// 使用被动模式设为默认
+			client.enterLocalPassiveMode();
+			// 二进制文件支持
+			client.setFileType(FTP.BINARY_FILE_TYPE);
+			// 设置模式
+			client.setFileTransferMode(FTP.STREAM_TRANSFER_MODE);
+
+		} catch (IOException e) {
+			throw new Exception("connection FTP fail! " + e);
+		}
+	}
+
+	/**
+	 * 断开FTP连接
+	 * 
+	 * @param ftpClient
+	 * @throws IOException
+	 */
+	public void close() {
+		if (client != null && client.isConnected()) {
+			try {
+				client.logout();
+				client.disconnect();
+			} catch (IOException e) {
+				e.printStackTrace();
+			}
+		}
+	}
+
+	/**
+	 * 获取文件列表
+	 * 
+	 * @return
+	 */
+	public List<FTPFile> list() {
+		List<FTPFile> list = null;
+		try {
+			FTPFile ff[] = client.listFiles(ftpPath);
+			if (ff != null && ff.length > 0) {
+				list = new ArrayList<FTPFile>(ff.length);
+				Collections.addAll(list, ff);
+			} else {
+				list = new ArrayList<FTPFile>(0);
+			}
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return list;
+	}
+
+	/**
+	 * 切换目录
+	 * 
+	 * @param path
+	 *            需要切换的目录
+	 * @param forcedIncrease
+	 *            如果目录不存在,是否增加
+	 */
+	public void switchDirectory(String path, boolean forcedIncrease) {
+		try {
+			if (path != null && !"".equals(path)) {
+				boolean ok = client.changeWorkingDirectory(path);
+				if (ok) {
+					this.ftpPath = path;
+				} else if (forcedIncrease) {
+					// ftpPath 不存在,手动创建目录
+					StringTokenizer token = new StringTokenizer(path, "\\//");
+					while (token.hasMoreTokens()) {
+						String npath = token.nextToken();
+						client.makeDirectory(npath);
+						client.changeWorkingDirectory(npath);
+						if (ok) {
+							this.ftpPath = path;
+						}
+					}
+				}
+			}
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+	}
+
+	/**
+	 * 创建目录
+	 * 
+	 * @param path
+	 */
+	public void createDirectory(String path) {
+		try {
+			if (path != null && !"".equals(path)) {
+				boolean ok = client.changeWorkingDirectory(path);
+				if (!ok) {
+					// ftpPath 不存在,手动创建目录
+					StringTokenizer token = new StringTokenizer(path, "\\//");
+					while (token.hasMoreTokens()) {
+						String npath = token.nextToken();
+						client.makeDirectory(npath);
+						client.changeWorkingDirectory(npath);
+					}
+				}
+			}
+			client.changeWorkingDirectory(this.ftpPath);
+		} catch (Exception e) {
+			e.printStackTrace();
+		}
+	}
+
+	/**
+	 * 删除目录,如果目录中存在文件或者文件夹则删除失败
+	 * 
+	 * @param path
+	 * @return
+	 */
+	public boolean deleteDirectory(String path) {
+		boolean flag = false;
+		try {
+			flag = client.removeDirectory(path);
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return flag;
+	}
+
+	/**
+	 * 删除文件
+	 * 
+	 * @param path
+	 * @return
+	 */
+	public boolean deleteFile(String path) {
+		boolean flag = false;
+		try {
+			flag = client.deleteFile(path);
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return flag;
+	}
+
+	/**
+	 * 上传文件,上传文件只会传到当前所在目录
+	 * 
+	 * @param localFile
+	 *            本地文件
+	 * @return
+	 */
+	public boolean upload(File localFile) {
+		return this.upload(localFile, "");
+	}
+
+	/**
+	 * 上传文件,会覆盖FTP上原有文件
+	 * 
+	 * @param localFile
+	 *            本地文件
+	 * @param reName
+	 *            重名
+	 * @return
+	 */
+	public boolean upload(File localFile, String reName) {
+		boolean flag = false;
+		String targetName = reName;
+		// 设置上传后文件名
+		if (reName == null || "".equals(reName)) {
+			targetName = localFile.getName();
+		}
+		FileInputStream fis = null;
+		try {
+			// 开始上传文件
+			fis = new FileInputStream(localFile);
+			client.setControlEncoding(Control_Encoding);
+			client.setFileType(FTPClient.BINARY_FILE_TYPE);
+			boolean ok = client.storeFile(targetName, fis);
+			if (ok) {
+				flag = true;
+			}
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return flag;
+	}
+
+	/**
+	 * 下载文件,如果存在会覆盖原文件
+	 * 
+	 * @param ftpFileName
+	 *            文件名称,FTP上的文件名称
+	 * @param savePath
+	 *            保存目录,本地保存目录
+	 * @return
+	 */
+	public boolean download(String ftpFileName, String savePath) {
+		boolean flag = false;
+
+		File dir = new File(savePath);
+
+		if (!dir.exists()) {
+			dir.mkdirs();
+		}
+
+		FileOutputStream fos = null;
+		try {
+			String saveFile = dir.getAbsolutePath() + File.separator + ftpFileName;
+			fos = new FileOutputStream(new File(saveFile));
+			boolean ok = client.retrieveFile(ftpFileName, fos);
+			if (ok) {
+				flag = true;
+			}
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return flag;
+	}
+
+
+}

+ 139 - 0
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;
+        }
+    }
+}

+ 51 - 0
src/main/java/com/youlai/boot/common/util/ResponseUtils.java

@@ -0,0 +1,51 @@
+package com.youlai.boot.common.util;
+
+import cn.hutool.json.JSONUtil;
+import com.youlai.boot.common.result.Result;
+import com.youlai.boot.common.result.ResultCode;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 响应工具类
+ *
+ * @author Ray Hao
+ * @since 2.0.0
+ */
+@Slf4j
+public class ResponseUtils {
+
+    /**
+     * 异常消息返回(适用过滤器中处理异常响应)
+     *
+     * @param response  HttpServletResponse
+     * @param resultCode 响应结果码
+     */
+    public static void writeErrMsg(HttpServletResponse response, ResultCode resultCode) {
+        // 根据不同的结果码设置HTTP状态
+        int status = switch (resultCode) {
+            case ACCESS_UNAUTHORIZED, TOKEN_INVALID -> HttpStatus.UNAUTHORIZED.value();
+            case TOKEN_ACCESS_FORBIDDEN -> HttpStatus.FORBIDDEN.value();
+            default -> HttpStatus.BAD_REQUEST.value();
+        };
+
+        response.setStatus(status);
+        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
+        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
+
+        try (PrintWriter writer = response.getWriter()) {
+            String jsonResponse = JSONUtil.toJsonStr(Result.failed(resultCode));
+            writer.print(jsonResponse);
+            writer.flush(); // 确保将响应内容写入到输出流
+        } catch (IOException e) {
+            log.error("响应异常处理失败", e);
+        }
+    }
+
+}

+ 61 - 0
src/main/java/com/youlai/boot/common/util/SFTPChannel.java

@@ -0,0 +1,61 @@
+package com.youlai.boot.common.util;
+
+import java.util.Map;
+import java.util.Properties;
+import com.jcraft.jsch.Channel;
+import com.jcraft.jsch.ChannelSftp;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.Session;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class SFTPChannel {
+    Session session = null;
+    Channel channel = null;
+
+
+  	//登录sftp
+    public ChannelSftp getChannel(Map<String, String> sftpDetails, int timeout) throws JSchException {
+        String ftpHost = sftpDetails.get(SFTPConstants.SFTP_REQ_HOST);
+        String port = sftpDetails.get(SFTPConstants.SFTP_REQ_PORT);
+        String ftpUserName = sftpDetails.get(SFTPConstants.SFTP_REQ_USERNAME);
+        String ftpPassword = sftpDetails.get(SFTPConstants.SFTP_REQ_PASSWORD);
+
+        int ftpPort = SFTPConstants.SFTP_DEFAULT_PORT;
+        if (port != null && !port.equals("")) {
+            ftpPort = Integer.valueOf(port);
+        }
+
+      	//创建JSch对象
+        JSch jsch = new JSch(); 
+      	//根据用户名,主机ip,端口获取一个Session对象
+        session = jsch.getSession(ftpUserName, ftpHost, ftpPort); 
+        log.debug("Session created.");
+        if (ftpPassword != null) {
+            session.setPassword(ftpPassword);
+        }
+        Properties config = new Properties();
+        config.put("StrictHostKeyChecking", "no");
+        session.setConfig(config); // 为Session对象设置properties
+        session.setTimeout(timeout); // 设置timeout时间
+        session.connect(); // 通过Session建立链接
+        log.debug("Session connected.");
+        log.debug("Opening Channel.");
+        channel = session.openChannel("sftp"); // 打开SFTP通道
+        channel.connect(); // 建立SFTP通道的连接
+        log.debug("Connected successfully to ftpHost = " + ftpHost + ",as ftpUserName = " + ftpUserName
+                + ", returning: " + channel);
+        return (ChannelSftp) channel;
+    }
+
+  	//退出sftp
+    public void closeChannel() throws Exception {
+        if (channel != null) {
+            channel.disconnect();
+        }
+        if (session != null) {
+            session.disconnect();
+        }
+    }
+}

+ 10 - 0
src/main/java/com/youlai/boot/common/util/SFTPConstants.java

@@ -0,0 +1,10 @@
+package com.youlai.boot.common.util;
+
+public class SFTPConstants {
+    public static final String SFTP_REQ_HOST = "host";
+    public static final String SFTP_REQ_PORT = "port";
+    public static final String SFTP_REQ_USERNAME = "username";
+    public static final String SFTP_REQ_PASSWORD = "password";
+    public static final int SFTP_DEFAULT_PORT = 22;
+    public static final String SFTP_REQ_LOC = "location";
+}

+ 41 - 0
src/main/java/com/youlai/boot/common/util/SFTPTest.java

@@ -0,0 +1,41 @@
+package com.youlai.boot.common.util;
+
+import java.util.HashMap;
+import java.util.Map;
+import com.jcraft.jsch.ChannelSftp;
+
+public class SFTPTest {
+
+    public SFTPChannel getSFTPChannel() {
+        return new SFTPChannel();
+    }
+
+    /**
+     * @param args
+     * @throws Exception
+     */
+    public static void main(String[] args) throws Exception {
+        SFTPTest test = new SFTPTest();
+
+        Map<String, String> sftpDetails = new HashMap<String, String>();
+        // 设置主机ip,端口,用户名,密码
+        sftpDetails.put(SFTPConstants.SFTP_REQ_HOST, "10.9.167.55");
+        sftpDetails.put(SFTPConstants.SFTP_REQ_USERNAME, "root");
+        sftpDetails.put(SFTPConstants.SFTP_REQ_PASSWORD, "arthur");
+        sftpDetails.put(SFTPConstants.SFTP_REQ_PORT, "22");
+        
+        String src = "D:\\DevSoft\\HB-SnagIt1001.rar"; // 本地文件名
+        String dst = "/home/omc/ylong/sftp/HB-SnagIt1001.rar"; // 目标文件名
+              
+        SFTPChannel channel = test.getSFTPChannel();
+        ChannelSftp chSftp = channel.getChannel(sftpDetails, 60000);
+
+        
+        chSftp.put(src, dst, ChannelSftp.OVERWRITE); // 代码段2(常用)
+        
+        // chSftp.put(new FileInputStream(src), dst, ChannelSftp.OVERWRITE); // 代码段3
+        
+        chSftp.quit();
+        channel.closeChannel();
+    }
+}

+ 98 - 0
src/main/java/com/youlai/boot/common/util/SystemUtil.java

@@ -0,0 +1,98 @@
+package com.youlai.boot.common.util;
+
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.*;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+@Slf4j
+public class SystemUtil {
+
+
+    public static boolean isLinuxServer(){
+        String os = System.getProperty("os.name").toLowerCase();
+        return os.contains("nix") || os.contains("nux") || os.contains("mac");
+    }
+
+    // 检查字符串是否包含中文字符
+    public static boolean containsChinese(String input) {
+        if (input == null || input.isEmpty()) {
+            return false;
+        }
+
+        for (char c : input.toCharArray()) {
+            // 检查字符是否在中文字符范围内
+            if (isChinese(c)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+
+    public static String renameFile(String originalFileName) {
+        String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
+        String[] split = originalFileName.split("\\.");
+        StringBuilder preName = new StringBuilder();
+        for (int i = 0; i < split.length-1; i++) {
+            preName.append(split[i]);
+        }
+        preName.append(timestamp);
+
+        return preName+"."+ split[split.length-1];
+
+    }
+
+
+   public static void downloadFile(String path, HttpServletResponse response) {
+        File file = new File(path);
+        response.reset();
+        response.setContentType("application/octet-stream");
+        response.setCharacterEncoding("utf-8");
+        response.setContentLength((int) file.length());
+
+        try(BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));) {
+            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(file.getName(), StandardCharsets.UTF_8) );
+
+            byte[] buff = new byte[1024];
+            OutputStream os  = response.getOutputStream();
+            int i = 0;
+            while ((i = bis.read(buff)) != -1) {
+                os.write(buff, 0, i);
+                os.flush();
+            }
+        } catch (IOException e) {
+            log.error(e.getMessage(),e);
+        }
+    }
+
+
+
+    // 检查单个字符是否为中文字符
+    private static boolean isChinese(char c) {
+        // Unicode 编码范围
+        // 基本区:\u4E00-\u9FFF
+        // 扩展A区:\u3400-\u4DBF
+        // 扩展B区:\u20000-\u2A6DF
+        // 扩展C区:\u2A700-\u2B73F
+        // 扩展D区:\u2B740-\u2B81F
+        // 扩展E区:\u2B820-\u2CEAF
+        // 扩展F区:\uF900-\uFAFF
+        // 兼容汉字:\u3300-\u33FF
+        // 兼容扩展:\uFE30-\uFE4F
+        // 兼容扩展补充:\u2F800-\u2FA1F
+
+        Character.UnicodeBlock ub = Character.UnicodeBlock.of(c);
+        return (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
+                || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS
+                || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A
+                || ub == Character.UnicodeBlock.GENERAL_PUNCTUATION
+                || ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION
+                || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS
+                || (c >= 0xF900 && c <= 0xFAFF) // 兼容汉字补充
+                || (c >= 0x2F800 && c <= 0x2FA1F)); // 兼容扩展补充
+    }
+}

+ 14 - 0
src/main/java/com/youlai/boot/config/AppConfig.java

@@ -0,0 +1,14 @@
+package com.youlai.boot.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class AppConfig {
+
+    @Bean
+    public RestTemplate restTemplate() {
+        return new RestTemplate();
+    }
+}

+ 55 - 0
src/main/java/com/youlai/boot/config/CaptchaConfig.java

@@ -0,0 +1,55 @@
+package com.youlai.boot.config;
+
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.captcha.generator.MathGenerator;
+import cn.hutool.captcha.generator.RandomGenerator;
+import com.youlai.boot.config.property.CaptchaProperties;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+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);
+        } 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);
+    }
+
+
+}

+ 42 - 0
src/main/java/com/youlai/boot/config/CorsConfig.java

@@ -0,0 +1,42 @@
+package com.youlai.boot.config;
+
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+
+import java.util.Collections;
+
+/**
+ * CORS 资源共享配置
+ *
+ * @author haoxr
+ * @since 2023/4/17
+ */
+@Configuration
+public class CorsConfig {
+
+    @Bean
+    public FilterRegistrationBean filterRegistrationBean() {
+        CorsConfiguration corsConfiguration = new CorsConfiguration();
+        //1.允许任何来源
+        corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*"));
+        //2.允许任何请求头
+        corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
+        //3.允许任何方法
+        corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
+        //4.允许凭证
+        corsConfiguration.setAllowCredentials(true);
+
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        source.registerCorsConfiguration("/**", corsConfiguration);
+        CorsFilter corsFilter = new CorsFilter(source);
+
+        FilterRegistrationBean<CorsFilter> filterRegistrationBean=new FilterRegistrationBean<>(corsFilter);
+        filterRegistrationBean.setOrder(-101);  // 小于 SpringSecurity Filter的 Order(-100) 即可
+
+        return filterRegistrationBean;
+    }
+}

+ 51 - 0
src/main/java/com/youlai/boot/config/MailConfig.java

@@ -0,0 +1,51 @@
+package com.youlai.boot.config;
+
+import com.youlai.boot.config.property.MailProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+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;
+    }
+}

+ 48 - 0
src/main/java/com/youlai/boot/config/MybatisConfig.java

@@ -0,0 +1,48 @@
+package com.youlai.boot.config;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.core.config.GlobalConfig;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import com.youlai.boot.core.handler.MyDataPermissionHandler;
+import com.youlai.boot.core.handler.MyMetaObjectHandler;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+/**
+ * mybatis-plus 自动配置类
+ *
+ * @author haoxr
+ * @since 2022/7/2
+ */
+@Configuration
+@EnableTransactionManagement
+public class MybatisConfig {
+
+    /**
+     * 分页插件和数据权限插件
+     */
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        //数据权限
+        interceptor.addInnerInterceptor(new DataPermissionInterceptor(new MyDataPermissionHandler()));
+        //分页插件
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+
+        return interceptor;
+    }
+
+    /**
+     * 自动填充数据库创建人、创建时间、更新人、更新时间
+     */
+    @Bean
+    public GlobalConfig globalConfig() {
+        GlobalConfig globalConfig = new GlobalConfig();
+        globalConfig.setMetaObjectHandler(new MyMetaObjectHandler());
+        return globalConfig;
+    }
+
+}

+ 74 - 0
src/main/java/com/youlai/boot/config/RedisCacheConfig.java

@@ -0,0 +1,74 @@
+package com.youlai.boot.config;
+
+import org.springframework.boot.autoconfigure.cache.CacheProperties;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+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
+ * @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;
+    }
+
+}

+ 42 - 0
src/main/java/com/youlai/boot/config/RedisConfig.java

@@ -0,0 +1,42 @@
+package com.youlai.boot.config;
+
+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.RedisSerializer;
+
+/**
+ * Redis 配置
+ *
+ * @author Ray
+ * @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);
+
+        redisTemplate.setKeySerializer(RedisSerializer.string());
+        redisTemplate.setValueSerializer(RedisSerializer.json());
+
+        redisTemplate.setHashKeySerializer(RedisSerializer.string());
+        redisTemplate.setHashValueSerializer(RedisSerializer.json());
+
+        redisTemplate.afterPropertiesSet();
+        return redisTemplate;
+    }
+
+}

+ 111 - 0
src/main/java/com/youlai/boot/config/SecurityConfig.java

@@ -0,0 +1,111 @@
+package com.youlai.boot.config;
+
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.core.collection.CollectionUtil;
+import com.youlai.boot.common.constant.SecurityConstants;
+import com.youlai.boot.config.property.SecurityProperties;
+import com.youlai.boot.core.filter.RateLimiterFilter;
+import com.youlai.boot.core.security.exception.MyAccessDeniedHandler;
+import com.youlai.boot.core.security.exception.MyAuthenticationEntryPoint;
+import com.youlai.boot.core.security.filter.JwtValidationFilter;
+import com.youlai.boot.core.security.filter.CaptchaValidationFilter;
+import com.youlai.boot.system.service.ConfigService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+/**
+ * Spring Security 权限配置
+ *
+ * @author Ray
+ * @since 2023/2/17
+ */
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity
+@RequiredArgsConstructor
+public class SecurityConfig {
+
+    private final MyAuthenticationEntryPoint authenticationEntryPoint;
+    private final MyAccessDeniedHandler accessDeniedHandler;
+    private final RedisTemplate<String, Object> redisTemplate;
+    private final CodeGenerator codeGenerator;
+    private final SecurityProperties securityProperties;
+    private final ConfigService configService;
+
+    @Bean
+    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+        http
+
+                .authorizeHttpRequests(requestMatcherRegistry ->
+                        requestMatcherRegistry.requestMatchers(SecurityConstants.LOGIN_PATH).permitAll()
+                                .requestMatchers(SecurityConstants.DOWNLOAD_PATH).permitAll()
+                                .requestMatchers(SecurityConstants.DOWNLOAD_PATH_2).permitAll()
+                                .requestMatchers(SecurityConstants.SYNC_PATH).permitAll()
+                                .anyRequest().authenticated()
+                )
+                .exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
+                        httpSecurityExceptionHandlingConfigurer
+                                .authenticationEntryPoint(authenticationEntryPoint)
+                                .accessDeniedHandler(accessDeniedHandler)
+                )
+                .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+                .csrf(AbstractHttpConfigurer::disable)
+                .headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
+
+        ;
+        // 限流过滤器
+        http.addFilterBefore(new RateLimiterFilter(redisTemplate, configService), UsernamePasswordAuthenticationFilter.class);
+        // 验证码校验过滤器
+//        http.addFilterBefore(new CaptchaValidationFilter(redisTemplate, codeGenerator), UsernamePasswordAuthenticationFilter.class);
+        // JWT 校验过滤器
+        http.addFilterBefore(new JwtValidationFilter(redisTemplate,securityProperties.getJwt().getKey()), UsernamePasswordAuthenticationFilter.class);
+
+        return http.build();
+    }
+
+    /**
+     * 不走过滤器链的放行配置
+     */
+    @Bean
+    public WebSecurityCustomizer webSecurityCustomizer() {
+        return (web) -> {
+            if (CollectionUtil.isNotEmpty(securityProperties.getIgnoreUrls())) {
+                web.ignoring().requestMatchers(securityProperties.getIgnoreUrls().toArray(new String[0]));
+            }
+        };
+    }
+
+    /**
+     * 密码编码器
+     */
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+
+    /**
+     * AuthenticationManager 手动注入
+     *
+     * @param authenticationConfiguration 认证配置
+     */
+    @Bean
+    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
+        return authenticationConfiguration.getAuthenticationManager();
+    }
+
+}

+ 84 - 0
src/main/java/com/youlai/boot/config/SwaggerConfig.java

@@ -0,0 +1,84 @@
+package com.youlai.boot.config;
+
+import io.swagger.v3.oas.models.Components;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Info;
+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;
+
+/**
+ * Swagger 配置
+ * <p>
+ *
+ * @author Ray
+ * @see <a href="https://doc.xiaominfo.com/docs/quick-start">knife4j 快速开始</a>
+ * @since 2023/2/17
+ */
+@Configuration
+@Slf4j
+@RequiredArgsConstructor
+public class SwaggerConfig {
+
+    private final Environment environment;
+
+    /**
+     * 接口信息
+     */
+    @Bean
+    public OpenAPI openApi() {
+
+        String appVersion = environment.getProperty("project.version", "1.0.0");
+
+        return new OpenAPI()
+                .info(new Info()
+                        .title("系统接口文档")
+                        .version(appVersion)
+                )
+                // 配置全局鉴权参数-Authorize
+                .components(new Components()
+                        .addSecuritySchemes(HttpHeaders.AUTHORIZATION,
+                                new SecurityScheme()
+                                        .name(HttpHeaders.AUTHORIZATION)
+                                        .type(SecurityScheme.Type.APIKEY)
+                                        .in(SecurityScheme.In.HEADER)
+                                        .scheme("Bearer")
+                                        .bearerFormat("JWT")
+                        )
+                );
+    }
+
+
+    /**
+     * 全局自定义扩展
+     * <p>
+     * 在OpenAPI规范中,Operation 是一个表示 API 端点(Endpoint)或操作的对象。
+     * 每个路径(Path)对象可以包含一个或多个 Operation 对象,用于描述与该路径相关联的不同 HTTP 方法(例如 GET、POST、PUT 等)。
+     */
+    @Bean
+    public GlobalOpenApiCustomizer globalOpenApiCustomizer() {
+        return openApi -> {
+            // 全局添加鉴权参数
+            if (openApi.getPaths() != null) {
+                openApi.getPaths().forEach((s, pathItem) -> {
+                    // 登录接口/验证码不需要添加鉴权参数
+                    if ("/api/v1/auth/login".equals(s) || "/api/v1/auth/captcha".equals(s)) {
+                        return;
+                    }
+                    // 接口添加鉴权参数
+                    pathItem.readOperations()
+                            .forEach(operation ->
+                                    operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION))
+                            );
+                });
+            }
+        };
+    }
+
+}

+ 25 - 0
src/main/java/com/youlai/boot/config/ThreadPoolConfig.java

@@ -0,0 +1,25 @@
+package com.youlai.boot.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+
+@Configuration
+public class ThreadPoolConfig {
+
+    @Bean(name = "ftpTaskExecutor")
+    @Primary
+    public Executor ftpTaskExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(5); // 核心线程数
+        executor.setMaxPoolSize(10); // 最大线程数
+        executor.setQueueCapacity(50); // 队列容量
+        executor.setThreadNamePrefix("FTPTask-"); // 线程名前缀
+        executor.initialize();
+        return executor;
+    }
+
+}

+ 67 - 0
src/main/java/com/youlai/boot/config/WebMvcConfig.java

@@ -0,0 +1,67 @@
+package com.youlai.boot.config;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import jakarta.validation.Validation;
+import jakarta.validation.Validator;
+import jakarta.validation.ValidatorFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.hibernate.validator.HibernateValidator;
+import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.validation.beanvalidation.SpringConstraintValidatorFactory;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.math.BigInteger;
+import java.util.List;
+
+/**
+ * WebMvc 自动装配配置
+ *
+ * @author Ray
+ * @since 2020/10/16
+ */
+@Configuration
+@Slf4j
+public class WebMvcConfig implements WebMvcConfigurer {
+
+    @Override
+    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
+        converters.add(new StringHttpMessageConverter());
+
+        MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
+        ObjectMapper objectMapper = jackson2HttpMessageConverter.getObjectMapper();
+        objectMapper.registerModule(new JavaTimeModule());
+
+        objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
+        objectMapper.configure(SerializationFeature.WRITE_ENUMS_USING_TO_STRING, true);
+
+        // 后台Long值传递给前端精度丢失问题(JS最大精度整数是Math.pow(2,53))
+        SimpleModule simpleModule = new SimpleModule();
+        simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
+        simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
+        objectMapper.registerModule(simpleModule);
+
+        jackson2HttpMessageConverter.setObjectMapper(objectMapper);
+        converters.add(jackson2HttpMessageConverter);
+    }
+
+    @Bean
+    public Validator validator(final AutowireCapableBeanFactory autowireCapableBeanFactory) {
+        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
+                .configure()
+                .failFast(true) // failFast=true 不校验所有参数,只要出现校验失败情况直接返回,不再进行后续参数校验
+                .constraintValidatorFactory(new SpringConstraintValidatorFactory(autowireCapableBeanFactory))
+                .buildValidatorFactory();
+
+        return validatorFactory.getValidator();
+    }
+}

+ 107 - 0
src/main/java/com/youlai/boot/config/WebSocketConfig.java

@@ -0,0 +1,107 @@
+package com.youlai.boot.config;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.jwt.JWTPayload;
+import cn.hutool.jwt.JWTUtil;
+import com.youlai.boot.common.constant.SecurityConstants;
+import com.youlai.boot.system.event.UserConnectionEvent;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpHeaders;
+import org.springframework.messaging.Message;
+import org.springframework.messaging.MessageChannel;
+import org.springframework.messaging.simp.config.ChannelRegistration;
+import org.springframework.messaging.simp.config.MessageBrokerRegistry;
+import org.springframework.messaging.simp.stomp.StompCommand;
+import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
+import org.springframework.messaging.support.ChannelInterceptor;
+import org.springframework.messaging.support.MessageHeaderAccessor;
+import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
+import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
+import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
+
+/**
+ * WebSocket 自动配置类
+ *
+ * @author haoxr
+ * @since 2.4.0
+ */
+// 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递
+@EnableWebSocketMessageBroker
+@Configuration
+@Slf4j
+public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
+
+    private final ApplicationEventPublisher eventPublisher;
+
+    public WebSocketConfig(ApplicationEventPublisher eventPublisher) {
+        this.eventPublisher = eventPublisher;
+    }
+    /**
+     * 注册一个端点,客户端通过这个端点进行连接
+     */
+    @Override
+    public void registerStompEndpoints(StompEndpointRegistry registry) {
+        registry
+                // 注册 /ws 的端点
+                .addEndpoint("/ws")
+                // 允许跨域
+                .setAllowedOriginPatterns("*");
+    }
+
+
+    /**
+     * 配置消息代理
+     */
+    @Override
+    public void configureMessageBroker(MessageBrokerRegistry registry) {
+        // 客户端发送消息的请求前缀
+        registry.setApplicationDestinationPrefixes("/app");
+
+        // 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送
+        registry.enableSimpleBroker("/topic", "/queue");
+
+        // 服务端通知客户端的前缀,可以不设置,默认为user
+        registry.setUserDestinationPrefix("/user");
+    }
+
+
+    /**
+     * 配置客户端入站通道拦截器
+     * <p>
+     * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户
+     *
+     * @param registration 通道注册器
+     */
+    @Override
+    public void configureClientInboundChannel(ChannelRegistration registration) {
+        registration.interceptors(new ChannelInterceptor() {
+            @Override
+            public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel channel) {
+                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
+                if (accessor != null) {
+                    if (StompCommand.CONNECT.equals(accessor.getCommand())) {
+                        String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
+                        if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {
+                            bearerToken = bearerToken.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
+                            String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT);
+                            if (StrUtil.isNotBlank(username)) {
+                                accessor.setUser(() -> username);
+                                eventPublisher.publishEvent(new UserConnectionEvent(this, username, true));
+                            }
+                        }
+                    } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {
+                        if (accessor.getUser() != null) {
+                            String username = accessor.getUser().getName();
+                            eventPublisher.publishEvent(new UserConnectionEvent(this, username, false));
+                        }
+                    }
+                }
+                return ChannelInterceptor.super.preSend(message, channel);
+            }
+        });
+    }
+
+}

+ 61 - 0
src/main/java/com/youlai/boot/config/XxlJobConfig.java

@@ -0,0 +1,61 @@
+package com.youlai.boot.config;
+
+import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * xxl-job config
+ *
+ * @author xuxueli 2017-04-28
+ */
+@Configuration
+@ConditionalOnProperty(name = "xxl.job.enabled") // xxl.job.enabled = true 才会自动装配
+@Slf4j
+public class XxlJobConfig {
+
+    @Value("${xxl.job.admin.addresses}")
+    private String adminAddresses;
+
+    @Value("${xxl.job.accessToken}")
+    private String accessToken;
+
+    @Value("${xxl.job.executor.appname}")
+    private String appname;
+
+    @Value("${xxl.job.executor.address}")
+    private String address;
+
+    @Value("${xxl.job.executor.ip}")
+    private String ip;
+
+    @Value("${xxl.job.executor.port}")
+    private int port;
+
+    @Value("${xxl.job.executor.logpath}")
+    private String logPath;
+
+    @Value("${xxl.job.executor.logretentiondays}")
+    private int logRetentionDays;
+
+
+    @Bean
+    public XxlJobSpringExecutor xxlJobExecutor() {
+        log.info(">>>>>>>>>>> xxl-job config init.");
+        XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
+        xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
+        xxlJobSpringExecutor.setAppname(appname);
+        xxlJobSpringExecutor.setAddress(address);
+        xxlJobSpringExecutor.setIp(ip);
+        xxlJobSpringExecutor.setPort(port);
+        xxlJobSpringExecutor.setAccessToken(accessToken);
+        xxlJobSpringExecutor.setLogPath(logPath);
+        xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
+
+        return xxlJobSpringExecutor;
+    }
+
+}

+ 50 - 0
src/main/java/com/youlai/boot/config/property/AliyunSmsProperties.java

@@ -0,0 +1,50 @@
+package com.youlai.boot.config.property;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Map;
+
+/**
+ * 阿里云短信配置
+ *
+ * @author Ray
+ * @since 2024/8/17
+ */
+@Configuration
+@ConfigurationProperties(prefix = "sms.aliyun")
+@Data
+public class AliyunSmsProperties {
+
+    /**
+     * 阿里云账户的Access Key ID,用于API请求认证
+     */
+    private String accessKeyId;
+
+    /**
+     *阿里云账户的Access Key Secret,用于API请求认证
+     */
+    private String accessKeySecret;
+
+    /**
+     * 阿里云短信服务API的域名 eg: dysmsapi.aliyuncs.com
+     */
+    private String domain;
+
+    /**
+     * 阿里云服务的区域ID,如cn-shanghai
+     */
+    private String regionId;
+
+    /**
+     * 短信签名,必须是已经在阿里云短信服务中注册并通过审核的
+     */
+    private String signName;
+
+    /**
+     * 模板编码
+     */
+    private Map<String, String> templateCodes;
+
+}

+ 92 - 0
src/main/java/com/youlai/boot/config/property/CaptchaProperties.java

@@ -0,0 +1,92 @@
+package com.youlai.boot.config.property;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+/**
+ * 验证码 属性配置
+ *
+ * @author haoxr
+ * @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;
+    }
+
+
+}

+ 96 - 0
src/main/java/com/youlai/boot/config/property/CodegenProperties.java

@@ -0,0 +1,96 @@
+package com.youlai.boot.config.property;
+
+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.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 代码生成配置属性
+ *
+ * @author Ray
+ * @since 2.11.0
+ */
+@Component
+@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;
+
+    }
+
+
+}

+ 89 - 0
src/main/java/com/youlai/boot/config/property/MailProperties.java

@@ -0,0 +1,89 @@
+package com.youlai.boot.config.property;
+
+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;
+            }
+        }
+    }
+}

+ 44 - 0
src/main/java/com/youlai/boot/config/property/SecurityProperties.java

@@ -0,0 +1,44 @@
+package com.youlai.boot.config.property;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.List;
+
+/**
+ * @author haoxr
+ * @since 2024/4/18
+ */
+@Data
+@ConfigurationProperties(prefix = "security")
+public class SecurityProperties {
+
+    /**
+     * 白名单 URL 集合
+     */
+    private List<String> ignoreUrls;
+
+    /**
+     * JWT 配置
+     */
+    private JwtProperty jwt;
+
+
+    /**
+     * JWT 配置
+     */
+    @Data
+    public static class JwtProperty {
+
+        /**
+         * JWT 密钥
+         */
+        private String key;
+
+        /**
+         * JWT 过期时间
+         */
+        private Long ttl;
+
+    }
+}

+ 95 - 0
src/main/java/com/youlai/boot/core/aspect/LogAspect.java

@@ -0,0 +1,95 @@
+package com.youlai.boot.core.aspect;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.date.TimeInterval;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.useragent.UserAgent;
+import cn.hutool.http.useragent.UserAgentUtil;
+import com.youlai.boot.common.constant.SecurityConstants;
+import com.youlai.boot.common.util.IPUtils;
+import com.youlai.boot.system.model.entity.Log;
+import com.youlai.boot.core.security.util.SecurityUtils;
+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.stereotype.Component;
+
+/**
+ * 日志切面
+ *
+ * @author Ray
+ * @since 2024/6/25
+ */
+@Aspect
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class LogAspect {
+
+    private final LogService logService;
+    private final HttpServletRequest request;
+
+    @Pointcut("@annotation(com.youlai.boot.common.annotation.Log)")
+    public void logPointcut() {
+    }
+
+    @Around("logPointcut() && @annotation(logAnnotation)")
+    public Object logExecutionTime(ProceedingJoinPoint joinPoint, com.youlai.boot.common.annotation.Log logAnnotation) throws Throwable {
+        String requestURI = request.getRequestURI();
+
+        Long userId = null;
+        // 非登录请求获取用户ID,登录请求在登录成功后(joinPoint.proceed())获取用户ID
+        if (!SecurityConstants.LOGIN_PATH.equals(requestURI)) {
+            userId = SecurityUtils.getUserId();
+        }
+
+        TimeInterval timer = DateUtil.timer();
+        // 执行方法
+        Object proceed = joinPoint.proceed();
+        long executionTime = timer.interval();
+
+        // 创建日志记录
+        Log log = new Log();
+        log.setModule(logAnnotation.module());
+        log.setContent(logAnnotation.value());
+        log.setRequestUri(requestURI);
+        // 登录方法需要在登录成功后获取用户ID
+        if (userId == null) {
+            userId = SecurityUtils.getUserId();
+        }
+        log.setCreateBy(userId);
+        String ipAddr = IPUtils.getIpAddr(request);
+        if (StrUtil.isNotBlank(ipAddr)) {
+            log.setIp(ipAddr);
+            String region = IPUtils.getRegion(ipAddr);
+            // 中国|0|四川省|成都市|电信 解析省和市
+            if (StrUtil.isNotBlank(region)) {
+                String[] regionArray = region.split("\\|");
+                if (regionArray.length > 2) {
+                    log.setProvince(regionArray[2]);
+                    log.setCity(regionArray[3]);
+                }
+            }
+        }
+        log.setExecutionTime(executionTime);
+        // 获取浏览器和终端系统信息
+        String userAgentString = request.getHeader("User-Agent");
+        UserAgent userAgent = UserAgentUtil.parse(userAgentString);
+        // 系统信息
+        log.setOs(userAgent.getOs().getName());
+        // 浏览器信息
+        log.setBrowser(userAgent.getBrowser().getName());
+        log.setBrowserVersion(userAgent.getBrowser().getVersion(userAgentString));
+        // 保存日志到数据库
+        logService.save(log);
+
+        return proceed;
+    }
+
+
+}

+ 82 - 0
src/main/java/com/youlai/boot/core/aspect/RepeatSubmitAspect.java

@@ -0,0 +1,82 @@
+package com.youlai.boot.core.aspect;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.jwt.JWTUtil;
+import cn.hutool.jwt.RegisteredPayload;
+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 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 haoxr
+ * @since 2.3.0
+ */
+@Aspect
+@Component
+@Slf4j
+@RequiredArgsConstructor
+public class RepeatSubmitAspect {
+
+    private final RedissonClient redissonClient;
+
+    /**
+     * 防重复提交切点
+     */
+    @Pointcut("@annotation(repeatSubmit)")
+    public void preventDuplicateSubmitPointCut(RepeatSubmit repeatSubmit) {
+        log.info("定义防重复提交切点");
+    }
+
+    @Around("preventDuplicateSubmitPointCut(repeatSubmit)")
+    public Object doAround(ProceedingJoinPoint pjp, RepeatSubmit repeatSubmit) throws Throwable {
+
+        String resubmitLockKey = generateResubmitLockKey();
+        if (resubmitLockKey != null) {
+            int expire = repeatSubmit.expire(); // 防重提交锁过期时间
+            RLock lock = redissonClient.getLock(resubmitLockKey);
+            boolean lockResult = lock.tryLock(0, expire, TimeUnit.SECONDS); // 获取锁失败,直接返回 false
+            if (!lockResult) {
+                throw new BusinessException(ResultCode.REPEAT_SUBMIT_ERROR); // 抛出重复提交提示信息
+            }
+        }
+        return pjp.proceed();
+    }
+
+
+    /**
+     * 获取重复提交锁的 key
+     */
+    private String generateResubmitLockKey() {
+        String resubmitLockKey = null;
+        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
+
+        String token = request.getHeader(HttpHeaders.AUTHORIZATION);
+        if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
+            token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
+            // 从 JWT Token 中获取 jti
+            String jti = (String) JWTUtil.parseToken(token).getPayload(RegisteredPayload.JWT_ID);
+            resubmitLockKey = RedisConstants.RESUBMIT_LOCK_PREFIX + jti + ":" + request.getMethod() + "-" + request.getRequestURI();
+        }
+        return resubmitLockKey;
+    }
+
+}

+ 80 - 0
src/main/java/com/youlai/boot/core/filter/RateLimiterFilter.java

@@ -0,0 +1,80 @@
+package com.youlai.boot.core.filter;
+
+import com.youlai.boot.common.constant.RedisConstants;
+import com.youlai.boot.common.result.ResultCode;
+import com.youlai.boot.common.util.IPUtils;
+import com.youlai.boot.common.util.ResponseUtils;
+import com.youlai.boot.system.service.ConfigService;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * IP限流过滤器
+ *
+ * @author Theo
+ * @since 2024/08/10 14:38
+ */
+@Slf4j
+public class RateLimiterFilter extends OncePerRequestFilter {
+
+    private final RedisTemplate<String, Object> redisTemplate;
+    private final ConfigService configService;
+
+    public RateLimiterFilter(RedisTemplate<String, Object> redisTemplate, ConfigService configService) {
+        this.redisTemplate = redisTemplate;
+        this.configService = configService;
+    }
+
+    /**
+     * 确认是否限流方法
+     * 默认情况下:限制同一个IP的QPS最大为10,可以通过修改系统配置进行调整
+     * 这里也可以进行扩展,比如redis记录同一个ip每天出发限流的上限次数,记录在redis中,达到某个阈值后,进行永久封禁这个ip
+     *
+     * @param ip ip地址
+     * @return  是否限流
+     */
+    public boolean rateLimit(String ip) {
+        String key = RedisConstants.IP_RATE_LIMITER_KEY + ip;
+        Long count = redisTemplate.opsForValue().increment(key);
+        if (count == null || count == 1) {
+            redisTemplate.expire(key,1, TimeUnit.SECONDS);
+        }
+        Object systemConfig = configService.getSystemConfig(RedisConstants.IP_QPS_THRESHOLD_LIMIT_KEY);
+        long limit = 10;
+        if(systemConfig != null){
+            limit =  Long.parseLong(systemConfig.toString());
+        }else{
+            log.warn("[RedisRateLimiterFilter.rateLimit]系统配置中未配置IP请求限制QPS阈值配置,使用默认值:{},请检查配置项:{}",
+                    limit,RedisConstants.IP_QPS_THRESHOLD_LIMIT_KEY);
+        }
+        return count != null && count > limit;
+    }
+
+    /**
+     * IP限流过滤器
+     * 默认情况下:限制同一个IP在一分钟内只能访问10次,可以通过修改系统配置进行调整
+     *
+     * @param request 请求体
+     * @param response 响应体
+     * @param filterChain 过滤器链
+     */
+    @Override
+    protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response,
+                                    @NotNull FilterChain filterChain) throws ServletException, IOException {
+        String ip = IPUtils.getIpAddr(request);
+        if (rateLimit(ip)) {
+            ResponseUtils.writeErrMsg(response, ResultCode.FLOW_LIMITING);
+            return;
+        }
+        filterChain.doFilter(request, response);
+    }
+}

+ 38 - 0
src/main/java/com/youlai/boot/core/filter/RequestLogFilter.java

@@ -0,0 +1,38 @@
+package com.youlai.boot.core.filter;
+
+import com.youlai.boot.common.util.IPUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.filter.CommonsRequestLoggingFilter;
+
+/**
+ * 请求日志打印过滤器
+ *
+ * @author haoxr
+ * @since 2023/03/03
+ */
+@Configuration
+@Slf4j
+public class RequestLogFilter extends CommonsRequestLoggingFilter {
+
+    @Override
+    protected boolean shouldLog(HttpServletRequest request) {
+        // 设置日志输出级别,默认debug
+        return this.logger.isInfoEnabled();
+    }
+
+    @Override
+    protected void beforeRequest(HttpServletRequest request, String message) {
+        String requestURI = request.getRequestURI();
+        String ip = IPUtils.getIpAddr(request);
+        log.info("request,ip:{}, uri: {}", ip, requestURI);
+        super.beforeRequest(request, message);
+    }
+
+    @Override
+    protected void afterRequest(HttpServletRequest request, String message) {
+        super.afterRequest(request, message);
+    }
+
+}

+ 100 - 0
src/main/java/com/youlai/boot/core/handler/MyDataPermissionHandler.java

@@ -0,0 +1,100 @@
+package com.youlai.boot.core.handler;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.toolkit.StringPool;
+import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
+import com.youlai.boot.common.annotation.DataPermission;
+import com.youlai.boot.common.base.IBaseEnum;
+import com.youlai.boot.common.enums.DataScopeEnum;
+import com.youlai.boot.core.security.util.SecurityUtils;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+
+import java.lang.reflect.Method;
+
+/**
+ * 数据权限控制器
+ *
+ * @author zc
+ * @since 2021-12-10 13:28
+ */
+@Slf4j
+public class MyDataPermissionHandler implements DataPermissionHandler {
+
+    @Override
+    @SneakyThrows
+    public Expression getSqlSegment(Expression where, String mappedStatementId) {
+
+        Class<?> clazz = Class.forName(mappedStatementId.substring(0, mappedStatementId.lastIndexOf(StringPool.DOT)));
+        String methodName = mappedStatementId.substring(mappedStatementId.lastIndexOf(StringPool.DOT) + 1);
+        Method[] methods = clazz.getDeclaredMethods();
+        for (Method method : methods) {
+            if (method.getName().equals(methodName)) {
+            DataPermission annotation = method.getAnnotation(DataPermission.class);
+            // 如果没有注解或者是超级管理员,直接返回
+            if (annotation == null || SecurityUtils.isRoot() ) {
+                return where;
+            }
+                return dataScopeFilter(annotation.deptAlias(), annotation.deptIdColumnName(), annotation.userAlias(), annotation.userIdColumnName(), where);
+            }
+        }
+        return where;
+    }
+
+    /**
+     * 构建过滤条件
+     *
+     * @param where 当前查询条件
+     * @return 构建后查询条件
+     */
+    @SneakyThrows
+    public static Expression dataScopeFilter(String deptAlias, String deptIdColumnName, String userAlias, String userIdColumnName, Expression where) {
+
+
+        String deptColumnName = StrUtil.isNotBlank(deptAlias) ? (deptAlias + StringPool.DOT + deptIdColumnName) : deptIdColumnName;
+        String userColumnName = StrUtil.isNotBlank(userAlias) ? (userAlias + StringPool.DOT + userIdColumnName) : userIdColumnName;
+
+        // 获取当前用户的数据权限
+        Integer dataScope = SecurityUtils.getDataScope();
+
+        DataScopeEnum dataScopeEnum = IBaseEnum.getEnumByValue(dataScope, DataScopeEnum.class);
+
+        Long deptId, userId;
+        String appendSqlStr;
+        switch (dataScopeEnum) {
+            case ALL:
+                return where;
+            case DEPT:
+                deptId = SecurityUtils.getDeptId();
+                appendSqlStr = deptColumnName + StringPool.EQUALS + deptId;
+                break;
+            case SELF:
+                userId = SecurityUtils.getUserId();
+                appendSqlStr = userColumnName + StringPool.EQUALS + userId;
+                break;
+            // 默认部门及子部门数据权限
+            default:
+                deptId = SecurityUtils.getDeptId();
+                appendSqlStr = deptColumnName + " IN ( SELECT id FROM sys_dept WHERE id = " + deptId + " OR FIND_IN_SET( " + deptId + " , tree_path ) )";
+                break;
+        }
+
+        if (StrUtil.isBlank(appendSqlStr)) {
+            return where;
+        }
+
+        Expression appendExpression = CCJSqlParserUtil.parseCondExpression(appendSqlStr);
+
+        if (where == null) {
+            return appendExpression;
+        }
+
+        return new AndExpression(where, appendExpression);
+    }
+
+
+}
+

+ 39 - 0
src/main/java/com/youlai/boot/core/handler/MyMetaObjectHandler.java

@@ -0,0 +1,39 @@
+package com.youlai.boot.core.handler;
+
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import org.apache.ibatis.reflection.MetaObject;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+
+/**
+ * mybatis-plus 字段自动填充
+ *
+ * @author haoxr
+ * @since 2022/10/14
+ */
+@Component
+public class MyMetaObjectHandler implements MetaObjectHandler {
+
+    /**
+     * 新增填充创建时间
+     *
+     * @param metaObject 元数据
+     */
+    @Override
+    public void insertFill(MetaObject metaObject) {
+        this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
+        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
+    }
+
+    /**
+     * 更新填充更新时间
+     *
+     * @param metaObject 元数据
+     */
+    @Override
+    public void updateFill(MetaObject metaObject) {
+        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
+    }
+
+}

+ 25 - 0
src/main/java/com/youlai/boot/core/security/exception/MyAccessDeniedHandler.java

@@ -0,0 +1,25 @@
+package com.youlai.boot.core.security.exception;
+
+import com.youlai.boot.common.result.ResultCode;
+import com.youlai.boot.common.util.ResponseUtils;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.stereotype.Component;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Spring Security访问异常处理器
+ *
+ * @author haoxr
+ * @since 2022/10/18
+ */
+@Component
+public class MyAccessDeniedHandler implements AccessDeniedHandler {
+    @Override
+    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
+        ResponseUtils.writeErrMsg(response, ResultCode.ACCESS_UNAUTHORIZED);
+    }
+}

+ 40 - 0
src/main/java/com/youlai/boot/core/security/exception/MyAuthenticationEntryPoint.java

@@ -0,0 +1,40 @@
+package com.youlai.boot.core.security.exception;
+
+import com.youlai.boot.common.result.ResultCode;
+import com.youlai.boot.common.util.ResponseUtils;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Component;
+
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * 认证异常处理
+ *
+ * @author haoxr
+ * @since 2.0.0
+ */
+@Component
+public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
+        @Override
+    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
+        int status = response.getStatus();
+        if (status == HttpServletResponse.SC_NOT_FOUND) {
+            // 资源不存在
+            ResponseUtils.writeErrMsg(response, ResultCode.RESOURCE_NOT_FOUND);
+        } else {
+
+            if(authException instanceof BadCredentialsException){
+                // 用户名或密码错误
+                ResponseUtils.writeErrMsg(response, ResultCode.USERNAME_OR_PASSWORD_ERROR);
+            }else {
+                // 未认证或者token过期
+                ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
+            }
+        }
+    }
+}

+ 75 - 0
src/main/java/com/youlai/boot/core/security/filter/CaptchaValidationFilter.java

@@ -0,0 +1,75 @@
+package com.youlai.boot.core.security.filter;
+
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.core.util.StrUtil;
+import com.youlai.boot.common.constant.RedisConstants;
+import com.youlai.boot.common.constant.SecurityConstants;
+import com.youlai.boot.common.constant.SymbolConstant;
+import com.youlai.boot.common.result.ResultCode;
+import com.youlai.boot.common.util.ResponseUtils;
+import com.youlai.boot.system.service.ConfigService;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+
+/**
+ * 图形验证码校验过滤器
+ *
+ * @author haoxr
+ * @since 2022/10/1
+ */
+public class CaptchaValidationFilter extends OncePerRequestFilter {
+
+    private static final AntPathRequestMatcher LOGIN_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(SecurityConstants.LOGIN_PATH, "POST");
+
+    public static final String CAPTCHA_CODE_PARAM_NAME = "captchaCode";
+    public static final String CAPTCHA_KEY_PARAM_NAME = "captchaKey";
+
+    private final RedisTemplate<String, Object> redisTemplate;
+
+    private final CodeGenerator codeGenerator;
+
+    public CaptchaValidationFilter(RedisTemplate<String, Object> redisTemplate, CodeGenerator codeGenerator) {
+        this.redisTemplate = redisTemplate;
+        this.codeGenerator = codeGenerator;
+    }
+
+
+    @Override
+    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
+        // 检验登录接口的验证码
+        if (LOGIN_PATH_REQUEST_MATCHER.matches(request)) {
+            // 请求中的验证码
+            String captchaCode = request.getParameter(CAPTCHA_CODE_PARAM_NAME);
+            // TODO 兼容没有验证码的版本(线上请移除这个判断)
+            if (StrUtil.isBlank(captchaCode)) {
+                chain.doFilter(request, response);
+                return;
+            }
+            // 缓存中的验证码
+            String verifyCodeKey = request.getParameter(CAPTCHA_KEY_PARAM_NAME);
+            String cacheVerifyCode = (String) redisTemplate.opsForValue().get(SecurityConstants.CAPTCHA_CODE_PREFIX + verifyCodeKey);
+            if (cacheVerifyCode == null) {
+                ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_TIMEOUT);
+            } else {
+                // 验证码比对
+                if (codeGenerator.verify(cacheVerifyCode, captchaCode)) {
+                    chain.doFilter(request, response);
+                } else {
+                    ResponseUtils.writeErrMsg(response, ResultCode.VERIFY_CODE_ERROR);
+                }
+            }
+        } else {
+            // 非登录接口放行
+            chain.doFilter(request, response);
+        }
+    }
+
+}

+ 83 - 0
src/main/java/com/youlai/boot/core/security/filter/JwtValidationFilter.java

@@ -0,0 +1,83 @@
+package com.youlai.boot.core.security.filter;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.jwt.JWT;
+import cn.hutool.jwt.JWTPayload;
+import cn.hutool.jwt.JWTUtil;
+import com.youlai.boot.common.constant.SecurityConstants;
+import com.youlai.boot.common.result.ResultCode;
+import com.youlai.boot.core.security.util.JwtUtils;
+import com.youlai.boot.common.util.ResponseUtils;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import java.io.IOException;
+
+/**
+ * JWT token 校验过滤器
+ *
+ * @author Ray Hao
+ * @since 2023/9/13
+ */
+public class JwtValidationFilter extends OncePerRequestFilter {
+
+    private final RedisTemplate<String, Object> redisTemplate;
+
+    private final byte[] secretKey;
+
+    public JwtValidationFilter(RedisTemplate<String, Object> redisTemplate, String secretKey) {
+        this.redisTemplate = redisTemplate;
+        this.secretKey = secretKey.getBytes();
+    }
+
+
+    /**
+     * 从请求中获取 JWT Token,校验 JWT Token 是否合法
+     * <p>
+     * 如果合法则将 Authentication 设置到 Spring Security Context 上下文中
+     * 如果不合法则清空 Spring Security Context 上下文,并直接返回响应
+     */
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+        String token = request.getHeader(HttpHeaders.AUTHORIZATION);
+        try {
+            if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
+                // 去除 Bearer 前缀
+                token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
+                // 解析 Token
+                JWT jwt = JWTUtil.parseToken(token);
+                // 检查 Token 是否有效(验签 + 是否过期)
+                boolean isValidate = jwt.setKey(secretKey).validate(0);
+                if (!isValidate) {
+                    ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
+                    return;
+                }
+                // 检查 Token 是否已被加入黑名单(注销)
+                JSONObject payloads = jwt.getPayloads();
+                String jti = payloads.getStr(JWTPayload.JWT_ID);
+                boolean isTokenBlacklisted = Boolean.TRUE.equals(redisTemplate.hasKey(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti));
+                if (isTokenBlacklisted) {
+                    ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
+                    return;
+                }
+                // Token 有效将其解析为 Authentication 对象,并设置到 Spring Security 上下文中
+                Authentication authentication = JwtUtils.getAuthentication(payloads);
+                SecurityContextHolder.getContext().setAuthentication(authentication);
+            }
+        } catch (Exception e) {
+            SecurityContextHolder.clearContext();
+            ResponseUtils.writeErrMsg(response, ResultCode.TOKEN_INVALID);
+            return;
+        }
+        // Token有效或无Token时继续执行过滤链
+        filterChain.doFilter(request, response);
+    }
+}

+ 85 - 0
src/main/java/com/youlai/boot/core/security/model/SysUserDetails.java

@@ -0,0 +1,85 @@
+package com.youlai.boot.core.security.model;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.ObjectUtil;
+import com.youlai.boot.system.model.dto.UserAuthInfo;
+import lombok.Data;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Spring Security 用户对象
+ *
+ * @author haoxr
+ * @since 3.0.0
+ */
+@Data
+@NoArgsConstructor
+public class SysUserDetails implements UserDetails {
+
+    @Getter
+    private Long userId;
+
+    private String username;
+
+    private String password;
+
+    private Boolean enabled;
+
+    private Collection<SimpleGrantedAuthority> authorities;
+
+    private Set<String> perms;
+
+    private Long deptId;
+
+    private Integer dataScope;
+
+    public SysUserDetails(UserAuthInfo user) {
+        this.userId = user.getUserId();
+        Set<String> roles = user.getRoles();
+        Set<SimpleGrantedAuthority> authorities;
+        if (CollectionUtil.isNotEmpty(roles)) {
+            authorities = roles.stream()
+                    .map(role -> new SimpleGrantedAuthority("ROLE_" + role)) // 标识角色
+                    .collect(Collectors.toSet());
+        } else {
+            authorities = Collections.emptySet();
+        }
+        this.authorities = authorities;
+        this.username = user.getUsername();
+        this.password = user.getPassword();
+        this.enabled = ObjectUtil.equal(user.getStatus(), 1);
+        this.perms = user.getPerms();
+        this.deptId = user.getDeptId();
+        this.dataScope = user.getDataScope();
+    }
+
+
+    @Override
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        return this.authorities;
+    }
+
+    @Override
+    public String getPassword() {
+        return this.password;
+    }
+
+    @Override
+    public String getUsername() {
+        return this.username;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return this.enabled;
+    }
+}

+ 97 - 0
src/main/java/com/youlai/boot/core/security/service/PermissionService.java

@@ -0,0 +1,97 @@
+package com.youlai.boot.core.security.service;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+import com.youlai.boot.common.constant.SecurityConstants;
+import com.youlai.boot.core.security.util.SecurityUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.util.PatternMatchUtils;
+
+import java.util.*;
+
+/**
+ * SpringSecurity 权限校验
+ *
+ * @author haoxr
+ * @since 2022/2/22
+ */
+@Component("ss")
+@RequiredArgsConstructor
+@Slf4j
+public class PermissionService {
+
+    private final RedisTemplate<String, Object> redisTemplate;
+
+    /**
+     * 判断当前登录用户是否拥有操作权限
+     *
+     * @param requiredPerm 所需权限
+     * @return 是否有权限
+     */
+    public boolean hasPerm(String requiredPerm) {
+
+        if (StrUtil.isBlank(requiredPerm)) {
+            return false;
+        }
+        // 超级管理员放行
+        if (SecurityUtils.isRoot()) {
+            return true;
+        }
+
+        // 获取当前登录用户的角色编码集合
+        Set<String> roleCodes = SecurityUtils.getRoles();
+        if (CollectionUtil.isEmpty(roleCodes)) {
+            return false;
+        }
+
+        // 获取当前登录用户的所有角色的权限列表
+        Set<String> rolePerms = this.getRolePermsFormCache(roleCodes);
+        if (CollectionUtil.isEmpty(rolePerms)) {
+            return false;
+        }
+        // 判断当前登录用户的所有角色的权限列表中是否包含所需权限
+        boolean hasPermission = rolePerms.stream()
+                .anyMatch(rolePerm ->
+                        // 匹配权限,支持通配符(* 等)
+                        PatternMatchUtils.simpleMatch(rolePerm, requiredPerm)
+                );
+
+        if (!hasPermission) {
+            log.error("用户无操作权限");
+        }
+        return hasPermission;
+    }
+
+
+    /**
+     * 从缓存中获取角色权限列表
+     *
+     * @param roleCodes 角色编码集合
+     * @return 角色权限列表
+     */
+    public Set<String> getRolePermsFormCache(Set<String> roleCodes) {
+        // 检查输入是否为空
+        if (CollectionUtil.isEmpty(roleCodes)) {
+            return Collections.emptySet();
+        }
+
+        Set<String> perms = new HashSet<>();
+        // 从缓存中一次性获取所有角色的权限
+        Collection<Object> roleCodesAsObjects = new ArrayList<>(roleCodes);
+        List<Object> rolePermsList = redisTemplate.opsForHash().multiGet(SecurityConstants.ROLE_PERMS_PREFIX, roleCodesAsObjects);
+
+        for (Object rolePermsObj : rolePermsList) {
+            if (rolePermsObj instanceof Set) {
+                @SuppressWarnings("unchecked")
+                Set<String> rolePerms = (Set<String>) rolePermsObj;
+                perms.addAll(rolePerms);
+            }
+        }
+
+        return perms;
+    }
+
+}

+ 49 - 0
src/main/java/com/youlai/boot/core/security/service/SysUserDetailsService.java

@@ -0,0 +1,49 @@
+package com.youlai.boot.core.security.service;
+
+import com.youlai.boot.core.security.model.SysUserDetails;
+import com.youlai.boot.system.model.dto.UserAuthInfo;
+import com.youlai.boot.system.service.UserService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+
+/**
+ * 系统用户认证 DetailsService
+ *
+ * @author Ray
+ * @since 2021/10/19
+ */
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class SysUserDetailsService implements UserDetailsService {
+
+    private final UserService userService;
+
+    /**
+     * 根据用户名获取用户信息
+     *
+     * @param username 用户名
+     * @return 用户信息
+     * @throws UsernameNotFoundException 用户名未找到异常
+     */
+    @Override
+    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+        try {
+            UserAuthInfo userAuthInfo = userService.getUserAuthInfo(username);
+            if (userAuthInfo == null) {
+                throw new UsernameNotFoundException(username);
+            }
+            return new SysUserDetails(userAuthInfo);
+        } catch (Exception e) {
+            e.printStackTrace();
+            // 记录异常日志
+            log.error("认证异常:{}", e.getMessage());
+            // 抛出异常
+            throw e;
+        }
+    }
+}

+ 110 - 0
src/main/java/com/youlai/boot/core/security/util/JwtUtils.java

@@ -0,0 +1,110 @@
+package com.youlai.boot.core.security.util;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.jwt.JWTPayload;
+import cn.hutool.jwt.JWTUtil;
+import com.youlai.boot.common.constant.JwtClaimConstants;
+import com.youlai.boot.core.security.model.SysUserDetails;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * JWT Token 工具类
+ *
+ * @author Ray Hao
+ * @since 2.6.0
+ */
+@Component
+public class JwtUtils {
+
+    /**
+     * JWT 加解密使用的密钥
+     */
+    private static byte[] key;
+
+
+    /**
+     * JWT Token 的有效时间(单位:秒)
+     */
+    private static int ttl;
+
+
+    @Value("${security.jwt.key}")
+    public void setKey(String key) {
+        JwtUtils.key = key.getBytes();
+    }
+
+    @Value("${security.jwt.ttl}")
+    public void setTtl(Integer ttl) {
+        JwtUtils.ttl = ttl;
+    }
+
+    /**
+     * 生成 JWT Token
+     *
+     * @param authentication 用户认证信息
+     * @return Token 字符串
+     */
+    public static String createToken(Authentication authentication) {
+
+        SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal();
+
+        Map<String, Object> payload = new HashMap<>();
+        payload.put(JwtClaimConstants.USER_ID, userDetails.getUserId()); // 用户ID
+        payload.put(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); // 部门ID
+        payload.put(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 数据权限范围
+
+        // claims 中添加角色信息
+        Set<String> roles = userDetails.getAuthorities().stream()
+                .map(GrantedAuthority::getAuthority)
+                .collect(Collectors.toSet());
+        payload.put(JwtClaimConstants.AUTHORITIES, roles);
+
+        Date now = new Date();
+        payload.put(JWTPayload.ISSUED_AT, now);
+
+        // 设置过期时间 -1 表示永不过期
+        if (ttl != -1) {
+            Date expiration = DateUtil.offsetSecond(now, ttl);
+            payload.put(JWTPayload.EXPIRES_AT, expiration);
+        }
+        payload.put(JWTPayload.SUBJECT, authentication.getName());
+        payload.put(JWTPayload.JWT_ID, IdUtil.simpleUUID());
+        return JWTUtil.createToken(payload, key);
+    }
+
+
+    /**
+     * 从 JWT Token 中解析 Authentication  用户认证信息
+     *
+     * @param payloads JWT 载体
+     * @return 用户认证信息
+     */
+    public static UsernamePasswordAuthenticationToken getAuthentication(JSONObject payloads) {
+        SysUserDetails userDetails = new SysUserDetails();
+        userDetails.setUserId(payloads.getLong(JwtClaimConstants.USER_ID)); // 用户ID
+        userDetails.setDeptId(payloads.getLong(JwtClaimConstants.DEPT_ID)); // 部门ID
+        userDetails.setDataScope(payloads.getInt(JwtClaimConstants.DATA_SCOPE)); // 数据权限范围
+
+        userDetails.setUsername(payloads.getStr(JWTPayload.SUBJECT)); // 用户名
+        // 角色集合
+        Set<SimpleGrantedAuthority> authorities = payloads.getJSONArray(JwtClaimConstants.AUTHORITIES)
+                .stream()
+                .map(authority -> new SimpleGrantedAuthority(Convert.toStr(authority)))
+                .collect(Collectors.toSet());
+
+        return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
+    }
+
+
+}

+ 109 - 0
src/main/java/com/youlai/boot/core/security/util/SecurityUtils.java

@@ -0,0 +1,109 @@
+package com.youlai.boot.core.security.util;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+import com.youlai.boot.common.constant.SystemConstants;
+import com.youlai.boot.core.security.model.SysUserDetails;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Spring Security 工具类
+ *
+ * @author Ray
+ * @since 2021/1/10
+ */
+public class SecurityUtils {
+
+    /**
+     * 获取当前登录人信息
+     *
+     * @return Optional<SysUserDetails>
+     */
+    public static Optional<SysUserDetails> getUser() {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication != null) {
+            Object principal = authentication.getPrincipal();
+            if (principal instanceof SysUserDetails) {
+                return Optional.of((SysUserDetails) principal);
+            }
+        }
+        return Optional.empty();
+    }
+
+
+    /**
+     * 获取用户ID
+     *
+     * @return Long
+     */
+    public static Long getUserId() {
+        return getUser().map(SysUserDetails::getUserId).orElse(null);
+    }
+
+
+    /**
+     * 获取用户账号
+     *
+     * @return String 用户账号
+     */
+    public static String getUsername() {
+        return getUser().map(SysUserDetails::getUsername).orElse(null);
+    }
+
+
+    /**
+     * 获取部门ID
+     *
+     * @return Long
+     */
+    public static Long getDeptId() {
+        return getUser().map(SysUserDetails::getDeptId).orElse(null);
+    }
+
+    /**
+     * 获取数据权限范围
+     *
+     * @return Integer
+     */
+    public static Integer getDataScope() {
+        return getUser().map(SysUserDetails::getDataScope).orElse(null);
+    }
+
+
+    /**
+     * 获取用户角色集合
+     *
+     * @return 角色集合
+     */
+    public static Set<String> getRoles() {
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication != null) {
+            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
+            if (CollectionUtil.isNotEmpty(authorities)) {
+                return authorities.stream().filter(item -> item.getAuthority().startsWith("ROLE_"))
+                        .map(item -> StrUtil.removePrefix(item.getAuthority(), "ROLE_"))
+                        .collect(Collectors.toSet());
+            }
+        }
+        return Collections.EMPTY_SET;
+    }
+
+    /**
+     * 是否超级管理员
+     * <p>
+     * 超级管理员忽视任何权限判断
+     */
+    public static boolean isRoot() {
+        Set<String> roles = getRoles();
+        return roles.contains(SystemConstants.ROOT_ROLE_CODE);
+    }
+
+}

+ 56 - 0
src/main/java/com/youlai/boot/module/auth/controller/AuthController.java

@@ -0,0 +1,56 @@
+package com.youlai.boot.module.auth.controller;
+
+import com.youlai.boot.common.enums.LogModuleEnum;
+import com.youlai.boot.common.result.Result;
+import com.youlai.boot.module.auth.service.AuthService;
+import com.youlai.boot.system.model.dto.CaptchaResult;
+import com.youlai.boot.system.model.dto.LoginResult;
+import com.youlai.boot.common.annotation.Log;
+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.*;
+
+/**
+ * 认证控制层
+ *
+ * @author Ray
+ * @since 2022/10/16
+ */
+@Tag(name = "01.认证中心")
+@RestController
+@RequestMapping("/api/v1/auth")
+@RequiredArgsConstructor
+@Slf4j
+public class AuthController {
+
+    private final AuthService authService;
+
+    @Operation(summary = "登录")
+    @PostMapping("/login")
+    @Log(value = "登录", module = LogModuleEnum.LOGIN)
+    public Result<LoginResult> login(
+            @Parameter(description = "用户名", example = "admin") @RequestParam String username,
+            @Parameter(description = "密码", example = "123456") @RequestParam String password
+    ) {
+        LoginResult loginResult = authService.login(username, password);
+        return Result.success(loginResult);
+    }
+
+    @Operation(summary = "注销")
+    @DeleteMapping("/logout")
+    @Log(value = "注销", module = LogModuleEnum.LOGIN)
+    public Result<?> logout() {
+        authService.logout();
+        return Result.success();
+    }
+
+    @Operation(summary = "获取验证码")
+    @GetMapping("/captcha")
+    public Result<CaptchaResult> getCaptcha() {
+        CaptchaResult captcha = authService.getCaptcha();
+        return Result.success(captcha);
+    }
+}

+ 27 - 0
src/main/java/com/youlai/boot/module/auth/enums/CaptchaTypeEnum.java

@@ -0,0 +1,27 @@
+package com.youlai.boot.module.auth.enums;
+
+/**
+ * EasyCaptcha 验证码类型枚举
+ *
+ * @author haoxr
+ * @since 2.5.1
+ */
+public enum CaptchaTypeEnum {
+
+    /**
+     * 圆圈干扰验证码
+     */
+    CIRCLE,
+    /**
+     * GIF验证码
+     */
+    GIF,
+    /**
+     * 干扰线验证码
+     */
+    LINE,
+    /**
+     * 扭曲干扰验证码
+     */
+    SHEAR
+}

+ 34 - 0
src/main/java/com/youlai/boot/module/auth/service/AuthService.java

@@ -0,0 +1,34 @@
+package com.youlai.boot.module.auth.service;
+
+import com.youlai.boot.system.model.dto.CaptchaResult;
+import com.youlai.boot.system.model.dto.LoginResult;
+
+/**
+ * 认证服务接口
+ *
+ * @author haoxr
+ * @since 2.4.0
+ */
+public interface AuthService {
+
+    /**
+     * 登录
+     *
+     * @param username 用户名
+     * @param password 密码
+     * @return 登录结果
+     */
+    LoginResult login(String username, String password);
+
+    /**
+     * 登出
+     */
+    void logout();
+
+    /**
+     * 获取验证码
+     *
+     * @return 验证码
+     */
+    CaptchaResult getCaptcha();
+}

+ 153 - 0
src/main/java/com/youlai/boot/module/auth/service/impl/AuthServiceImpl.java

@@ -0,0 +1,153 @@
+package com.youlai.boot.module.auth.service.impl;
+
+import cn.hutool.captcha.AbstractCaptcha;
+import cn.hutool.captcha.CaptchaUtil;
+import cn.hutool.captcha.generator.CodeGenerator;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.jwt.JWTPayload;
+import cn.hutool.jwt.JWTUtil;
+import com.youlai.boot.common.constant.SecurityConstants;
+import com.youlai.boot.module.auth.enums.CaptchaTypeEnum;
+import com.youlai.boot.module.auth.service.AuthService;
+import com.youlai.boot.system.model.dto.CaptchaResult;
+import com.youlai.boot.system.model.dto.LoginResult;
+import com.youlai.boot.config.property.CaptchaProperties;
+import com.youlai.boot.core.security.util.JwtUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.http.HttpHeaders;
+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.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.awt.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 认证服务实现类
+ *
+ * @author haoxr
+ * @since 2.4.0
+ */
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class AuthServiceImpl implements AuthService {
+
+    private final AuthenticationManager authenticationManager;
+    private final RedisTemplate<String, Object> redisTemplate;
+    private final CodeGenerator codeGenerator;
+    private final Font captchaFont;
+    private final CaptchaProperties captchaProperties;
+
+    /**
+     * 登录
+     *
+     * @param username 用户名
+     * @param password 密码
+     * @return 登录结果
+     */
+    @Override
+    public LoginResult login(String username, String password) {
+        // 创建认证令牌对象
+        UsernamePasswordAuthenticationToken authenticationToken =
+                new UsernamePasswordAuthenticationToken(username.toLowerCase().trim(), password);
+        // 执行用户认证
+        Authentication authentication = authenticationManager.authenticate(authenticationToken);
+        // 认证成功后生成JWT令牌
+        String accessToken = JwtUtils.createToken(authentication);
+        // 将认证信息存入Security上下文,便于在AOP(如日志记录)中获取当前用户信息
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+        // 返回包含JWT令牌的登录结果
+        return LoginResult.builder()
+                .tokenType("Bearer")
+                .accessToken(accessToken)
+                .build();
+    }
+
+    /**
+     * 注销
+     */
+    @Override
+    public void logout() {
+        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
+        String token = request.getHeader(HttpHeaders.AUTHORIZATION);
+        if (StrUtil.isNotBlank(token) && token.startsWith(SecurityConstants.JWT_TOKEN_PREFIX)) {
+            token = token.substring(SecurityConstants.JWT_TOKEN_PREFIX.length());
+            // 解析Token以获取有效载荷(payload)
+            JSONObject payloads = JWTUtil.parseToken(token).getPayloads();
+            // 解析 Token 获取 jti(JWT ID) 和 exp(过期时间)
+            String jti = payloads.getStr(JWTPayload.JWT_ID);
+            Long expiration = payloads.getLong(JWTPayload.EXPIRES_AT); // 过期时间(秒)
+            // 如果exp存在,则计算Token剩余有效时间
+            if (expiration != null) {
+                long currentTimeSeconds = System.currentTimeMillis() / 1000;
+                if (expiration < currentTimeSeconds) {
+                    // Token已过期,不再加入黑名单
+                    return;
+                }
+                // 将Token的jti加入黑名单,并设置剩余有效时间,使其在过期后自动从黑名单移除
+                long ttl = expiration - currentTimeSeconds;
+                redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null, ttl, TimeUnit.SECONDS);
+            } else {
+                // 如果exp不存在,说明Token永不过期,则永久加入黑名单
+                redisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN_PREFIX + jti, null);
+            }
+        }
+        // 清空Spring Security上下文
+        SecurityContextHolder.clearContext();
+    }
+
+    /**
+     * 获取验证码
+     *
+     * @return 验证码
+     */
+    @Override
+    public CaptchaResult getCaptcha() {
+
+        String captchaType = captchaProperties.getType();
+        int width = captchaProperties.getWidth();
+        int height = captchaProperties.getHeight();
+        int interfereCount = captchaProperties.getInterfereCount();
+        int codeLength = captchaProperties.getCode().getLength();
+
+        AbstractCaptcha captcha;
+        if (CaptchaTypeEnum.CIRCLE.name().equalsIgnoreCase(captchaType)) {
+            captcha = CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount);
+        } else if (CaptchaTypeEnum.GIF.name().equalsIgnoreCase(captchaType)) {
+            captcha = CaptchaUtil.createGifCaptcha(width, height, codeLength);
+        } else if (CaptchaTypeEnum.LINE.name().equalsIgnoreCase(captchaType)) {
+            captcha = CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount);
+        } else if (CaptchaTypeEnum.SHEAR.name().equalsIgnoreCase(captchaType)) {
+            captcha = CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount);
+        } else {
+            throw new IllegalArgumentException("Invalid captcha type: " + captchaType);
+        }
+        captcha.setGenerator(codeGenerator);
+        captcha.setTextAlpha(captchaProperties.getTextAlpha());
+        captcha.setFont(captchaFont);
+
+        String captchaCode = captcha.getCode();
+        String imageBase64Data = captcha.getImageBase64Data();
+
+        // 验证码文本缓存至Redis,用于登录校验
+        String captchaKey = IdUtil.fastSimpleUUID();
+        redisTemplate.opsForValue().set(SecurityConstants.CAPTCHA_CODE_PREFIX + captchaKey, captchaCode,
+                captchaProperties.getExpireSeconds(), TimeUnit.SECONDS);
+
+        return CaptchaResult.builder()
+                .captchaKey(captchaKey)
+                .captchaBase64(imageBase64Data)
+                .build();
+    }
+
+}

+ 110 - 0
src/main/java/com/youlai/boot/module/codegen/controller/CodegenController.java

@@ -0,0 +1,110 @@
+package com.youlai.boot.module.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.config.property.CodegenProperties;
+import com.youlai.boot.common.enums.LogModuleEnum;
+import com.youlai.boot.module.codegen.service.CodegenService;
+import com.youlai.boot.module.codegen.model.form.GenConfigForm;
+import com.youlai.boot.module.codegen.model.query.TablePageQuery;
+import com.youlai.boot.module.codegen.model.vo.CodegenPreviewVO;
+import com.youlai.boot.module.codegen.model.vo.TablePageVO;
+import com.youlai.boot.common.annotation.Log;
+import com.youlai.boot.module.codegen.service.GenConfigService;
+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 = "09.代码生成")
+@RestController
+@RequestMapping("/api/v1/codegen")
+@RequiredArgsConstructor
+@Slf4j
+public class CodegenController {
+
+    private final CodegenService codegenService;
+    private final GenConfigService genConfigService;
+    private final CodegenProperties codegenProperties;
+
+    @Operation(summary = "获取数据表分页列表")
+    @GetMapping("/table/page")
+    @Log(value = "代码生成分页列表", module = LogModuleEnum.OTHER)
+    public PageResult<TablePageVO> getTablePage(
+            TablePageQuery queryParams
+    ) {
+
+        Page<TablePageVO> result = codegenService.getTablePage(queryParams);
+        return PageResult.success(result);
+    }
+
+    @Operation(summary = "获取代码生成配置")
+    @GetMapping("/{tableName}/config")
+    public Result<GenConfigForm> getGenConfigFormData(
+            @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName
+    ) {
+        GenConfigForm formData = genConfigService.getGenConfigFormData(tableName);
+        return Result.success(formData);
+    }
+
+    @Operation(summary = "保存代码生成配置")
+    @PostMapping("/{tableName}/config")
+    @Log(value = "生成代码", module = LogModuleEnum.OTHER)
+    public Result<?> saveGenConfig(@RequestBody GenConfigForm formData) {
+        genConfigService.saveGenConfig(formData);
+        return Result.success();
+    }
+
+    @Operation(summary = "删除代码生成配置")
+    @DeleteMapping("/{tableName}/config")
+    public Result<?> deleteGenConfig(
+            @Parameter(description = "表名", example = "sys_user") @PathVariable String tableName
+    ) {
+        genConfigService.deleteGenConfig(tableName);
+        return Result.success();
+    }
+
+    @Operation(summary = "获取预览生成代码")
+    @GetMapping("/{tableName}/preview")
+    @Log(value = "预览生成代码", module = LogModuleEnum.OTHER)
+    public Result<List<CodegenPreviewVO>> getTablePreviewData(@PathVariable String tableName) {
+        List<CodegenPreviewVO> list = codegenService.getCodegenPreviewData(tableName);
+        return Result.success(list);
+    }
+
+    @Operation(summary = "下载代码")
+    @GetMapping("/{tableName}/download")
+    @Log(value = "下载代码", module = LogModuleEnum.OTHER)
+    public void downloadZip(HttpServletResponse response, @PathVariable String tableName) {
+        String[] tableNames = tableName.split(",");
+        byte[] data = codegenService.downloadCode(tableNames);
+
+        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 file to response", e);
+            throw new RuntimeException("Failed to write the zip file to response", e);
+        }
+    }
+}

+ 39 - 0
src/main/java/com/youlai/boot/module/codegen/converter/CodegenConverter.java

@@ -0,0 +1,39 @@
+package com.youlai.boot.module.codegen.converter;
+
+import com.youlai.boot.module.codegen.model.entity.GenConfig;
+import com.youlai.boot.module.codegen.model.entity.GenFieldConfig;
+import com.youlai.boot.module.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 = "genConfig.tableName", target = "tableName")
+    @Mapping(source = "genConfig.businessName", target = "businessName")
+    @Mapping(source = "genConfig.moduleName", target = "moduleName")
+    @Mapping(source = "genConfig.packageName", target = "packageName")
+    @Mapping(source = "genConfig.entityName", target = "entityName")
+    @Mapping(source = "genConfig.author", target = "author")
+    @Mapping(source = "fieldConfigs", target = "fieldConfigs")
+    GenConfigForm toGenConfigForm(GenConfig genConfig, List<GenFieldConfig> fieldConfigs);
+
+    List<GenConfigForm.FieldConfig> toGenFieldConfigForm(List<GenFieldConfig> fieldConfigs);
+
+    GenConfigForm.FieldConfig toGenFieldConfigForm(GenFieldConfig genFieldConfig);
+
+    GenConfig toGenConfig(GenConfigForm formData);
+
+    List<GenFieldConfig> toGenFieldConfig(List<GenConfigForm.FieldConfig> fieldConfigs);
+
+    GenFieldConfig toGenFieldConfig(GenConfigForm.FieldConfig fieldConfig);
+
+}

+ 84 - 0
src/main/java/com/youlai/boot/module/codegen/enums/FormTypeEnum.java

@@ -0,0 +1,84 @@
+package com.youlai.boot.module.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, "日期时间框");
+
+
+    //  Mybatis-Plus 提供注解表示插入数据库时插入该值
+    @EnumValue
+    @JsonValue
+    private final Integer value;
+
+    // @JsonValue //  表示对枚举序列化时返回此字段
+    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);
+    }
+}

+ 84 - 0
src/main/java/com/youlai/boot/module/codegen/enums/JavaTypeEnum.java

@@ -0,0 +1,84 @@
+package com.youlai.boot.module.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", "Date"),
+    DATETIME("datetime", "LocalDateTime", "Date");
+
+    // 数据库类型
+    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) {
+        JavaTypeEnum javaTypeEnum = typeMap.get(columnType);
+        if (javaTypeEnum != null) {
+            return javaTypeEnum.getJavaType();
+        }
+        return null;
+    }
+
+    /**
+     * 根据Java类型获取对应的TypeScript类型
+     *
+     * @param javaType Java类型
+     * @return 对应的TypeScript类型
+     */
+    public static String getTsTypeByJavaType(String javaType) {
+        for (JavaTypeEnum javaTypeEnum : JavaTypeEnum.values()) {
+            if (javaTypeEnum.getJavaType().equals(javaType)) {
+                return javaTypeEnum.getTsType();
+            }
+        }
+        return null;
+    }
+}

+ 73 - 0
src/main/java/com/youlai/boot/module/codegen/enums/QueryTypeEnum.java

@@ -0,0 +1,73 @@
+package com.youlai.boot.module.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);
+    }
+
+}

+ 23 - 0
src/main/java/com/youlai/boot/module/codegen/mapper/DatabaseMapper.java

@@ -0,0 +1,23 @@
+package com.youlai.boot.module.codegen.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.youlai.boot.module.codegen.model.bo.ColumnMetaData;
+import com.youlai.boot.module.codegen.model.bo.TableMetaData;
+import com.youlai.boot.module.codegen.model.query.TablePageQuery;
+import com.youlai.boot.module.codegen.model.vo.TablePageVO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.util.List;
+
+
+@Mapper
+public interface DatabaseMapper extends BaseMapper {
+
+
+    Page<TablePageVO> getTablePage(Page<TablePageVO> page, TablePageQuery queryParams);
+
+    List<ColumnMetaData> getTableColumns(String tableName);
+
+    TableMetaData getTableMetadata(String tableName);
+}

+ 20 - 0
src/main/java/com/youlai/boot/module/codegen/mapper/GenConfigMapper.java

@@ -0,0 +1,20 @@
+package com.youlai.boot.module.codegen.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.youlai.boot.module.codegen.model.entity.GenConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 代码生成基础配置访问层
+ *
+ * @author Ray
+ * @since 2.10.0
+ */
+@Mapper
+public interface GenConfigMapper extends BaseMapper<GenConfig> {
+
+}
+
+
+
+

+ 20 - 0
src/main/java/com/youlai/boot/module/codegen/mapper/GenFieldConfigMapper.java

@@ -0,0 +1,20 @@
+package com.youlai.boot.module.codegen.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.youlai.boot.module.codegen.model.entity.GenFieldConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 代码生成字段配置访问层
+ *
+ * @author Ray
+ * @since 2.10.0
+ */
+@Mapper
+public interface GenFieldConfigMapper extends BaseMapper<GenFieldConfig> {
+
+}
+
+
+
+

+ 50 - 0
src/main/java/com/youlai/boot/module/codegen/model/bo/ColumnMetaData.java

@@ -0,0 +1,50 @@
+package com.youlai.boot.module.codegen.model.bo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Schema(description = "数据表字段VO")
+@Data
+public class ColumnMetaData {
+
+        /**
+         * 字段名称
+         */
+        private String columnName;
+
+        /**
+         * 字段类型
+         */
+        private String dataType;
+
+        /**
+         * 字段描述
+         */
+        private String columnComment;
+
+        /**
+         * 字段长度
+         */
+        private Integer characterMaximumLength;
+
+        /**
+         * 是否主键(1-是 0-否)
+         */
+        private Integer isPrimaryKey;
+
+        /**
+         * 是否可为空(1-是 0-否)
+         */
+        private String isNullable;
+
+        /**
+         * 字符集
+         */
+        private String characterSetName;
+
+        /**
+         * 排序规则
+         */
+        private String collationName;
+
+}

+ 45 - 0
src/main/java/com/youlai/boot/module/codegen/model/bo/TableMetaData.java

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

+ 54 - 0
src/main/java/com/youlai/boot/module/codegen/model/entity/GenConfig.java

@@ -0,0 +1,54 @@
+package com.youlai.boot.module.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_config")
+@Getter
+@Setter
+public class GenConfig extends BaseEntity {
+
+    /**
+     * 表名
+     */
+    private String tableName;
+
+    /**
+     * 包名
+     */
+    private String packageName;
+
+    /**
+     * 模块名
+     */
+    private String moduleName;
+
+    /**
+     * 实体类名
+     */
+    private String entityName;
+
+    /**
+     * 业务名
+     */
+    private String businessName;
+
+    /**
+     * 父菜单ID
+     */
+    private Long parentMenuId;
+
+    /**
+     * 作者
+     */
+    private String author;
+}

+ 106 - 0
src/main/java/com/youlai/boot/module/codegen/model/entity/GenFieldConfig.java

@@ -0,0 +1,106 @@
+package com.youlai.boot.module.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.module.codegen.enums.FormTypeEnum;
+import com.youlai.boot.module.codegen.enums.QueryTypeEnum;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * 字段生成配置实体
+ *
+ * @author Ray
+ * @since 2.10.0
+ */
+@TableName(value = "gen_field_config")
+@Getter
+@Setter
+public class GenFieldConfig extends BaseEntity {
+
+
+    /**
+     * 关联的配置ID
+     */
+    private Long configId;
+
+    /**
+     * 列名
+     */
+    private String columnName;
+
+    /**
+     * 列类型
+     */
+    private String columnType;
+
+    /**
+     * 字段长度
+     */
+    private Integer 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;
+}

+ 103 - 0
src/main/java/com/youlai/boot/module/codegen/model/form/GenConfigForm.java

@@ -0,0 +1,103 @@
+package com.youlai.boot.module.codegen.model.form;
+
+import com.youlai.boot.module.codegen.enums.FormTypeEnum;
+import com.youlai.boot.module.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 = "字段配置")
+    @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 - 0
src/main/java/com/youlai/boot/module/codegen/model/query/TablePageQuery.java

@@ -0,0 +1,31 @@
+package com.youlai.boot.module.codegen.model.query;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.youlai.boot.common.base.BasePageQuery;
+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 BasePageQuery {
+
+    @Schema(description="关键字(表名)")
+    private String keywords;
+
+    /**
+     * 排除的表名
+     */
+    @JsonIgnore
+    private List<String> excludeTables;
+
+}

+ 19 - 0
src/main/java/com/youlai/boot/module/codegen/model/vo/CodegenPreviewVO.java

@@ -0,0 +1,19 @@
+package com.youlai.boot.module.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;
+
+}

+ 32 - 0
src/main/java/com/youlai/boot/module/codegen/model/vo/TablePageVO.java

@@ -0,0 +1,32 @@
+package com.youlai.boot.module.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;
+
+}

+ 41 - 0
src/main/java/com/youlai/boot/module/codegen/service/CodegenService.java

@@ -0,0 +1,41 @@
+package com.youlai.boot.module.codegen.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.youlai.boot.module.codegen.model.form.GenConfigForm;
+import com.youlai.boot.module.codegen.model.query.TablePageQuery;
+import com.youlai.boot.module.codegen.model.vo.CodegenPreviewVO;
+import com.youlai.boot.module.codegen.model.vo.TablePageVO;
+
+import java.util.List;
+
+/**
+ * 代码生成配置接口
+ *
+ * @author Ray
+ * @since 2.10.0
+ */
+public interface CodegenService {
+
+    /**
+     * 获取数据表分页列表
+     *
+     * @param queryParams 查询参数
+     * @return
+     */
+    Page<TablePageVO> getTablePage(TablePageQuery queryParams);
+
+    /**
+     * 获取预览生成代码
+     *
+     * @param tableName 表名
+     * @return
+     */
+    List<CodegenPreviewVO> getCodegenPreviewData(String tableName);
+
+    /**
+     * 下载代码
+     * @param tableNames 表名
+     * @return
+     */
+    byte[] downloadCode(String[] tableNames);
+}

+ 39 - 0
src/main/java/com/youlai/boot/module/codegen/service/GenConfigService.java

@@ -0,0 +1,39 @@
+package com.youlai.boot.module.codegen.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.youlai.boot.module.codegen.model.entity.GenConfig;
+import com.youlai.boot.module.codegen.model.form.GenConfigForm;
+
+/**
+ * 代码生成配置接口
+ *
+ * @author Ray
+ * @since 2.10.0
+ */
+public interface GenConfigService extends IService<GenConfig> {
+
+    /**
+     * 获取代码生成配置
+     *
+     * @param tableName 表名
+     * @return
+     */
+    GenConfigForm getGenConfigFormData(String tableName);
+
+    /**
+     * 保存代码生成配置
+     *
+     * @param formData 表单数据
+     * @return
+     */
+    void saveGenConfig(GenConfigForm formData);
+
+    /**
+     * 删除代码生成配置
+     *
+     * @param tableName 表名
+     * @return
+     */
+    void deleteGenConfig(String tableName);
+
+}

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio