Explorar el Código

feat 发布1.2.0

Uncarbon hace 4 años
commit
04fa19fe5d
Se han modificado 100 ficheros con 7494 adiciones y 0 borrados
  1. 32 0
      .gitignore
  2. 127 0
      LICENSE
  3. 12 0
      README.md
  4. 452 0
      attachments/schema/MySQL/helio_boot.sql
  5. 92 0
      pom.xml
  6. 18 0
      src/main/java/cc/uncarbon/HelioBootApplication.java
  7. 33 0
      src/main/java/cc/uncarbon/config/AdminSaTokenExtendConfiguration.java
  8. 62 0
      src/main/java/cc/uncarbon/config/AsyncConfiguration.java
  9. 56 0
      src/main/java/cc/uncarbon/config/CustomInterceptorConfiguration.java
  10. 47 0
      src/main/java/cc/uncarbon/interceptor/AdminSaTokenParseInterceptor.java
  11. 38 0
      src/main/java/cc/uncarbon/interceptor/AppSaTokenRouteInterceptor.java
  12. 11 0
      src/main/java/cc/uncarbon/module/app/constant/AppConstant.java
  13. 66 0
      src/main/java/cc/uncarbon/module/app/controller/AppAuthController.java
  14. 22 0
      src/main/java/cc/uncarbon/module/sys/annotation/SysLog.java
  15. 82 0
      src/main/java/cc/uncarbon/module/sys/aspect/SysLogAspect.java
  16. 51 0
      src/main/java/cc/uncarbon/module/sys/constant/SysConstant.java
  17. 92 0
      src/main/java/cc/uncarbon/module/sys/controller/AdminAuthController.java
  18. 84 0
      src/main/java/cc/uncarbon/module/sys/controller/AdminSysDataDictController.java
  19. 83 0
      src/main/java/cc/uncarbon/module/sys/controller/AdminSysDeptController.java
  20. 56 0
      src/main/java/cc/uncarbon/module/sys/controller/AdminSysLogController.java
  21. 134 0
      src/main/java/cc/uncarbon/module/sys/controller/AdminSysMenuController.java
  22. 83 0
      src/main/java/cc/uncarbon/module/sys/controller/AdminSysParamController.java
  23. 110 0
      src/main/java/cc/uncarbon/module/sys/controller/AdminSysRoleController.java
  24. 85 0
      src/main/java/cc/uncarbon/module/sys/controller/AdminSysTenantController.java
  25. 139 0
      src/main/java/cc/uncarbon/module/sys/controller/AdminSysUserController.java
  26. 61 0
      src/main/java/cc/uncarbon/module/sys/entity/SysDataDictEntity.java
  27. 45 0
      src/main/java/cc/uncarbon/module/sys/entity/SysDeptEntity.java
  28. 57 0
      src/main/java/cc/uncarbon/module/sys/entity/SysLogEntity.java
  29. 66 0
      src/main/java/cc/uncarbon/module/sys/entity/SysMenuEntity.java
  30. 40 0
      src/main/java/cc/uncarbon/module/sys/entity/SysParamEntity.java
  31. 36 0
      src/main/java/cc/uncarbon/module/sys/entity/SysRoleEntity.java
  32. 36 0
      src/main/java/cc/uncarbon/module/sys/entity/SysRoleMenuRelationEntity.java
  33. 41 0
      src/main/java/cc/uncarbon/module/sys/entity/SysTenantEntity.java
  34. 36 0
      src/main/java/cc/uncarbon/module/sys/entity/SysUserDeptRelationEntity.java
  35. 70 0
      src/main/java/cc/uncarbon/module/sys/entity/SysUserEntity.java
  36. 36 0
      src/main/java/cc/uncarbon/module/sys/entity/SysUserRoleRelationEntity.java
  37. 37 0
      src/main/java/cc/uncarbon/module/sys/enums/GenderEnum.java
  38. 32 0
      src/main/java/cc/uncarbon/module/sys/enums/GenericStatusEnum.java
  39. 38 0
      src/main/java/cc/uncarbon/module/sys/enums/SysErrorEnum.java
  40. 32 0
      src/main/java/cc/uncarbon/module/sys/enums/SysLogStatusEnum.java
  41. 42 0
      src/main/java/cc/uncarbon/module/sys/enums/SysMenuTypeEnum.java
  42. 32 0
      src/main/java/cc/uncarbon/module/sys/enums/SysUserStatusEnum.java
  43. 32 0
      src/main/java/cc/uncarbon/module/sys/enums/UserTypeEnum.java
  44. 16 0
      src/main/java/cc/uncarbon/module/sys/mapper/SysDataDictMapper.java
  45. 14 0
      src/main/java/cc/uncarbon/module/sys/mapper/SysDeptMapper.java
  46. 14 0
      src/main/java/cc/uncarbon/module/sys/mapper/SysLogMapper.java
  47. 14 0
      src/main/java/cc/uncarbon/module/sys/mapper/SysMenuMapper.java
  48. 14 0
      src/main/java/cc/uncarbon/module/sys/mapper/SysParamMapper.java
  49. 14 0
      src/main/java/cc/uncarbon/module/sys/mapper/SysRoleMapper.java
  50. 14 0
      src/main/java/cc/uncarbon/module/sys/mapper/SysRoleMenuRelationMapper.java
  51. 14 0
      src/main/java/cc/uncarbon/module/sys/mapper/SysTenantMapper.java
  52. 14 0
      src/main/java/cc/uncarbon/module/sys/mapper/SysUserDeptRelationMapper.java
  53. 25 0
      src/main/java/cc/uncarbon/module/sys/mapper/SysUserMapper.java
  54. 14 0
      src/main/java/cc/uncarbon/module/sys/mapper/SysUserRoleRelationMapper.java
  55. 33 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminBindUserRoleRelationDTO.java
  56. 58 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysDataDictDTO.java
  57. 45 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysDeptDTO.java
  58. 63 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysMenuDTO.java
  59. 43 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysParamDTO.java
  60. 43 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysRoleDTO.java
  61. 68 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysUserDTO.java
  62. 44 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertSysTenantDTO.java
  63. 29 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminKickOutSysUserDTO.java
  64. 28 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysDataDictDTO.java
  65. 30 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysDeptDTO.java
  66. 48 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysLogDTO.java
  67. 35 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysMenuDTO.java
  68. 30 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysParamDTO.java
  69. 44 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysRoleDTO.java
  70. 48 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysTenantDTO.java
  71. 41 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysUserDTO.java
  72. 36 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminResetSysUserPasswordDTO.java
  73. 40 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminUpdateCurrentSysUserPasswordDTO.java
  74. 45 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminUpdateSysTenantDTO.java
  75. 41 0
      src/main/java/cc/uncarbon/module/sys/model/request/SysUserLoginDTO.java
  76. 68 0
      src/main/java/cc/uncarbon/module/sys/model/response/SysDataDictBO.java
  77. 60 0
      src/main/java/cc/uncarbon/module/sys/model/response/SysDeptBO.java
  78. 48 0
      src/main/java/cc/uncarbon/module/sys/model/response/SysLogBO.java
  79. 88 0
      src/main/java/cc/uncarbon/module/sys/model/response/SysMenuBO.java
  80. 52 0
      src/main/java/cc/uncarbon/module/sys/model/response/SysParamBO.java
  81. 53 0
      src/main/java/cc/uncarbon/module/sys/model/response/SysRoleBO.java
  82. 59 0
      src/main/java/cc/uncarbon/module/sys/model/response/SysTenantBO.java
  83. 82 0
      src/main/java/cc/uncarbon/module/sys/model/response/SysUserBO.java
  84. 47 0
      src/main/java/cc/uncarbon/module/sys/model/response/SysUserBaseInfoBO.java
  85. 48 0
      src/main/java/cc/uncarbon/module/sys/model/response/SysUserLoginBO.java
  86. 35 0
      src/main/java/cc/uncarbon/module/sys/model/response/VbenAdminMenuMetaBO.java
  87. 32 0
      src/main/java/cc/uncarbon/module/sys/model/response/VbenAdminUserInfoBO.java
  88. 162 0
      src/main/java/cc/uncarbon/module/sys/service/SysDataDictService.java
  89. 199 0
      src/main/java/cc/uncarbon/module/sys/service/SysDeptService.java
  90. 103 0
      src/main/java/cc/uncarbon/module/sys/service/SysLogService.java
  91. 495 0
      src/main/java/cc/uncarbon/module/sys/service/SysMenuService.java
  92. 190 0
      src/main/java/cc/uncarbon/module/sys/service/SysParamService.java
  93. 75 0
      src/main/java/cc/uncarbon/module/sys/service/SysRoleMenuRelationService.java
  94. 194 0
      src/main/java/cc/uncarbon/module/sys/service/SysRoleService.java
  95. 251 0
      src/main/java/cc/uncarbon/module/sys/service/SysTenantService.java
  96. 42 0
      src/main/java/cc/uncarbon/module/sys/service/SysUserDeptRelationService.java
  97. 81 0
      src/main/java/cc/uncarbon/module/sys/service/SysUserRoleRelationService.java
  98. 337 0
      src/main/java/cc/uncarbon/module/sys/service/SysUserService.java
  99. 769 0
      src/main/java/cc/uncarbon/module/sys/util/AdminStpUtil.java
  100. 29 0
      src/main/java/cc/uncarbon/module/sys/util/PwdUtil.java

+ 32 - 0
.gitignore

@@ -0,0 +1,32 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# MacOS
+.DS_Store
+
+# IntelliJ IDEA
+.idea
+*.iws
+*.iml
+*.ipr

+ 127 - 0
LICENSE

@@ -0,0 +1,127 @@
+                     木兰宽松许可证, 第2版
+
+   木兰宽松许可证, 第2版 
+   2020年1月 http://license.coscl.org.cn/MulanPSL2
+
+
+   您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束:
+
+   0. 定义
+
+      “软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。
+
+      “贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。
+
+      “贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。
+
+      “法人实体”是指提交贡献的机构及其“关联实体”。
+
+      “关联实体”是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。
+
+   1. 授予版权许可
+
+      每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。
+
+   2. 授予专利许可
+
+      每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。
+
+   3. 无商标许可
+
+      “本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。
+
+   4. 分发限制
+
+      您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。
+
+   5. 免责声明与责任限制
+
+      “软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。 
+
+   6. 语言
+      “本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。
+
+   条款结束 
+
+   如何将木兰宽松许可证,第2版,应用到您的软件
+   
+   如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步:
+
+      1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字;
+
+      2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中;
+
+      3, 请将如下声明文本放入每个源文件的头部注释中。
+
+   Copyright (c) [Year] [name of copyright holder]
+   [Software Name] is licensed under Mulan PSL v2.
+   You can use this software according to the terms and conditions of the Mulan PSL v2. 
+   You may obtain a copy of Mulan PSL v2 at:
+            http://license.coscl.org.cn/MulanPSL2 
+   THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.  
+   See the Mulan PSL v2 for more details.  
+
+
+                     Mulan Permissive Software License,Version 2
+
+   Mulan Permissive Software License,Version 2 (Mulan PSL v2)
+   January 2020 http://license.coscl.org.cn/MulanPSL2
+
+   Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions: 
+   
+   0. Definition
+   
+      Software means the program and related documents which are licensed under this License and comprise all Contribution(s). 
+   
+      Contribution means the copyrightable work licensed by a particular Contributor under this License.
+   
+      Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License.
+   
+      Legal Entity means the entity making a Contribution and all its Affiliates.
+   
+      Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity.
+
+   1. Grant of Copyright License
+
+      Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not.
+
+   2. Grant of Patent License 
+
+      Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken.
+
+   3. No Trademark License
+
+      No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4.
+
+   4. Distribution Restriction
+
+      You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software.
+
+   5. Disclaimer of Warranty and Limitation of Liability
+
+      THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+   6. Language
+
+      THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL.
+
+   END OF THE TERMS AND CONDITIONS
+
+   How to Apply the Mulan Permissive Software License,Version 2 (Mulan PSL v2) to Your Software
+
+      To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps:
+
+      i Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner; 
+
+      ii Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package;
+
+      iii Attach the statement to the appropriate annotated syntax at the beginning of each source file.
+
+
+   Copyright (c) [Year] [name of copyright holder]
+   [Software Name] is licensed under Mulan PSL v2.
+   You can use this software according to the terms and conditions of the Mulan PSL v2. 
+   You may obtain a copy of Mulan PSL v2 at:
+               http://license.coscl.org.cn/MulanPSL2 
+   THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.  
+   See the Mulan PSL v2 for more details.  

+ 12 - 0
README.md

@@ -0,0 +1,12 @@
+# helio-boot
+
+## 项目介绍
+`helio-boot`基于[JDK15](http://jdk.java.net/archive/) + Spring Boot 2.4.x,是一款预置SaaS、RBAC能力的单体项目脚手架,适合初学者学习、老手接外包干私活、初创企业快速上马项目
+
+需要先安装[helio-starters](https://github.com/uncarbon97/helio-starters) ,可参考[官方文档](https://helio.uncarbon.cc/)
+
+## 配套后台管理前端模板 && 代码生成器
+| 项目名 | 简介 | Gitee | GitHub
+| ---- | ---- | ---- | ---- |
+| helio-generator | 可一键生成单体or微服务版的前、后端代码,减少无谓的重复劳动 | [Gitee](https://gitee.com/uncarbon97/helio-generator) | [GitHub](https://github.com/uncarbon97/helio-generator)
+| helio-admin-vue-vben | 基于[Vue vben admin](https://github.com/anncwb/vue-vben-admin) 改造适配的前端框架,开箱即用 | [Gitee](https://gitee.com/uncarbon97/helio-admin-vue-vben) | [GitHub](https://github.com/uncarbon97/helio-admin-vue-vben)

+ 452 - 0
attachments/schema/MySQL/helio_boot.sql

@@ -0,0 +1,452 @@
+SET NAMES utf8mb4;
+SET
+FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------
+-- Table structure for sys_data_dict
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_data_dict`;
+CREATE TABLE `sys_data_dict`
+(
+    `id`              bigint(20) NOT NULL COMMENT '主键ID',
+    `tenant_id`       bigint(20) NULL DEFAULT NULL COMMENT '租户ID',
+    `revision`        bigint(20) NOT NULL DEFAULT 1 COMMENT '乐观锁',
+    `del_flag`        smallint(6) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识',
+    `created_at`      datetime(0) NOT NULL COMMENT '创建时刻',
+    `created_by`      varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者',
+    `updated_at`      datetime(0) NOT NULL COMMENT '更新时刻',
+    `updated_by`      varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新者',
+    `remark`          varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+    `camel_case_key`  varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '驼峰式键名',
+    `under_case_key`  varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '下划线式键名',
+    `pascal_case_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '帕斯卡式键名',
+    `value`           varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '键值',
+    `description`     varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '参数描述',
+    `unit`            varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '单位',
+    `value_range`     varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '取值范围',
+    `alias_key`       varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '别称键名',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '数据字典';
+
+-- ----------------------------
+-- Records of sys_data_dict
+-- ----------------------------
+BEGIN;
+COMMIT;
+
+-- ----------------------------
+-- Table structure for sys_dept
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_dept`;
+CREATE TABLE `sys_dept`
+(
+    `id`         bigint(20) NOT NULL COMMENT '主键ID',
+    `tenant_id`  bigint(20) NULL DEFAULT NULL COMMENT '租户ID',
+    `revision`   bigint(20) NOT NULL DEFAULT 1 COMMENT '乐观锁',
+    `del_flag`   smallint(6) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识',
+    `created_at` datetime(0) NOT NULL COMMENT '创建时刻',
+    `created_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者',
+    `updated_at` datetime(0) NOT NULL COMMENT '更新时刻',
+    `updated_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新者',
+    `remark`     varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+    `title`      varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '名称',
+    `parent_id`  bigint(20) NOT NULL DEFAULT 0 COMMENT '上级ID(根节点设置为0)',
+    `sort`       int(11) NOT NULL DEFAULT 1 COMMENT '排序',
+    `status`     int(11) NOT NULL DEFAULT 0 COMMENT '状态(0=禁用 1=启用)',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '部门';
+
+-- ----------------------------
+-- Records of sys_dept
+-- ----------------------------
+BEGIN;
+COMMIT;
+
+-- ----------------------------
+-- Table structure for sys_log
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_log`;
+CREATE TABLE `sys_log`
+(
+    `id`         bigint(20) NOT NULL COMMENT '主键ID',
+    `tenant_id`  bigint(20) NULL DEFAULT NULL COMMENT '租户ID',
+    `revision`   bigint(20) NOT NULL DEFAULT 1 COMMENT '乐观锁',
+    `del_flag`   smallint(6) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识',
+    `created_at` datetime(0) NOT NULL COMMENT '创建时刻',
+    `created_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者',
+    `updated_at` datetime(0) NOT NULL COMMENT '更新时刻',
+    `updated_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新者',
+    `remark`     varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+    `user_id`    bigint(20) NULL DEFAULT NULL COMMENT '用户ID',
+    `username`   varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户账号',
+    `operation`  varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '操作内容',
+    `method`     varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求方法',
+    `params`     text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '请求参数',
+    `ip`         varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'IP地址',
+    `status`     int(11) NOT NULL DEFAULT 0 COMMENT '状态(参考SysLogStatusEnum)',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '后台操作日志';
+
+-- ----------------------------
+-- Records of sys_log
+-- ----------------------------
+BEGIN;
+COMMIT;
+
+-- ----------------------------
+-- Table structure for sys_menu
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_menu`;
+CREATE TABLE `sys_menu`
+(
+    `id`            bigint(20) NOT NULL COMMENT '主键ID',
+    `tenant_id`     bigint(20) NULL DEFAULT NULL COMMENT '租户ID',
+    `revision`      bigint(20) NOT NULL DEFAULT 1 COMMENT '乐观锁',
+    `del_flag`      smallint(6) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识',
+    `created_at`    datetime(0) NOT NULL COMMENT '创建时刻',
+    `created_by`    varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者',
+    `updated_at`    datetime(0) NOT NULL COMMENT '更新时刻',
+    `updated_by`    varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新者',
+    `remark`        varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+    `title`         varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci  NOT NULL COMMENT '名称',
+    `parent_id`     bigint(20) NOT NULL DEFAULT 0 COMMENT '上级菜单ID(根节点设置为0)',
+    `type`          int(11) NOT NULL DEFAULT 1 COMMENT '菜单类型(参考MenuTypeEnum)',
+    `permission`    varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '权限标识',
+    `icon`          varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图标',
+    `sort`          int(11) NULL DEFAULT 1 COMMENT '排序',
+    `status`        int(11) NOT NULL DEFAULT 0 COMMENT '状态(0=禁用 1=启用)',
+    `component`     varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组件',
+    `external_link` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '外链地址',
+    PRIMARY KEY (`id`) USING BTREE,
+    INDEX           `idx_sys_menu_parent_id`(`parent_id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '后台菜单';
+
+-- ----------------------------
+-- Records of sys_menu
+-- ----------------------------
+BEGIN;
+INSERT INTO `sys_menu`
+VALUES (1, 0, 1, 0, '2021-06-01 17:16:48', NULL, '2021-06-02 17:14:16', NULL, NULL, '系统管理', 0, 0, 'Sys',
+        'ant-design:setting-outlined', 3, 1, 'LAYOUT', ''),
+       (2, 0, 1, 0, '2021-06-01 17:13:13', NULL, '2021-07-18 00:10:33', 'admin', NULL, '中控台', 0, 0, 'Dashboard',
+        'ant-design:appstore-outlined', 1, 1, 'LAYOUT', ''),
+       (3, 0, 1, 0, '2021-06-01 17:14:22', NULL, '2021-06-02 11:24:38', NULL, NULL, '分析页(默认页必须分配, 否则进不去后台)', 2, 1,
+        'Dashboard:analysis', 'ant-design:fund-outlined', 1, 1, '/dashboard/analysis/index', ''),
+       (4, 0, 1, 0, '2021-06-01 17:14:47', NULL, '2021-06-02 11:32:23', NULL, NULL, '工作台', 2, 1, 'Dashboard:workbench',
+        'ant-design:database-outlined', 2, 1, '/dashboard/workbench/index', ''),
+       (5, 0, 1, 0, '2021-06-01 17:15:38', NULL, '2021-07-15 23:56:43', 'admin', NULL, '关于', 0, 1, 'About',
+        'ant-design:eye-outlined', 2, 1, '/sys/vben/about/index', ''),
+       (6, 0, 1, 0, '2021-06-02 16:06:58', NULL, '2021-07-17 23:55:52', 'admin', NULL, '学(mo)习(yu)', 2, 3, '',
+        'ant-design:zhihu-outlined', 3, 1, 'https://www.zhihu.com/', 'https://www.zhihu.com/'),
+       (7, 0, 1, 0, '2021-07-17 23:32:15', NULL, '2021-07-17 23:32:15', NULL, NULL, '部门管理', 1, 1, 'SysDept',
+        'ant-design:flag-outlined', 1, 1, '/sys/SysDept/index', ''),
+       (8, 0, 1, 0, '2021-07-17 23:32:15', NULL, '2021-07-17 23:32:15', NULL, NULL, '查询', 7, 2, 'SysDept:retrieve',
+        NULL, 1, 1, NULL, ''),
+       (9, 0, 1, 0, '2021-07-17 23:32:15', NULL, '2021-07-17 23:32:15', NULL, NULL, '新增', 7, 2, 'SysDept:create', NULL,
+        2, 1, NULL, ''),
+       (10, 0, 1, 0, '2021-07-17 23:32:15', NULL, '2021-07-17 23:32:15', NULL, NULL, '删除', 7, 2, 'SysDept:delete', NULL,
+        3, 1, NULL, ''),
+       (11, 0, 1, 0, '2021-07-17 23:32:16', NULL, '2021-07-17 23:32:16', NULL, NULL, '编辑', 7, 2, 'SysDept:update', NULL,
+        4, 1, NULL, ''),
+       (12, 0, 1, 0, '2021-07-17 23:32:16', NULL, '2021-07-17 23:32:16', NULL, NULL, '角色管理', 1, 1, 'SysRole',
+        'ant-design:usergroup-add-outlined', 2, 1, '/sys/SysRole/index', ''),
+       (13, 0, 1, 0, '2021-07-17 23:32:16', NULL, '2021-07-17 23:32:16', NULL, NULL, '查询', 12, 2, 'SysRole:retrieve',
+        NULL, 1, 1, NULL, ''),
+       (14, 0, 1, 0, '2021-07-17 23:32:16', NULL, '2021-07-17 23:32:16', NULL, NULL, '新增', 12, 2, 'SysRole:create',
+        NULL, 2, 1, NULL, ''),
+       (15, 0, 1, 0, '2021-07-17 23:32:16', NULL, '2021-07-17 23:32:16', NULL, NULL, '删除', 12, 2, 'SysRole:delete',
+        NULL, 3, 1, NULL, ''),
+       (16, 0, 1, 0, '2021-07-17 23:32:16', NULL, '2021-07-17 23:32:16', NULL, NULL, '编辑', 12, 2, 'SysRole:update',
+        NULL, 4, 1, NULL, ''),
+       (17, 0, 1, 0, '2021-07-17 23:32:17', NULL, '2021-07-17 23:32:17', NULL, NULL, '后台用户', 1, 1, 'SysUser',
+        'ant-design:user-outlined', 3, 1, '/sys/SysUser/index', ''),
+       (18, 0, 1, 0, '2021-07-17 23:32:17', NULL, '2021-07-17 23:32:17', NULL, NULL, '查询', 17, 2, 'SysUser:retrieve',
+        NULL, 1, 1, NULL, ''),
+       (19, 0, 1, 0, '2021-07-17 23:32:17', NULL, '2021-07-17 23:32:17', NULL, NULL, '新增', 17, 2, 'SysUser:create',
+        NULL, 2, 1, NULL, ''),
+       (20, 0, 1, 0, '2021-07-17 23:32:17', NULL, '2021-07-17 23:32:17', NULL, NULL, '删除', 17, 2, 'SysUser:delete',
+        NULL, 3, 1, NULL, ''),
+       (21, 0, 1, 0, '2021-07-17 23:32:17', NULL, '2021-07-17 23:32:17', NULL, NULL, '编辑', 17, 2, 'SysUser:update',
+        NULL, 4, 1, NULL, ''),
+       (22, 0, 1, 0, '2021-07-17 23:32:18', NULL, '2021-07-17 23:32:18', NULL, NULL, '菜单管理', 1, 1, 'SysMenu',
+        'ant-design:align-left-outlined', 4, 1, '/sys/SysMenu/index', ''),
+       (23, 0, 1, 0, '2021-07-17 23:32:18', NULL, '2021-07-17 23:32:18', NULL, NULL, '查询', 22, 2, 'SysMenu:retrieve',
+        NULL, 1, 1, NULL, ''),
+       (24, 0, 1, 0, '2021-07-17 23:32:18', NULL, '2021-07-17 23:32:18', NULL, NULL, '新增', 22, 2, 'SysMenu:create',
+        NULL, 2, 1, NULL, ''),
+       (25, 0, 1, 0, '2021-07-17 23:32:18', NULL, '2021-07-17 23:32:18', NULL, NULL, '删除', 22, 2, 'SysMenu:delete',
+        NULL, 3, 1, NULL, ''),
+       (26, 0, 1, 0, '2021-07-17 23:32:18', NULL, '2021-07-17 23:32:18', NULL, NULL, '编辑', 22, 2, 'SysMenu:update',
+        NULL, 4, 1, NULL, ''),
+       (27, 0, 1, 0, '2021-07-17 23:32:18', NULL, '2021-07-17 23:32:18', NULL, NULL, '参数管理', 1, 1, 'SysParam',
+        'ant-design:ant-design-outlined', 5, 1, '/sys/SysParam/index', ''),
+       (28, 0, 1, 0, '2021-07-17 23:32:19', NULL, '2021-07-17 23:32:19', NULL, NULL, '查询', 27, 2, 'SysParam:retrieve',
+        NULL, 1, 1, NULL, ''),
+       (29, 0, 1, 0, '2021-07-17 23:32:19', NULL, '2021-07-17 23:32:19', NULL, NULL, '新增', 27, 2, 'SysParam:create',
+        NULL, 2, 1, NULL, ''),
+       (30, 0, 1, 0, '2021-07-17 23:32:19', NULL, '2021-07-17 23:32:19', NULL, NULL, '删除', 27, 2, 'SysParam:delete',
+        NULL, 3, 1, NULL, ''),
+       (31, 0, 1, 0, '2021-07-17 23:32:19', NULL, '2021-07-17 23:32:19', NULL, NULL, '编辑', 27, 2, 'SysParam:update',
+        NULL, 4, 1, NULL, ''),
+       (32, 0, 1, 0, '2021-07-17 23:32:19', NULL, '2021-07-17 23:32:19', NULL, NULL, '租户管理', 1, 1, 'SysTenant',
+        'ant-design:aliyun-outlined', 6, 1, '/sys/SysTenant/index', ''),
+       (33, 0, 1, 0, '2021-07-17 23:32:19', NULL, '2021-07-17 23:32:19', NULL, NULL, '查询', 32, 2, 'SysTenant:retrieve',
+        NULL, 1, 1, NULL, ''),
+       (34, 0, 1, 0, '2021-07-17 23:32:20', NULL, '2021-07-17 23:32:20', NULL, NULL, '新增', 32, 2, 'SysTenant:create',
+        NULL, 2, 1, NULL, ''),
+       (35, 0, 1, 0, '2021-07-17 23:32:20', NULL, '2021-07-17 23:32:20', NULL, NULL, '删除', 32, 2, 'SysTenant:delete',
+        NULL, 3, 1, NULL, ''),
+       (36, 0, 1, 0, '2021-07-17 23:32:20', NULL, '2021-07-17 23:32:20', NULL, NULL, '编辑', 32, 2, 'SysTenant:update',
+        NULL, 4, 1, NULL, ''),
+       (37, 0, 1, 0, '2021-07-17 23:32:20', NULL, '2021-07-17 23:32:20', NULL, NULL, '数据字典', 1, 1, 'SysDataDict',
+        'ant-design:field-string-outlined', 7, 1, '/sys/SysDataDict/index', ''),
+       (38, 0, 1, 0, '2021-07-17 23:32:20', NULL, '2021-07-17 23:32:20', NULL, NULL, '查询', 37, 2,
+        'SysDataDict:retrieve', NULL, 1, 1, NULL, ''),
+       (39, 0, 1, 0, '2021-07-17 23:32:21', NULL, '2021-07-17 23:32:21', NULL, NULL, '新增', 37, 2, 'SysDataDict:create',
+        NULL, 2, 1, NULL, ''),
+       (40, 0, 1, 0, '2021-07-17 23:32:21', NULL, '2021-07-17 23:32:21', NULL, NULL, '删除', 37, 2, 'SysDataDict:delete',
+        NULL, 3, 1, NULL, ''),
+       (41, 0, 1, 0, '2021-07-17 23:32:21', NULL, '2021-07-17 23:32:21', NULL, NULL, '编辑', 37, 2, 'SysDataDict:update',
+        NULL, 4, 1, NULL, ''),
+       (42, 0, 1, 0, '2021-07-17 23:32:21', NULL, '2021-07-17 23:32:21', NULL, NULL, '系统日志', 1, 1, 'SysLog',
+        'ant-design:edit-twotone', 8, 1, '/sys/SysLog/index', ''),
+       (43, 0, 1, 0, '2021-07-17 23:32:21', NULL, '2021-07-17 23:32:21', NULL, NULL, '查询', 42, 2, 'SysLog:retrieve',
+        NULL, 1, 1, NULL, ''),
+       (44, 0, 1, 0, '2021-07-17 23:37:26', 'admin', '2021-07-17 23:38:28', 'admin', NULL, '修改当前用户密码', 1, 1, '',
+        'ant-design:compass-outlined', 9, 1, '/sys/SysUser/change-password/index', ''),
+       (45, 0, 1, 0, '2021-07-17 23:39:41', 'admin', '2021-07-17 23:39:41', 'admin', NULL, '重置某用户密码', 17, 2,
+        'SysUser:resetPassword', 'ant-design:redo-outlined', 5, 1, NULL, ''),
+       (46, 0, 1, 0, '2021-07-17 23:40:26', 'admin', '2021-07-17 23:40:26', 'admin', NULL, '绑定用户与角色关联关系', 17, 2,
+        'SysUser:bindRoles', 'ant-design:share-alt-outlined', 6, 1, NULL, ''),
+       (47, 0, 1, 0, '2021-07-17 23:40:47', 'admin', '2021-07-17 23:41:15', 'admin', NULL, '踢某用户下线', 17, 2,
+        'SysUser:kickOut', 'ant-design:disconnect-outlined', 7, 1, 'LAYOUT', '');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for sys_param
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_param`;
+CREATE TABLE `sys_param`
+(
+    `id`          bigint(20) NOT NULL COMMENT '主键ID',
+    `tenant_id`   bigint(20) NULL DEFAULT NULL COMMENT '租户ID',
+    `revision`    bigint(20) NOT NULL DEFAULT 1 COMMENT '乐观锁',
+    `del_flag`    smallint(6) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识',
+    `created_at`  datetime(0) NOT NULL COMMENT '创建时刻',
+    `created_by`  varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者',
+    `updated_at`  datetime(0) NOT NULL COMMENT '更新时刻',
+    `updated_by`  varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新者',
+    `remark`      varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+    `key`         varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci  NOT NULL COMMENT '键名',
+    `value`       varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '键值',
+    `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '参数描述',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统参数';
+
+-- ----------------------------
+-- Records of sys_param
+-- ----------------------------
+BEGIN;
+COMMIT;
+
+-- ----------------------------
+-- Table structure for sys_role
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_role`;
+CREATE TABLE `sys_role`
+(
+    `id`         bigint(20) NOT NULL COMMENT '主键ID',
+    `tenant_id`  bigint(20) NULL DEFAULT NULL COMMENT '租户ID',
+    `revision`   bigint(20) NOT NULL DEFAULT 1 COMMENT '乐观锁',
+    `del_flag`   smallint(6) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识',
+    `created_at` datetime(0) NOT NULL COMMENT '创建时刻',
+    `created_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者',
+    `updated_at` datetime(0) NOT NULL COMMENT '更新时刻',
+    `updated_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新者',
+    `remark`     varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+    `title`      varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci  NOT NULL COMMENT '名称',
+    `value`      varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '值',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '后台角色';
+
+-- ----------------------------
+-- Records of sys_role
+-- ----------------------------
+BEGIN;
+INSERT INTO `sys_role`
+VALUES (1, 0, 1, 0, '2021-04-17 02:34:30', NULL, '2021-06-07 16:22:28', 'admin', NULL, '超级管理员(代码中固定拥有所有菜单的权限)',
+        'SuperAdmin');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for sys_role_menu_relation
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_role_menu_relation`;
+CREATE TABLE `sys_role_menu_relation`
+(
+    `id`         bigint(20) NOT NULL COMMENT '主键ID',
+    `tenant_id`  bigint(20) NULL DEFAULT NULL COMMENT '租户ID',
+    `revision`   bigint(20) NOT NULL DEFAULT 1 COMMENT '乐观锁',
+    `del_flag`   smallint(6) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识',
+    `created_at` datetime(0) NOT NULL COMMENT '创建时刻',
+    `created_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者',
+    `updated_at` datetime(0) NOT NULL COMMENT '更新时刻',
+    `updated_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新者',
+    `remark`     varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+    `role_id`    bigint(20) NOT NULL COMMENT '角色ID',
+    `menu_id`    bigint(20) NOT NULL COMMENT '菜单ID',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '后台角色-可见菜单关联';
+
+-- ----------------------------
+-- Records of sys_role_menu_relation
+-- ----------------------------
+BEGIN;
+COMMIT;
+
+-- ----------------------------
+-- Table structure for sys_tenant
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_tenant`;
+CREATE TABLE `sys_tenant`
+(
+    `id`                   bigint(20) NOT NULL COMMENT '主键ID',
+    `revision`             bigint(20) NOT NULL DEFAULT 1 COMMENT '乐观锁',
+    `del_flag`             smallint(6) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识',
+    `created_at`           datetime(0) NOT NULL COMMENT '创建时刻',
+    `created_by`           varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者',
+    `updated_at`           datetime(0) NOT NULL COMMENT '更新时刻',
+    `updated_by`           varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新者',
+    `remark`               varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+    `tenant_name`          varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '租户名',
+    `tenant_id`            bigint(20) NULL DEFAULT NULL COMMENT '租户ID',
+    `status`               int(11) NOT NULL DEFAULT 0 COMMENT '状态(0=禁用 1=启用)',
+    `tenant_admin_user_id` bigint(20) NULL DEFAULT NULL COMMENT '租户管理员用户ID',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统租户';
+
+-- ----------------------------
+-- Records of sys_tenant
+-- ----------------------------
+BEGIN;
+INSERT INTO `sys_tenant`
+VALUES (1, 1, 0, '2021-05-11 16:06:47', '', '2021-06-10 16:29:32', 'admin', '可无视SQL拦截器', '超级租户', 0, 1, 1);
+COMMIT;
+
+-- ----------------------------
+-- Table structure for sys_user
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_user`;
+CREATE TABLE `sys_user`
+(
+    `id`            bigint(20) NOT NULL COMMENT '主键ID',
+    `tenant_id`     bigint(20) NULL DEFAULT NULL COMMENT '租户ID',
+    `revision`      bigint(20) NOT NULL DEFAULT 1 COMMENT '乐观锁',
+    `del_flag`      smallint(6) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识',
+    `created_at`    datetime(0) NOT NULL COMMENT '创建时刻',
+    `created_by`    varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者',
+    `updated_at`    datetime(0) NOT NULL COMMENT '更新时刻',
+    `updated_by`    varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新者',
+    `remark`        varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+    `pin`           varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci  NOT NULL COMMENT '账号',
+    `pwd`           varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码',
+    `salt`          varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci  NOT NULL COMMENT '盐',
+    `nickname`      varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci  NOT NULL DEFAULT '' COMMENT '昵称',
+    `status`        int(11) NOT NULL DEFAULT 0 COMMENT '状态(0=禁用 1=启用)',
+    `gender`        int(11) NULL DEFAULT 0 COMMENT '性别',
+    `email`         varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '邮箱',
+    `phone_no`      varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',
+    `last_login_at` datetime(0) NULL DEFAULT NULL COMMENT '最后登录时刻',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '后台用户';
+
+-- ----------------------------
+-- Records of sys_user
+-- ----------------------------
+BEGIN;
+INSERT INTO `sys_user`
+VALUES (1, 0, 1, 0, '2021-04-06 09:56:02', NULL, '2021-06-28 16:37:07', 'admin', '', 'admin',
+        'b7f6692251833658a56ff2d09e9835b91384d203721bf27a544586d3c9a379ff', '6a8e9339-dfd1-4cfe-80cb-30ca9fc3d81e',
+        '超级管理员', 1, 0, 'admin@example.com', '17874585544', '2021-07-18 00:32:08');
+COMMIT;
+
+-- ----------------------------
+-- Table structure for sys_user_dept_relation
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_user_dept_relation`;
+CREATE TABLE `sys_user_dept_relation`
+(
+    `id`         bigint(20) NOT NULL COMMENT '主键ID',
+    `tenant_id`  bigint(20) NULL DEFAULT NULL COMMENT '租户ID',
+    `revision`   bigint(20) NOT NULL DEFAULT 1 COMMENT '乐观锁',
+    `del_flag`   smallint(6) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识',
+    `created_at` datetime(0) NOT NULL COMMENT '创建时刻',
+    `created_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者',
+    `updated_at` datetime(0) NOT NULL COMMENT '更新时刻',
+    `updated_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新者',
+    `remark`     varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+    `user_id`    bigint(20) NOT NULL COMMENT '用户ID',
+    `dept_id`    bigint(20) NOT NULL COMMENT '部门ID',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '后台用户-部门关联';
+
+-- ----------------------------
+-- Records of sys_user_dept_relation
+-- ----------------------------
+BEGIN;
+COMMIT;
+
+-- ----------------------------
+-- Table structure for sys_user_role_relation
+-- ----------------------------
+DROP TABLE IF EXISTS `sys_user_role_relation`;
+CREATE TABLE `sys_user_role_relation`
+(
+    `id`         bigint(20) NOT NULL COMMENT '主键ID',
+    `tenant_id`  bigint(20) NULL DEFAULT NULL COMMENT '租户ID',
+    `revision`   bigint(20) NOT NULL DEFAULT 1 COMMENT '乐观锁',
+    `del_flag`   smallint(6) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识',
+    `created_at` datetime(0) NOT NULL COMMENT '创建时刻',
+    `created_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者',
+    `updated_at` datetime(0) NOT NULL COMMENT '更新时刻',
+    `updated_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新者',
+    `remark`     varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+    `user_id`    bigint(20) NOT NULL COMMENT '用户ID',
+    `role_id`    bigint(20) NOT NULL COMMENT '角色ID',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '后台用户-角色关联';
+
+-- ----------------------------
+-- Records of sys_user_role_relation
+-- ----------------------------
+BEGIN;
+INSERT INTO `sys_user_role_relation`
+VALUES (1, 0, 1, 0, '2021-01-01 18:22:12', NULL, '2021-01-01 18:22:12', NULL, NULL, 1, 1);
+COMMIT;
+
+-- ----------------------------
+-- Table structure for 数据表模板
+-- ----------------------------
+DROP TABLE IF EXISTS `数据表模板`;
+CREATE TABLE `数据表模板`
+(
+    `id`         bigint(20) NOT NULL COMMENT '主键ID',
+    `tenant_id`  bigint(20) NULL DEFAULT NULL COMMENT '租户ID',
+    `revision`   bigint(20) NOT NULL DEFAULT 1 COMMENT '乐观锁',
+    `del_flag`   smallint(6) NOT NULL DEFAULT 0 COMMENT '逻辑删除标识',
+    `created_at` datetime(0) NOT NULL COMMENT '创建时刻',
+    `created_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '创建者',
+    `updated_at` datetime(0) NOT NULL COMMENT '更新时刻',
+    `updated_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '更新者',
+    `remark`     varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
+    PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '数据表注释';
+
+-- ----------------------------
+-- Records of 数据表模板
+-- ----------------------------
+BEGIN;
+COMMIT;
+
+SET
+FOREIGN_KEY_CHECKS = 1;

+ 92 - 0
pom.xml

@@ -0,0 +1,92 @@
+<?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>
+
+    <parent>
+        <groupId>cc.uncarbon.framework</groupId>
+        <artifactId>helio-starters</artifactId>
+        <version>1.2.0</version>
+    </parent>
+
+    <artifactId>helio-boot</artifactId>
+    <version>1.2.0</version>
+
+    <properties>
+    </properties>
+
+    <dependencies>
+        <!-- 脚手架starter -->
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-starter-aop</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-starter-crud</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-starter-knife4j</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.github.xiaoymin</groupId>
+                    <artifactId>knife4j-micro-spring-boot-starter</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-starter-redis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-starter-satoken</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-starter-web</artifactId>
+        </dependency>
+
+        <!-- 这里写其他第三方依赖 -->
+
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <fork>true</fork>
+                </configuration>
+            </plugin>
+
+            <!-- 跳过单元测试 -->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <skipTests>true</skipTests>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 18 - 0
src/main/java/cc/uncarbon/HelioBootApplication.java

@@ -0,0 +1,18 @@
+package cc.uncarbon;
+
+import cc.uncarbon.framework.crud.annotation.EnableInitHikariPoolAtStartup;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+/**
+ * @author Uncarbon
+ */
+@EnableInitHikariPoolAtStartup
+@SpringBootApplication
+public class HelioBootApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(HelioBootApplication.class, args);
+    }
+
+}

+ 33 - 0
src/main/java/cc/uncarbon/config/AdminSaTokenExtendConfiguration.java

@@ -0,0 +1,33 @@
+package cc.uncarbon.config;
+
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cn.dev33.satoken.stp.StpInterface;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+
+/**
+ * 自定义权限验证接口扩展
+ * 用于后台管理
+ *
+ * @author Uncarbon
+ */
+@Component
+public class AdminSaTokenExtendConfiguration implements StpInterface {
+    /**
+     * 返回一个账号所拥有的权限码集合
+     */
+    @Override
+    public List<String> getPermissionList(Object loginId, String loginType) {
+        return UserContextHolder.getUserContext().getPermissions();
+    }
+
+    /**
+     * 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
+     */
+    @Override
+    public List<String> getRoleList(Object loginId, String loginType) {
+        return UserContextHolder.getUserContext().getRoles();
+    }
+}

+ 62 - 0
src/main/java/cc/uncarbon/config/AsyncConfiguration.java

@@ -0,0 +1,62 @@
+package cc.uncarbon.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
+import org.springframework.boot.autoconfigure.task.TaskExecutionProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.AsyncConfigurer;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import javax.annotation.Resource;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+
+/**
+ * @author Uncarbon
+ */
+@Slf4j
+@EnableAsync
+@Configuration
+public class AsyncConfiguration implements AsyncConfigurer {
+
+    @Resource
+    private TaskExecutionProperties taskExecutionProperties;
+
+    @Bean(name = "taskExecutor")
+    public ThreadPoolTaskExecutor taskExecutor() {
+        final String threadNamePrefix = "taskExecutor-";
+
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        // 核心线程池大小 or 10
+        executor.setCorePoolSize(taskExecutionProperties.getPool().getCoreSize());
+        // 最大线程数 or 50
+        executor.setMaxPoolSize(taskExecutionProperties.getPool().getMaxSize());
+        // 队列容量 or 10
+        executor.setQueueCapacity(taskExecutionProperties.getPool().getQueueCapacity());
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        // 线程名前缀
+        executor.setThreadNamePrefix(threadNamePrefix);
+        // 这个配置是为了graceful shutdown?
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+
+        executor.initialize();
+        return executor;
+    }
+
+    @Override
+    public Executor getAsyncExecutor() {
+        log.debug("Creating Default Async Task Executor");
+        return this.taskExecutor();
+    }
+
+    @Override
+    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
+        return (ex, method, params) -> {
+            log.error("执行异步任务'{}'出错", method);
+            ex.printStackTrace();
+        };
+    }
+}

+ 56 - 0
src/main/java/cc/uncarbon/config/CustomInterceptorConfiguration.java

@@ -0,0 +1,56 @@
+package cc.uncarbon.config;
+
+import cc.uncarbon.framework.core.props.HelioProperties;
+import cc.uncarbon.framework.satoken.interceptor.DefaultSaTokenParseInterceptor;
+import cc.uncarbon.interceptor.AdminSaTokenParseInterceptor;
+import cc.uncarbon.interceptor.AppSaTokenRouteInterceptor;
+import cc.uncarbon.module.app.constant.AppConstant;
+import cc.uncarbon.module.sys.constant.SysConstant;
+import cn.dev33.satoken.interceptor.SaAnnotationInterceptor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import javax.annotation.Resource;
+
+
+/**
+ * 将自定义拦截器加入到拦截器队列中
+ * @author Uncarbon
+ */
+@Configuration
+public class CustomInterceptorConfiguration implements WebMvcConfigurer {
+
+    @Resource
+    private HelioProperties helioProperties;
+
+
+    @Override
+    public void addInterceptors(InterceptorRegistry registry) {
+        /*
+        1. 请求头解析, 设定用户上下文
+         */
+        registry
+                .addInterceptor(new DefaultSaTokenParseInterceptor())
+                .addPathPatterns(AppConstant.APP_MODULE_CONTEXT_PATH + "/**");
+
+        registry
+                .addInterceptor(new AdminSaTokenParseInterceptor())
+                .addPathPatterns(SysConstant.SYS_MODULE_CONTEXT_PATH + "/**");
+
+        /*
+        2. App-路由拦截器, 使几乎所有接口都需要登录
+         */
+        registry
+                .addInterceptor(new AppSaTokenRouteInterceptor(helioProperties))
+                .addPathPatterns(AppConstant.APP_MODULE_CONTEXT_PATH + "/**")
+                .excludePathPatterns(helioProperties.getSecurity().getExcludeRoutes());
+
+        /*
+        3. 注解拦截器, 启用注解功能
+         */
+        registry
+                .addInterceptor(new SaAnnotationInterceptor())
+                .addPathPatterns("/**");
+    }
+}

+ 47 - 0
src/main/java/cc/uncarbon/interceptor/AdminSaTokenParseInterceptor.java

@@ -0,0 +1,47 @@
+package cc.uncarbon.interceptor;
+
+import cc.uncarbon.framework.core.context.UserContext;
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cc.uncarbon.framework.web.util.IPUtil;
+import cc.uncarbon.module.sys.util.AdminStpUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.servlet.AsyncHandlerInterceptor;
+import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * 从请求头解析并赋值到用户上下文
+ * 其实就是"DefaultSaTokenParseInterceptor"改个名, 工具类换成"AdminStpUtil"
+ * @author Uncarbon
+ */
+@Slf4j
+public class AdminSaTokenParseInterceptor implements AsyncHandlerInterceptor {
+
+    @Override
+    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+
+        if (handler instanceof ResourceHttpRequestHandler) {
+            // 直接放行静态资源
+            return true;
+        }
+
+        // 从请求头解析用户上下文
+        if (AdminStpUtil.isLogin()) {
+            UserContext currentUser = (UserContext) AdminStpUtil.getSession().get("userContext");
+            log.debug("[SA-Token][Admin] 从请求头解析出用户上下文 >> {}", currentUser);
+
+            currentUser
+                    .setClientIP(IPUtil.getClientIPAddress(request))
+            ;
+            UserContextHolder.setUserContext(currentUser);
+        } else {
+            UserContextHolder.setUserContext(null);
+        }
+
+        return true;
+    }
+
+}

+ 38 - 0
src/main/java/cc/uncarbon/interceptor/AppSaTokenRouteInterceptor.java

@@ -0,0 +1,38 @@
+package cc.uncarbon.interceptor;
+
+import cc.uncarbon.framework.core.props.HelioProperties;
+import cc.uncarbon.module.app.constant.AppConstant;
+import cn.dev33.satoken.interceptor.SaRouteInterceptor;
+import cn.dev33.satoken.router.SaRouter;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.servlet.ModelAndView;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Collections;
+
+/**
+ * 路由拦截器,自定义验证规则
+ * http://sa-token.dev33.cn/doc/index.html#/use/route-check
+ * @author Uncarbon
+ */
+@RequiredArgsConstructor
+public class AppSaTokenRouteInterceptor extends SaRouteInterceptor {
+
+    private final HelioProperties helioProperties;
+
+
+    @Override
+    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
+        /*
+        App登录验证
+        这里是按路由匹配,除了配置文件中指定的URI外,其他URI都需要登录才行
+         */
+        SaRouter
+                .match(Collections.singletonList(AppConstant.APP_MODULE_CONTEXT_PATH + "/**"))
+                .notMatch(helioProperties.getSecurity().getExcludeRoutes())
+                .check(StpUtil::checkLogin)
+                ;
+    }
+}

+ 11 - 0
src/main/java/cc/uncarbon/module/app/constant/AppConstant.java

@@ -0,0 +1,11 @@
+package cc.uncarbon.module.app.constant;
+
+
+/**
+ * @author Uncarbon
+ */
+public interface AppConstant {
+
+    String APP_MODULE_CONTEXT_PATH = "/app";
+
+}

+ 66 - 0
src/main/java/cc/uncarbon/module/app/controller/AppAuthController.java

@@ -0,0 +1,66 @@
+package cc.uncarbon.module.app.controller;
+
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.context.UserContext;
+import cc.uncarbon.framework.web.model.response.ApiResult;
+import cc.uncarbon.module.app.constant.AppConstant;
+import cc.uncarbon.module.sys.enums.UserTypeEnum;
+import cn.dev33.satoken.stp.StpUtil;
+import cn.hutool.core.collection.CollUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * @author Uncarbon
+ */
+@Slf4j
+@Api(value = "APP鉴权接口", tags = {"APP鉴权接口"})
+@RequestMapping(AppConstant.APP_MODULE_CONTEXT_PATH + HelioConstant.Version.APP_API_VERSION_V1 + "/auth")
+@RestController
+public class AppAuthController {
+
+    @ApiOperation(value = "登录", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping("/login")
+    public ApiResult<?> login() {
+        /*
+        鉴权方法请参考AdminAuthController.login,自行根据业务实现
+         */
+
+        // 构造用户上下文
+        UserContext userContext = UserContext.builder()
+                .userId(1L)
+                .userName("App测试登录")
+                .userPhoneNo("12345678910")
+                .userType(UserTypeEnum.APP_USER_INDIVIDUAL)
+                .extraData(null)
+                .roles(CollUtil.newArrayList())
+                .permissions(CollUtil.newArrayList())
+                .relationalTenant(null)
+                .build();
+
+        // 注册到SA-Token
+        StpUtil.login(userContext.getUserId());
+        StpUtil.getSession().set("userContext", userContext);
+
+
+        // 返回登录token
+        Map<String, Object> tokenInfo = new HashMap<>(16);
+        tokenInfo.put("tokenName", StpUtil.getTokenName());
+        tokenInfo.put("tokenValue", StpUtil.getTokenValue());
+        tokenInfo.put("roles", userContext.getRoles());
+        tokenInfo.put("permissions", userContext.getPermissions());
+
+        return ApiResult.data("登录成功", tokenInfo);
+    }
+
+}

+ 22 - 0
src/main/java/cc/uncarbon/module/sys/annotation/SysLog.java

@@ -0,0 +1,22 @@
+package cc.uncarbon.module.sys.annotation;
+
+import java.lang.annotation.*;
+
+
+/**
+ * 放在方法上,可将操作记录至系统日志中
+ *
+ * @author Uncarbon
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface SysLog {
+
+    /**
+     * 操作内容(如:新增部门)
+     */
+    String value() default "";
+
+}

+ 82 - 0
src/main/java/cc/uncarbon/module/sys/aspect/SysLogAspect.java

@@ -0,0 +1,82 @@
+package cc.uncarbon.module.sys.aspect;
+
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cc.uncarbon.module.sys.annotation.SysLog;
+import cc.uncarbon.module.sys.entity.SysLogEntity;
+import cc.uncarbon.module.sys.enums.SysLogStatusEnum;
+import cc.uncarbon.module.sys.service.SysLogService;
+import cn.hutool.json.JSONUtil;
+import lombok.Data;
+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.aspectj.lang.reflect.MethodSignature;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+/**
+ * SysLog切面实现类
+ * @author Uncarbon
+ */
+@Aspect
+@Component
+@Slf4j
+@Data
+public class SysLogAspect {
+
+    @Resource
+    private SysLogService sysLogService;
+
+
+    @Pointcut("@annotation(cc.uncarbon.module.sys.annotation.SysLog)")
+    public void sysLogPointcut() {
+        // AOP Pointcut
+    }
+
+    @Around("sysLogPointcut()")
+    public Object sysLogAround(ProceedingJoinPoint point) throws Throwable {
+        // --------------------Begin @SysLog--------------------
+
+        Object executeResult;
+        executeResult = point.proceed();
+
+        SysLogEntity sysLogEntity = new SysLogEntity()
+                .setUserId(UserContextHolder.getUserId())
+                .setUsername(UserContextHolder.getUserName())
+                ;
+
+        // 请求方法
+        MethodSignature methodSignature = (MethodSignature) point.getSignature();
+        sysLogEntity.setMethod(methodSignature.toString());
+
+        // 操作内容
+        SysLog sysLogAnnotation = methodSignature.getMethod().getAnnotation(SysLog.class);
+        sysLogEntity.setOperation(sysLogAnnotation.value());
+
+        // 请求参数
+        sysLogEntity.setParams(Arrays.stream(point.getArgs()).map(JSONUtil::toJsonStr).collect(Collectors.joining(",")));
+
+        // IP地址
+        sysLogEntity.setIp(UserContextHolder.getUserContext().getClientIP());
+
+        // 状态
+        sysLogEntity.setStatus(SysLogStatusEnum.SUCCESS);
+
+        this.callSysLogServiceSave(sysLogEntity);
+        // --------------------End @SysLog--------------------
+
+        return executeResult;
+    }
+
+    @Async(value = "taskExecutor")
+    void callSysLogServiceSave(SysLogEntity sysLogEntity) {
+        sysLogService.save(sysLogEntity);
+    }
+
+}

+ 51 - 0
src/main/java/cc/uncarbon/module/sys/constant/SysConstant.java

@@ -0,0 +1,51 @@
+package cc.uncarbon.module.sys.constant;
+
+
+/**
+ * @author Uncarbon
+ */
+public interface SysConstant {
+
+    String SYS_MODULE_CONTEXT_PATH = "/sys";
+
+    /**
+     * 无上级节点的父级ID
+     */
+    Long ROOT_PARENT_ID = 0L;
+
+    /**
+     * Vben Admin后台管理-空页面
+     */
+    String VBEN_ADMIN_BLANK_VIEW = "LAYOUT";
+
+    /**
+     * 超级管理员角色ID
+     */
+    Long SUPER_ADMIN_ROLE_ID = 1L;
+
+    /**
+     * 缓存键名-侧边菜单-前缀
+     */
+    String REDIS_KEY_SIDE_MENU = "sys-service:sideMenu:";
+
+    /**
+     * 缓存键名-侧边菜单-区分用户ID
+     */
+    String REDIS_KEY_SIDE_MENU_BY_USERID = REDIS_KEY_SIDE_MENU + "userId_%s";
+
+    /**
+     * 缓存键名-所有可见菜单-前缀
+     */
+    String REDIS_KEY_VISIBLE_MENU = "sys-service:visibleMenu:";
+
+    /**
+     * 缓存键名-所有可见菜单-区分用户ID
+     */
+    String REDIS_KEY_VISIBLE_MENU_BY_USERID = REDIS_KEY_VISIBLE_MENU + "userId_%s";
+
+    /**
+     * 参数键名-缓存菜单时长
+     */
+    String PARAM_KEY_CACHE_MENU_DURATION = "sys:cache-menu-duration";
+
+}

+ 92 - 0
src/main/java/cc/uncarbon/module/sys/controller/AdminAuthController.java

@@ -0,0 +1,92 @@
+package cc.uncarbon.module.sys.controller;
+
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.context.UserContext;
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cc.uncarbon.framework.web.model.response.ApiResult;
+import cc.uncarbon.module.sys.constant.SysConstant;
+import cc.uncarbon.module.sys.enums.UserTypeEnum;
+import cc.uncarbon.module.sys.model.request.SysUserLoginDTO;
+import cc.uncarbon.module.sys.model.response.SysUserLoginBO;
+import cc.uncarbon.module.sys.service.SysMenuService;
+import cc.uncarbon.module.sys.service.SysUserService;
+import cc.uncarbon.module.sys.util.AdminStpUtil;
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * @author Uncarbon
+ */
+@Slf4j
+@Api(value = "SAAS管理后台鉴权接口", tags = {"SAAS鉴权接口"})
+@RequestMapping(SysConstant.SYS_MODULE_CONTEXT_PATH + HelioConstant.Version.SAAS_API_VERSION_V1 + "/auth")
+@RestController
+public class AdminAuthController {
+
+    @Resource
+    private SysUserService sysUserService;
+
+    @Resource
+    private SysMenuService sysMenuService;
+
+
+    @ApiOperation(value = "登录", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping(value = "/login")
+    public ApiResult<?> login(@RequestBody @Valid SysUserLoginDTO dto) {
+        // RPC调用, 失败抛异常, 成功返回用户信息
+        SysUserLoginBO userInfo = sysUserService.adminLogin(dto);
+
+        // 构造用户上下文
+        UserContext userContext = UserContext.builder()
+                .userId(userInfo.getId())
+                .userName(userInfo.getUsername())
+                .userPhoneNo(userInfo.getPhoneNo())
+                .userType(UserTypeEnum.ADMIN_USER)
+                .extraData(null)
+                .roles(userInfo.getRoles())
+                .permissions(userInfo.getPermissions())
+                .relationalTenant(userInfo.getRelationalTenant())
+                .build();
+
+        // 注册到SA-Token
+        AdminStpUtil.login(userInfo.getId(), dto.getRememberMe());
+        AdminStpUtil.getSession().set("userContext", userContext);
+
+
+        // 返回登录token
+        Map<String, Object> tokenInfo = new HashMap<>(16);
+        tokenInfo.put("tokenName", AdminStpUtil.getTokenName());
+        tokenInfo.put("tokenValue", AdminStpUtil.getTokenValue());
+        tokenInfo.put("roles", userInfo.getRoles());
+        tokenInfo.put("permissions", userInfo.getPermissions());
+
+        return ApiResult.data("登录成功", tokenInfo);
+    }
+
+    @SaCheckLogin(type = AdminStpUtil.TYPE)
+    @ApiOperation(value = "登出", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping(value = "/logout")
+    public ApiResult<?> logout() {
+        sysMenuService.cleanMenuCacheInRedis(UserContextHolder.getUserId());
+
+        AdminStpUtil.logout();
+        UserContextHolder.setUserContext(null);
+
+        return ApiResult.success();
+    }
+
+}

+ 84 - 0
src/main/java/cc/uncarbon/module/sys/controller/AdminSysDataDictController.java

@@ -0,0 +1,84 @@
+package cc.uncarbon.module.sys.controller;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.page.PageParam;
+import cc.uncarbon.framework.core.page.PageResult;
+import cc.uncarbon.framework.web.model.request.IdsDTO;
+import cc.uncarbon.framework.web.model.response.ApiResult;
+import cc.uncarbon.module.sys.constant.SysConstant;
+import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysDataDictDTO;
+import cc.uncarbon.module.sys.model.request.AdminListSysDataDictDTO;
+import cc.uncarbon.module.sys.model.response.SysDataDictBO;
+import cc.uncarbon.module.sys.service.SysDataDictService;
+import cc.uncarbon.module.sys.util.AdminStpUtil;
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+
+/**
+ * @author Uncarbon
+ */
+@SaCheckLogin(type = AdminStpUtil.TYPE)
+@Slf4j
+@Api(value = "数据字典管理接口", tags = {"数据字典管理接口"})
+@RequestMapping(SysConstant.SYS_MODULE_CONTEXT_PATH + HelioConstant.Version.SAAS_API_VERSION_V1 + "/sys/dataDicts")
+@RestController
+public class AdminSysDataDictController {
+
+    private static final String PERMISSION_PREFIX = "SysDataDict:" ;
+
+    @Resource
+    private SysDataDictService sysDataDictService;
+
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "分页列表", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping
+    public ApiResult<PageResult<SysDataDictBO>> list(PageParam pageParam, AdminListSysDataDictDTO dto) {
+        return ApiResult.data(sysDataDictService.adminList(pageParam, dto));
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "详情", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping(value = "/{id}")
+    public ApiResult<SysDataDictBO> getById(@PathVariable Long id) {
+        return ApiResult.data(sysDataDictService.getOneById(id));
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.CREATE)
+    @ApiOperation(value = "新增", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping
+    public ApiResult<?> insert(@RequestBody @Valid AdminInsertOrUpdateSysDataDictDTO dto) {
+        sysDataDictService.adminInsert(dto);
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.UPDATE)
+    @ApiOperation(value = "编辑", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PutMapping(value = "/{id}")
+    public ApiResult<?> update(@PathVariable Long id, @RequestBody @Valid AdminInsertOrUpdateSysDataDictDTO dto) {
+        dto.setId(id);
+        sysDataDictService.adminUpdate(dto);
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.DELETE)
+    @ApiOperation(value = "删除", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @DeleteMapping
+    public ApiResult<?> delete(@RequestBody @Valid IdsDTO<Long> dto) {
+        sysDataDictService.adminDelete(dto.getIds());
+
+        return ApiResult.success();
+    }
+
+}

+ 83 - 0
src/main/java/cc/uncarbon/module/sys/controller/AdminSysDeptController.java

@@ -0,0 +1,83 @@
+package cc.uncarbon.module.sys.controller;
+
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.web.model.request.IdsDTO;
+import cc.uncarbon.framework.web.model.response.ApiResult;
+import cc.uncarbon.module.sys.constant.SysConstant;
+import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysDeptDTO;
+import cc.uncarbon.module.sys.model.request.AdminListSysDeptDTO;
+import cc.uncarbon.module.sys.model.response.SysDeptBO;
+import cc.uncarbon.module.sys.service.SysDeptService;
+import cc.uncarbon.module.sys.util.AdminStpUtil;
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+/**
+ * @author Uncarbon
+ */
+@SaCheckLogin(type = AdminStpUtil.TYPE)
+@Slf4j
+@Api(value = "部门管理接口", tags = {"部门管理接口"})
+@RequestMapping(SysConstant.SYS_MODULE_CONTEXT_PATH + HelioConstant.Version.SAAS_API_VERSION_V1 + "/sys/depts")
+@RestController
+public class AdminSysDeptController {
+
+    private static final String PERMISSION_PREFIX = "SysDept:";
+    
+    @Resource
+    private SysDeptService sysDeptService;
+
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "列表", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping
+    public ApiResult<List<SysDeptBO>> list(AdminListSysDeptDTO dto) {
+        return ApiResult.data(sysDeptService.adminList(dto));
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "详情", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping(value = "/{id}")
+    public ApiResult<SysDeptBO> getById(@PathVariable Long id) {
+        return ApiResult.data(sysDeptService.getOneById(id));
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.CREATE)
+    @ApiOperation(value = "新增", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping
+    public ApiResult<?> insert(@RequestBody @Valid AdminInsertOrUpdateSysDeptDTO dto) {
+        sysDeptService.adminInsert(dto);
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.UPDATE)
+    @ApiOperation(value = "编辑", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PutMapping(value = "/{id}")
+    public ApiResult<?> update(@PathVariable Long id, @RequestBody @Valid AdminInsertOrUpdateSysDeptDTO dto) {
+        dto.setId(id);
+        sysDeptService.adminUpdate(dto);
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.DELETE)
+    @ApiOperation(value = "删除", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @DeleteMapping
+    public ApiResult<?> delete(@RequestBody @Valid IdsDTO<Long> dto) {
+        sysDeptService.adminDelete(dto.getIds());
+
+        return ApiResult.success();
+    }
+
+}

+ 56 - 0
src/main/java/cc/uncarbon/module/sys/controller/AdminSysLogController.java

@@ -0,0 +1,56 @@
+package cc.uncarbon.module.sys.controller;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.page.PageParam;
+import cc.uncarbon.framework.core.page.PageResult;
+import cc.uncarbon.framework.web.model.response.ApiResult;
+import cc.uncarbon.module.sys.constant.SysConstant;
+import cc.uncarbon.module.sys.model.request.AdminListSysLogDTO;
+import cc.uncarbon.module.sys.model.response.SysLogBO;
+import cc.uncarbon.module.sys.service.SysLogService;
+import cc.uncarbon.module.sys.util.AdminStpUtil;
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+
+/**
+ * @author Uncarbon
+ */
+@SaCheckLogin(type = AdminStpUtil.TYPE)
+@Slf4j
+@Api(value = "后台操作日志管理接口", tags = {"后台操作日志管理接口"})
+@RequestMapping(SysConstant.SYS_MODULE_CONTEXT_PATH + HelioConstant.Version.SAAS_API_VERSION_V1 + "/sys/logs")
+@RestController
+public class AdminSysLogController {
+
+    private static final String PERMISSION_PREFIX = "SysLog:";
+    
+    @Resource
+    private SysLogService sysLogService;
+
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "分页列表", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping
+    public ApiResult<PageResult<SysLogBO>> list(PageParam pageParam, AdminListSysLogDTO dto) {
+        return ApiResult.data(sysLogService.adminList(pageParam, dto));
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "详情", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping(value = "/{id}")
+    public ApiResult<SysLogBO> getById(@PathVariable Long id) {
+        return ApiResult.data(sysLogService.getOneById(id));
+    }
+
+}

+ 134 - 0
src/main/java/cc/uncarbon/module/sys/controller/AdminSysMenuController.java

@@ -0,0 +1,134 @@
+package cc.uncarbon.module.sys.controller;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cc.uncarbon.framework.web.model.request.IdsDTO;
+import cc.uncarbon.framework.web.model.response.ApiResult;
+import cc.uncarbon.module.sys.constant.SysConstant;
+import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysMenuDTO;
+import cc.uncarbon.module.sys.model.request.AdminListSysMenuDTO;
+import cc.uncarbon.module.sys.model.response.SysMenuBO;
+import cc.uncarbon.module.sys.service.SysMenuService;
+import cc.uncarbon.module.sys.service.SysParamService;
+import cc.uncarbon.module.sys.util.AdminStpUtil;
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.hutool.json.JSONUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * @author Uncarbon
+ */
+@SaCheckLogin(type = AdminStpUtil.TYPE)
+@Slf4j
+@Api(value = "后台菜单管理接口", tags = {"后台菜单管理接口"})
+@RequestMapping(SysConstant.SYS_MODULE_CONTEXT_PATH + HelioConstant.Version.SAAS_API_VERSION_V1 + "/sys/menus")
+@RestController
+public class AdminSysMenuController {
+
+    private static final String PERMISSION_PREFIX = "SysMenu:";
+
+    @Resource
+    private SysMenuService sysMenuService;
+
+    @Resource
+    private SysParamService sysParamService;
+
+    @Resource
+    private StringRedisTemplate stringRedisTemplate;
+
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "列表", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping
+    public ApiResult<List<SysMenuBO>> list(AdminListSysMenuDTO dto) {
+        return ApiResult.data(sysMenuService.adminList(dto));
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "详情", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping(value = "/{id}")
+    public ApiResult<SysMenuBO> getById(@PathVariable Long id) {
+        return ApiResult.data(sysMenuService.getOneById(id));
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.CREATE)
+    @ApiOperation(value = "新增", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping
+    public ApiResult<?> insert(@RequestBody @Valid AdminInsertOrUpdateSysMenuDTO dto) {
+        sysMenuService.adminInsert(dto);
+        sysMenuService.cleanMenuCacheInRedis();
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.UPDATE)
+    @ApiOperation(value = "编辑", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PutMapping(value = "/{id}")
+    public ApiResult<?> update(@PathVariable Long id, @RequestBody @Valid AdminInsertOrUpdateSysMenuDTO dto) {
+        dto.setId(id);
+        sysMenuService.adminUpdate(dto);
+        sysMenuService.cleanMenuCacheInRedis();
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.DELETE)
+    @ApiOperation(value = "删除", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @DeleteMapping
+    public ApiResult<?> delete(@RequestBody @Valid IdsDTO<Long> dto) {
+        sysMenuService.adminDelete(dto.getIds());
+        sysMenuService.cleanMenuCacheInRedis();
+
+        return ApiResult.success();
+    }
+
+    @ApiOperation(value = "取当前账号可见侧边菜单", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping("/side")
+    public ApiResult<List<SysMenuBO>> adminListSideMenu() {
+        String redisKey = String.format(SysConstant.REDIS_KEY_SIDE_MENU_BY_USERID, UserContextHolder.getUserContext().getUserId());
+        Object redisValue = stringRedisTemplate.opsForValue().get(redisKey);
+
+        if (redisValue == null) {
+            redisValue = sysMenuService.adminListSideMenu();
+            // 记录到缓存
+            String sysMenuCacheDuration = sysParamService.getParamValueByKey(SysConstant.PARAM_KEY_CACHE_MENU_DURATION, "30");
+            stringRedisTemplate.opsForValue().set(redisKey, JSONUtil.toJsonStr(redisValue), Integer.parseInt(sysMenuCacheDuration), TimeUnit.MINUTES);
+        } else {
+            redisValue = JSONUtil.parse(redisValue).toBean(ArrayList.class);
+        }
+
+        return ApiResult.data((List<SysMenuBO>) redisValue);
+    }
+
+    @ApiOperation(value = "取当前账号所有可见菜单", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping("/all")
+    public ApiResult<List<SysMenuBO>> adminListVisibleMenu() {
+        String redisKey = String.format(SysConstant.REDIS_KEY_VISIBLE_MENU_BY_USERID, UserContextHolder.getUserContext().getUserId());
+        Object redisValue = stringRedisTemplate.opsForValue().get(redisKey);
+
+        if (redisValue == null) {
+            redisValue = sysMenuService.adminListVisibleMenu();
+            // 记录到缓存
+            String sysMenuCacheDuration = sysParamService.getParamValueByKey(SysConstant.PARAM_KEY_CACHE_MENU_DURATION, "30");
+            stringRedisTemplate.opsForValue().set(redisKey, JSONUtil.toJsonStr(redisValue), Integer.parseInt(sysMenuCacheDuration), TimeUnit.MINUTES);
+        } else {
+            redisValue = JSONUtil.parse(redisValue).toBean(ArrayList.class);
+        }
+
+        return ApiResult.data((List<SysMenuBO>) redisValue);
+    }
+
+}

+ 83 - 0
src/main/java/cc/uncarbon/module/sys/controller/AdminSysParamController.java

@@ -0,0 +1,83 @@
+package cc.uncarbon.module.sys.controller;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.page.PageParam;
+import cc.uncarbon.framework.core.page.PageResult;
+import cc.uncarbon.framework.web.model.request.IdsDTO;
+import cc.uncarbon.framework.web.model.response.ApiResult;
+import cc.uncarbon.module.sys.constant.SysConstant;
+import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysParamDTO;
+import cc.uncarbon.module.sys.model.request.AdminListSysParamDTO;
+import cc.uncarbon.module.sys.model.response.SysParamBO;
+import cc.uncarbon.module.sys.service.SysParamService;
+import cc.uncarbon.module.sys.util.AdminStpUtil;
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+/**
+ * @author Uncarbon
+ */
+@SaCheckLogin(type = AdminStpUtil.TYPE)
+@Slf4j
+@Api(value = "系统参数管理接口", tags = {"系统参数管理接口"})
+@RequestMapping(SysConstant.SYS_MODULE_CONTEXT_PATH + HelioConstant.Version.SAAS_API_VERSION_V1 + "/sys/params")
+@RestController
+public class AdminSysParamController {
+
+    private static final String PERMISSION_PREFIX = "SysParam:";
+    
+    @Resource
+    private SysParamService sysParamService;
+
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "分页列表", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping
+    public ApiResult<PageResult<SysParamBO>> list(PageParam pageParam, AdminListSysParamDTO dto) {
+        return ApiResult.data(sysParamService.adminList(pageParam, dto));
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "详情", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping(value = "/{id}")
+    public ApiResult<SysParamBO> getById(@PathVariable Long id) {
+        return ApiResult.data(sysParamService.getOneById(id));
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.CREATE)
+    @ApiOperation(value = "新增", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping
+    public ApiResult<?> insert(@RequestBody @Valid AdminInsertOrUpdateSysParamDTO dto) {
+        sysParamService.adminInsert(dto);
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.UPDATE)
+    @ApiOperation(value = "编辑", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PutMapping(value = "/{id}")
+    public ApiResult<?> update(@PathVariable Long id, @RequestBody @Valid AdminInsertOrUpdateSysParamDTO dto) {
+        dto.setId(id);
+        sysParamService.adminUpdate(dto);
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.DELETE)
+    @ApiOperation(value = "删除", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @DeleteMapping
+    public ApiResult<?> delete(@RequestBody @Valid IdsDTO<Long> dto) {
+        sysParamService.adminDelete(dto.getIds());
+
+        return ApiResult.success();
+    }
+
+}

+ 110 - 0
src/main/java/cc/uncarbon/module/sys/controller/AdminSysRoleController.java

@@ -0,0 +1,110 @@
+package cc.uncarbon.module.sys.controller;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.page.PageParam;
+import cc.uncarbon.framework.core.page.PageResult;
+import cc.uncarbon.framework.web.model.request.IdsDTO;
+import cc.uncarbon.framework.web.model.response.ApiResult;
+import cc.uncarbon.module.sys.constant.SysConstant;
+import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysRoleDTO;
+import cc.uncarbon.module.sys.model.request.AdminListSysRoleDTO;
+import cc.uncarbon.module.sys.model.response.SysRoleBO;
+import cc.uncarbon.module.sys.service.SysRoleService;
+import cc.uncarbon.module.sys.service.SysUserRoleRelationService;
+import cc.uncarbon.module.sys.util.AdminStpUtil;
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.hutool.core.collection.CollUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.List;
+
+
+/**
+ * @author Uncarbon
+ */
+@SaCheckLogin(type = AdminStpUtil.TYPE)
+@Slf4j
+@Api(value = "后台角色管理接口", tags = {"后台角色管理接口"})
+@RequestMapping(SysConstant.SYS_MODULE_CONTEXT_PATH + HelioConstant.Version.SAAS_API_VERSION_V1 + "/sys/roles")
+@RestController
+public class AdminSysRoleController {
+
+    private static final String PERMISSION_PREFIX = "SysRole:";
+
+    @Resource
+    private SysRoleService sysRoleService;
+
+    @Resource
+    private SysUserRoleRelationService sysUserRoleRelationService;
+
+    @Resource
+    private StringRedisTemplate stringRedisTemplate;
+
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "分页列表", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping
+    public ApiResult<PageResult<SysRoleBO>> list(PageParam pageParam, AdminListSysRoleDTO dto) {
+        return ApiResult.data(sysRoleService.adminList(pageParam, dto));
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "详情", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping(value = "/{id}")
+    public ApiResult<SysRoleBO> getById(@PathVariable Long id) {
+        return ApiResult.data(sysRoleService.getOneById(id));
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.CREATE)
+    @ApiOperation(value = "新增", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping
+    public ApiResult<?> insert(@RequestBody @Valid AdminInsertOrUpdateSysRoleDTO dto) {
+        sysRoleService.adminInsert(dto);
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.UPDATE)
+    @ApiOperation(value = "编辑", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PutMapping(value = "/{id}")
+    public ApiResult<?> update(@PathVariable Long id, @RequestBody @Valid AdminInsertOrUpdateSysRoleDTO dto) {
+        dto.setId(id);
+        sysRoleService.adminUpdate(dto);
+
+        /*
+        清除该角色下所有用户, 菜单相关的缓存
+         */
+        List<Long> userIds = sysUserRoleRelationService.listUserIdByRoleIds(CollUtil.newArrayList(dto.getId()));
+        userIds.forEach(
+                userId -> {
+                    String redisKey;
+
+                    redisKey = String.format(SysConstant.REDIS_KEY_SIDE_MENU_BY_USERID, userId);
+                    stringRedisTemplate.delete(redisKey);
+
+                    redisKey = String.format(SysConstant.REDIS_KEY_VISIBLE_MENU_BY_USERID, userId);
+                    stringRedisTemplate.delete(redisKey);
+                }
+        );
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.DELETE)
+    @ApiOperation(value = "删除", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @DeleteMapping
+    public ApiResult<?> delete(@RequestBody @Valid IdsDTO<Long> dto) {
+        sysRoleService.adminDelete(dto.getIds());
+
+        return ApiResult.success();
+    }
+
+}

+ 85 - 0
src/main/java/cc/uncarbon/module/sys/controller/AdminSysTenantController.java

@@ -0,0 +1,85 @@
+package cc.uncarbon.module.sys.controller;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.page.PageParam;
+import cc.uncarbon.framework.core.page.PageResult;
+import cc.uncarbon.framework.web.model.request.IdsDTO;
+import cc.uncarbon.framework.web.model.response.ApiResult;
+import cc.uncarbon.module.sys.constant.SysConstant;
+import cc.uncarbon.module.sys.model.request.AdminInsertSysTenantDTO;
+import cc.uncarbon.module.sys.model.request.AdminListSysTenantDTO;
+import cc.uncarbon.module.sys.model.request.AdminUpdateSysTenantDTO;
+import cc.uncarbon.module.sys.model.response.SysTenantBO;
+import cc.uncarbon.module.sys.service.SysTenantService;
+import cc.uncarbon.module.sys.util.AdminStpUtil;
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+
+/**
+ * @author Uncarbon
+ */
+@SaCheckLogin(type = AdminStpUtil.TYPE)
+@Slf4j
+@Api(value = "系统租户管理接口", tags = {"系统租户管理接口"})
+@RequestMapping(SysConstant.SYS_MODULE_CONTEXT_PATH + HelioConstant.Version.SAAS_API_VERSION_V1 + "/sys/tenants")
+@RestController
+public class AdminSysTenantController {
+
+    private static final String PERMISSION_PREFIX = "SysTenant:";
+    
+    @Resource
+    private SysTenantService sysTenantService;
+
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "分页列表", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping
+    public ApiResult<PageResult<SysTenantBO>> list(PageParam pageParam, AdminListSysTenantDTO dto) {
+        return ApiResult.data(sysTenantService.adminList(pageParam, dto));
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "详情", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping(value = "/{id}")
+    public ApiResult<SysTenantBO> getById(@PathVariable Long id) {
+        return ApiResult.data(sysTenantService.getOneById(id));
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.CREATE)
+    @ApiOperation(value = "新增", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping
+    public ApiResult<?> insert(@RequestBody @Valid AdminInsertSysTenantDTO dto) {
+        sysTenantService.adminInsert(dto);
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.UPDATE)
+    @ApiOperation(value = "编辑", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PutMapping(value = "/{id}")
+    public ApiResult<?> update(@PathVariable Long id, @RequestBody @Valid AdminUpdateSysTenantDTO dto) {
+        dto.setId(id);
+        sysTenantService.adminUpdate(dto);
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.DELETE)
+    @ApiOperation(value = "删除", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @DeleteMapping
+    public ApiResult<?> delete(@RequestBody @Valid IdsDTO<Long> dto) {
+        sysTenantService.adminDelete(dto.getIds());
+
+        return ApiResult.success();
+    }
+
+}

+ 139 - 0
src/main/java/cc/uncarbon/module/sys/controller/AdminSysUserController.java

@@ -0,0 +1,139 @@
+package cc.uncarbon.module.sys.controller;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.exception.BusinessException;
+import cc.uncarbon.framework.core.page.PageParam;
+import cc.uncarbon.framework.core.page.PageResult;
+import cc.uncarbon.framework.web.model.request.IdsDTO;
+import cc.uncarbon.framework.web.model.response.ApiResult;
+import cc.uncarbon.module.sys.constant.SysConstant;
+import cc.uncarbon.module.sys.model.request.*;
+import cc.uncarbon.module.sys.model.response.SysUserBO;
+import cc.uncarbon.module.sys.service.SysUserService;
+import cc.uncarbon.module.sys.util.AdminStpUtil;
+import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.hutool.core.util.StrUtil;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+
+/**
+ * @author Uncarbon
+ */
+@SaCheckLogin(type = AdminStpUtil.TYPE)
+@Slf4j
+@Api(value = "后台用户管理接口", tags = {"后台用户管理接口"})
+@RequestMapping(SysConstant.SYS_MODULE_CONTEXT_PATH + HelioConstant.Version.SAAS_API_VERSION_V1 + "/sys/users")
+@RestController
+public class AdminSysUserController {
+
+    private static final String PERMISSION_PREFIX = "SysUser:";
+    
+    @Resource
+    private SysUserService sysUserService;
+
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "分页列表", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping
+    public ApiResult<PageResult<SysUserBO>> list(PageParam pageParam, AdminListSysUserDTO dto) {
+        return ApiResult.data(sysUserService.adminList(pageParam, dto));
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
+    @ApiOperation(value = "详情", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping(value = "/{id}")
+    public ApiResult<SysUserBO> getById(@PathVariable Long id) {
+        return ApiResult.data(sysUserService.getOneById(id));
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.CREATE)
+    @ApiOperation(value = "新增", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping
+    public ApiResult<?> insert(@RequestBody @Valid AdminInsertOrUpdateSysUserDTO dto) {
+        sysUserService.adminInsert(dto);
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.UPDATE)
+    @ApiOperation(value = "编辑", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PutMapping(value = "/{id}")
+    public ApiResult<?> update(@PathVariable Long id, @RequestBody @Valid AdminInsertOrUpdateSysUserDTO dto) {
+        dto.setId(id);
+        sysUserService.adminUpdate(dto);
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.DELETE)
+    @ApiOperation(value = "删除", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @DeleteMapping
+    public ApiResult<?> delete(@RequestBody @Valid IdsDTO<Long> dto) {
+        sysUserService.adminDelete(dto.getIds());
+
+        return ApiResult.success();
+    }
+
+    @ApiOperation(value = "取当前用户信息", produces = MediaType.APPLICATION_JSON_VALUE)
+    @GetMapping(value = "/info")
+    public ApiResult<?> getCurrentUserInfo() {
+        return ApiResult.data(sysUserService.adminGetCurrentUserInfo());
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = "SysUser:resetPassword")
+    @ApiOperation(value = "重置某用户密码", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping(value = "/resetPassword")
+    public ApiResult<?> resetPassword(@RequestBody @Valid AdminResetSysUserPasswordDTO dto) {
+        sysUserService.adminResetUserPassword(dto);
+
+        // 踢出原登录
+        AdminStpUtil.kickout(dto.getUserId());
+
+        return ApiResult.success();
+    }
+
+    @ApiOperation(value = "修改当前用户密码", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping(value = "/updatePassword")
+    public ApiResult<?> updatePassword(@RequestBody @Valid AdminUpdateCurrentSysUserPasswordDTO dto) {
+        if (StrUtil.isBlank(dto.getNewPassword()) || StrUtil.isBlank(dto.getConfirmNewPassword())) {
+            throw new BusinessException(400, "密码或确认密码不能为空");
+        }
+
+        if (!dto.getConfirmNewPassword().equals(dto.getNewPassword())) {
+            throw new BusinessException(400, "密码与确认密码不同,请检查");
+        }
+        sysUserService.adminUpdateCurrentUserPassword(dto);
+
+        // 用户更改密码后使其当前会话直接过期
+        AdminStpUtil.logout();
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = "SysUser:bindRoles")
+    @ApiOperation(value = "绑定用户与角色关联关系", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping(value = "/bindRoles")
+    public ApiResult<?> bindRoles(@RequestBody @Valid AdminBindUserRoleRelationDTO dto) {
+        sysUserService.adminBindRoles(dto);
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = "SysUser:kickOut")
+    @ApiOperation(value = "踢某用户下线", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping(value = "/kickOut")
+    public ApiResult<?> kickOut(@RequestBody @Valid AdminKickOutSysUserDTO dto) {
+        AdminStpUtil.kickout(dto.getUserId());
+
+        return ApiResult.success();
+    }
+
+}

+ 61 - 0
src/main/java/cc/uncarbon/module/sys/entity/SysDataDictEntity.java

@@ -0,0 +1,61 @@
+package cc.uncarbon.module.sys.entity;
+
+import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+
+/**
+ * 数据字典
+ *
+ * @author Uncarbon
+ */
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@TableName(value = "sys_data_dict")
+public class SysDataDictEntity extends HelioBaseEntity<Long> {
+
+	@ApiModelProperty(value = "驼峰式键名")
+	@TableField(value = "camel_case_key")
+	private String camelCaseKey;
+
+	@ApiModelProperty(value = "下划线式键名")
+	@TableField(value = "under_case_key")
+	private String underCaseKey;
+
+	@ApiModelProperty(value = "帕斯卡式键名")
+	@TableField(value = "pascal_case_key")
+	private String pascalCaseKey;
+
+	@ApiModelProperty(value = "键值")
+	@TableField(value = "value")
+	private String value;
+
+	@ApiModelProperty(value = "参数描述")
+	@TableField(value = "description")
+	private String description;
+
+	@ApiModelProperty(value = "单位")
+	@TableField(value = "unit")
+	private String unit;
+
+	@ApiModelProperty(value = "取值范围")
+	@TableField(value = "value_range")
+	private String valueRange;
+
+	@ApiModelProperty(value = "别称键名")
+	@TableField(value = "alias_key")
+	private String aliasKey;
+
+}

+ 45 - 0
src/main/java/cc/uncarbon/module/sys/entity/SysDeptEntity.java

@@ -0,0 +1,45 @@
+package cc.uncarbon.module.sys.entity;
+
+import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import cc.uncarbon.module.sys.enums.GenericStatusEnum;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+
+/**
+ * 部门
+ * @author Uncarbon
+ */
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@TableName(value = "sys_dept")
+public class SysDeptEntity extends HelioBaseEntity<Long> {
+
+	@ApiModelProperty(value = "名称")
+	@TableField(value = "title")
+	private String title;
+
+	@ApiModelProperty(value = "上级ID(无上级节点设置为0)")
+	@TableField(value = "parent_id")
+	private Long parentId;
+
+	@ApiModelProperty(value = "排序")
+	@TableField(value = "sort")
+	private Integer sort;
+
+	@ApiModelProperty(value = "状态(0=禁用 1=启用)")
+	@TableField(value = "status")
+	private GenericStatusEnum status;
+
+}

+ 57 - 0
src/main/java/cc/uncarbon/module/sys/entity/SysLogEntity.java

@@ -0,0 +1,57 @@
+package cc.uncarbon.module.sys.entity;
+
+import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import cc.uncarbon.module.sys.enums.SysLogStatusEnum;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+
+/**
+ * 后台操作日志
+ * @author Uncarbon
+ */
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@TableName(value = "sys_log")
+public class SysLogEntity extends HelioBaseEntity<Long> {
+
+	@ApiModelProperty(value = "用户ID")
+	@TableField(value = "user_id")
+	private Long userId;
+
+	@ApiModelProperty(value = "用户账号")
+	@TableField(value = "username")
+	private String username;
+
+	@ApiModelProperty(value = "操作内容")
+	@TableField(value = "operation")
+	private String operation;
+
+	@ApiModelProperty(value = "请求方法")
+	@TableField(value = "method")
+	private String method;
+
+	@ApiModelProperty(value = "请求参数")
+	@TableField(value = "params")
+	private String params;
+
+	@ApiModelProperty(value = "IP地址")
+	@TableField(value = "ip")
+	private String ip;
+
+	@ApiModelProperty(value = "状态")
+	@TableField(value = "status")
+	private SysLogStatusEnum status;
+
+}

+ 66 - 0
src/main/java/cc/uncarbon/module/sys/entity/SysMenuEntity.java

@@ -0,0 +1,66 @@
+package cc.uncarbon.module.sys.entity;
+
+import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import cc.uncarbon.module.sys.enums.GenericStatusEnum;
+import cc.uncarbon.module.sys.enums.SysMenuTypeEnum;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+
+/**
+ * 后台菜单
+ * @author Uncarbon
+ */
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@TableName(value = "sys_menu")
+public class SysMenuEntity extends HelioBaseEntity<Long> {
+
+	@ApiModelProperty(value = "名称")
+	@TableField(value = "title")
+	private String title;
+
+	@ApiModelProperty(value = "上级菜单ID")
+	@TableField(value = "parent_id")
+	private Long parentId;
+
+	@ApiModelProperty(value = "菜单类型(参考MenuTypeEnum)")
+	@TableField(value = "type")
+	private SysMenuTypeEnum type;
+
+	@ApiModelProperty(value = "权限标识")
+	@TableField(value = "permission")
+	private String permission;
+
+	@ApiModelProperty(value = "图标")
+	@TableField(value = "icon")
+	private String icon;
+
+	@ApiModelProperty(value = "排序")
+	@TableField(value = "sort")
+	private Integer sort;
+
+	@ApiModelProperty(value = "状态(0=禁用 1=启用)")
+	@TableField(value = "status")
+	private GenericStatusEnum status;
+
+	@ApiModelProperty(value = "组件", notes = "Vue项目中`/@/views/`的子路径; `LAYOUT`为空页面")
+	@TableField(value = "component")
+	private String component;
+
+	@ApiModelProperty(value = "外链地址")
+	@TableField(value = "external_link")
+	private String externalLink;
+
+}

+ 40 - 0
src/main/java/cc/uncarbon/module/sys/entity/SysParamEntity.java

@@ -0,0 +1,40 @@
+package cc.uncarbon.module.sys.entity;
+
+import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+
+/**
+ * 系统参数
+ * @author Uncarbon
+ */
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@TableName(value = "sys_param")
+public class SysParamEntity extends HelioBaseEntity<Long> {
+
+    @ApiModelProperty(value = "键名")
+    @TableField(value = "`key`")
+    private String key;
+
+    @ApiModelProperty(value = "键值")
+    @TableField(value = "value")
+    private String value;
+
+    @ApiModelProperty(value = "描述")
+    @TableField(value = "description")
+    private String description;
+
+}

+ 36 - 0
src/main/java/cc/uncarbon/module/sys/entity/SysRoleEntity.java

@@ -0,0 +1,36 @@
+package cc.uncarbon.module.sys.entity;
+
+import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+
+/**
+ * 后台角色
+ * @author Uncarbon
+ */
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@TableName(value = "sys_role")
+public class SysRoleEntity extends HelioBaseEntity<Long> {
+
+	@ApiModelProperty(value = "名称")
+	@TableField(value = "title")
+	private String title;
+
+	@ApiModelProperty(value = "值")
+	@TableField(value = "value")
+	private String value;
+
+}

+ 36 - 0
src/main/java/cc/uncarbon/module/sys/entity/SysRoleMenuRelationEntity.java

@@ -0,0 +1,36 @@
+package cc.uncarbon.module.sys.entity;
+
+import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+
+/**
+ * 后台角色-可见菜单关联
+ * @author Uncarbon
+ */
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@TableName(value = "sys_role_menu_relation")
+public class SysRoleMenuRelationEntity extends HelioBaseEntity<Long> {
+
+	@ApiModelProperty(value = "角色ID")
+	@TableField(value = "role_id")
+	private Long roleId;
+
+	@ApiModelProperty(value = "菜单ID")
+	@TableField(value = "menu_id")
+	private Long menuId;
+
+}

+ 41 - 0
src/main/java/cc/uncarbon/module/sys/entity/SysTenantEntity.java

@@ -0,0 +1,41 @@
+package cc.uncarbon.module.sys.entity;
+
+import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import cc.uncarbon.module.sys.enums.GenericStatusEnum;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+
+/**
+ * 系统租户
+ * @author Uncarbon
+ */
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@TableName(value = "sys_tenant")
+public class SysTenantEntity extends HelioBaseEntity<Long> {
+
+    @ApiModelProperty(value = "租户名")
+    @TableField(value = "tenant_name")
+    private String tenantName;
+
+    @ApiModelProperty(value = "状态(0=禁用 1=启用)")
+    @TableField(value = "status")
+    private GenericStatusEnum status;
+
+    @ApiModelProperty(value = "租户管理员用户ID")
+    @TableField(value = "tenant_admin_user_id")
+    private Long tenantAdminUserId;
+
+}

+ 36 - 0
src/main/java/cc/uncarbon/module/sys/entity/SysUserDeptRelationEntity.java

@@ -0,0 +1,36 @@
+package cc.uncarbon.module.sys.entity;
+
+import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+
+/**
+ * 后台用户-部门关联
+ * @author Uncarbon
+ */
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@TableName(value = "sys_user_dept_relation")
+public class SysUserDeptRelationEntity extends HelioBaseEntity<Long> {
+
+	@ApiModelProperty(value = "用户ID")
+	@TableField(value = "user_id")
+	private Long userId;
+
+	@ApiModelProperty(value = "部门ID")
+	@TableField(value = "dept_id")
+	private Long deptId;
+
+}

+ 70 - 0
src/main/java/cc/uncarbon/module/sys/entity/SysUserEntity.java

@@ -0,0 +1,70 @@
+package cc.uncarbon.module.sys.entity;
+
+import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import cc.uncarbon.module.sys.enums.GenderEnum;
+import cc.uncarbon.module.sys.enums.SysUserStatusEnum;
+import cn.hutool.core.annotation.Alias;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+import java.time.LocalDateTime;
+
+
+/**
+ * 后台用户
+ * @author Uncarbon
+ */
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@TableName(value = "sys_user")
+public class SysUserEntity extends HelioBaseEntity<Long> {
+
+	@ApiModelProperty(value = "账号")
+	@Alias(value = "username")
+	@TableField(value = "pin")
+	private String pin;
+
+	@ApiModelProperty(value = "密码")
+	@TableField(value = "pwd")
+	private String pwd;
+
+	@ApiModelProperty(value = "盐")
+	@TableField(value = "salt")
+	private String salt;
+
+	@ApiModelProperty(value = "昵称")
+	@TableField(value = "nickname")
+	private String nickname;
+
+	@ApiModelProperty(value = "状态(0=正常 -1=封禁)")
+	@TableField(value = "status")
+	private SysUserStatusEnum status;
+
+	@ApiModelProperty(value = "性别")
+	@TableField(value = "gender")
+	private GenderEnum gender;
+
+	@ApiModelProperty(value = "邮箱")
+	@TableField(value = "email")
+	private String email;
+
+	@ApiModelProperty(value = "手机号")
+	@TableField(value = "phone_no")
+	private String phoneNo;
+
+	@ApiModelProperty(value = "最后登录时刻")
+	@TableField(value = "last_login_at")
+	private LocalDateTime lastLoginAt;
+
+}

+ 36 - 0
src/main/java/cc/uncarbon/module/sys/entity/SysUserRoleRelationEntity.java

@@ -0,0 +1,36 @@
+package cc.uncarbon.module.sys.entity;
+
+import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+
+/**
+ * 后台用户-角色关联
+ * @author Uncarbon
+ */
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@SuperBuilder(toBuilder = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+@TableName(value = "sys_user_role_relation")
+public class SysUserRoleRelationEntity extends HelioBaseEntity<Long> {
+
+	@ApiModelProperty(value = "用户ID")
+	@TableField(value = "user_id")
+	private Long userId;
+
+	@ApiModelProperty(value = "角色ID")
+	@TableField(value = "role_id")
+	private Long roleId;
+
+}

+ 37 - 0
src/main/java/cc/uncarbon/module/sys/enums/GenderEnum.java

@@ -0,0 +1,37 @@
+package cc.uncarbon.module.sys.enums;
+
+import cc.uncarbon.framework.core.enums.HelioBaseEnum;
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 生理性别枚举类
+ * @author Uncarbon
+ */
+@AllArgsConstructor
+@Getter
+public enum GenderEnum implements HelioBaseEnum<Integer> {
+
+    /**
+     * 未知
+     */
+    UNKNOWN(0, "未知"),
+
+    /**
+     * 男
+     */
+    MALE(1, "男"),
+
+    /**
+     * 女
+     */
+    FEMALE(2, "女"),
+    ;
+
+    @EnumValue
+    private final Integer value;
+    private final String label;
+
+}

+ 32 - 0
src/main/java/cc/uncarbon/module/sys/enums/GenericStatusEnum.java

@@ -0,0 +1,32 @@
+package cc.uncarbon.module.sys.enums;
+
+import cc.uncarbon.framework.core.enums.HelioBaseEnum;
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 通用状态枚举类
+ * @author Uncarbon
+ */
+@AllArgsConstructor
+@Getter
+public enum GenericStatusEnum implements HelioBaseEnum<Integer> {
+
+    /**
+     * 禁用
+     */
+    DISABLED(0, "禁用"),
+
+    /**
+     * 启用
+     */
+    ENABLED(1, "启用"),
+    ;
+
+    @EnumValue
+    private final Integer value;
+    private final String label;
+
+}

+ 38 - 0
src/main/java/cc/uncarbon/module/sys/enums/SysErrorEnum.java

@@ -0,0 +1,38 @@
+package cc.uncarbon.module.sys.enums;
+
+import cc.uncarbon.framework.core.enums.HelioBaseEnum;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 后台管理异常枚举类
+ * @author Uncarbon
+ */
+@AllArgsConstructor
+@Getter
+public enum SysErrorEnum implements HelioBaseEnum<Integer> {
+
+    INVALID_ID(400, "无效ID"),
+
+    USER_NOT_EXISTS(400, "用户不存在"),
+
+    INCORRECT_USER_PASSWORD(400, "密码不正确"),
+
+    BANNED_USER(400, "用户被封禁"),
+
+    INVALID_TENANT(400, "所属租户无效"),
+
+    DISABLED_TENANT(400, "所属租户已禁用"),
+
+    INCORRECT_OLD_PASSWORD(400, "原密码有误"),
+
+    NO_ROLE_AVAILABLE_FOR_CURRENT_USER(400, "当前用户没有可用角色"),
+
+    NO_MENU_AVAILABLE_FOR_CURRENT_ROLE(400, "当前角色没有可用菜单"),
+    ;
+
+    private final Integer value;
+    private final String label;
+
+}

+ 32 - 0
src/main/java/cc/uncarbon/module/sys/enums/SysLogStatusEnum.java

@@ -0,0 +1,32 @@
+package cc.uncarbon.module.sys.enums;
+
+import cc.uncarbon.framework.core.enums.HelioBaseEnum;
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 系统日志状态枚举类
+ * @author Uncarbon
+ */
+@AllArgsConstructor
+@Getter
+public enum SysLogStatusEnum implements HelioBaseEnum<Integer> {
+
+    /**
+     * 未执行
+     */
+    NON_EXECUTION(0, "未执行"),
+
+    /**
+     * 成功
+     */
+    SUCCESS(1, "成功"),
+    ;
+
+    @EnumValue
+    private final Integer value;
+    private final String label;
+
+}

+ 42 - 0
src/main/java/cc/uncarbon/module/sys/enums/SysMenuTypeEnum.java

@@ -0,0 +1,42 @@
+package cc.uncarbon.module.sys.enums;
+
+import cc.uncarbon.framework.core.enums.HelioBaseEnum;
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 菜单类型枚举类
+ * @author Uncarbon
+ */
+@AllArgsConstructor
+@Getter
+public enum SysMenuTypeEnum implements HelioBaseEnum<Integer> {
+
+    /**
+     * 可以认为是父级菜单
+     */
+    DIR(0, "目录"),
+
+    /**
+     * 可以认为是子菜单
+     */
+    MENU(1, "菜单"),
+
+    /**
+     * 可以认为是页内按钮
+     */
+    BUTTON(2, "按钮"),
+
+    /**
+     * 外链
+     */
+    EXTERNAL_LINK(3, "外链"),
+    ;
+
+    @EnumValue
+    private final Integer value;
+    private final String label;
+
+}

+ 32 - 0
src/main/java/cc/uncarbon/module/sys/enums/SysUserStatusEnum.java

@@ -0,0 +1,32 @@
+package cc.uncarbon.module.sys.enums;
+
+import cc.uncarbon.framework.core.enums.HelioBaseEnum;
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 后台用户状态枚举类
+ * @author Uncarbon
+ */
+@AllArgsConstructor
+@Getter
+public enum SysUserStatusEnum implements HelioBaseEnum<Integer> {
+
+    /**
+     * 封禁
+     */
+    BANNED(0, "封禁"),
+
+    /**
+     * 正常
+     */
+    ENABLED(1, "正常"),
+    ;
+
+    @EnumValue
+    private final Integer value;
+    private final String label;
+
+}

+ 32 - 0
src/main/java/cc/uncarbon/module/sys/enums/UserTypeEnum.java

@@ -0,0 +1,32 @@
+package cc.uncarbon.module.sys.enums;
+
+
+import cc.uncarbon.framework.core.enums.HelioBaseEnum;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+
+/**
+ * 用户类型枚举类
+ * @author Uncarbon
+ */
+@AllArgsConstructor
+@Getter
+public enum UserTypeEnum implements HelioBaseEnum<Integer> {
+
+    /**
+     * 后台管理用户
+     */
+    ADMIN_USER(1, "后台管理用户"),
+
+    /**
+     * C端个人用户
+     * 未来可以自行扩展企业用户
+     */
+    APP_USER_INDIVIDUAL(2, "C端个人用户"),
+    ;
+
+    private final Integer value;
+    private final String label;
+
+}

+ 16 - 0
src/main/java/cc/uncarbon/module/sys/mapper/SysDataDictMapper.java

@@ -0,0 +1,16 @@
+package cc.uncarbon.module.sys.mapper;
+
+import cc.uncarbon.module.sys.entity.SysDataDictEntity;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+
+/**
+ * 数据字典
+ *
+ * @author Uncarbon
+ */
+@Mapper
+public interface SysDataDictMapper extends BaseMapper<SysDataDictEntity> {
+
+}

+ 14 - 0
src/main/java/cc/uncarbon/module/sys/mapper/SysDeptMapper.java

@@ -0,0 +1,14 @@
+package cc.uncarbon.module.sys.mapper;
+
+import cc.uncarbon.module.sys.entity.SysDeptEntity;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 部门
+ * @author Uncarbon
+ */
+@Mapper
+public interface SysDeptMapper extends BaseMapper<SysDeptEntity> {
+	
+}

+ 14 - 0
src/main/java/cc/uncarbon/module/sys/mapper/SysLogMapper.java

@@ -0,0 +1,14 @@
+package cc.uncarbon.module.sys.mapper;
+
+import cc.uncarbon.module.sys.entity.SysLogEntity;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 后台操作日志
+ * @author Uncarbon
+ */
+@Mapper
+public interface SysLogMapper extends BaseMapper<SysLogEntity> {
+	
+}

+ 14 - 0
src/main/java/cc/uncarbon/module/sys/mapper/SysMenuMapper.java

@@ -0,0 +1,14 @@
+package cc.uncarbon.module.sys.mapper;
+
+import cc.uncarbon.module.sys.entity.SysMenuEntity;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 后台菜单
+ * @author Uncarbon
+ */
+@Mapper
+public interface SysMenuMapper extends BaseMapper<SysMenuEntity> {
+	
+}

+ 14 - 0
src/main/java/cc/uncarbon/module/sys/mapper/SysParamMapper.java

@@ -0,0 +1,14 @@
+package cc.uncarbon.module.sys.mapper;
+
+import cc.uncarbon.module.sys.entity.SysParamEntity;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 系统参数
+ * @author Uncarbon
+ */
+@Mapper
+public interface SysParamMapper extends BaseMapper<SysParamEntity> {
+	
+}

+ 14 - 0
src/main/java/cc/uncarbon/module/sys/mapper/SysRoleMapper.java

@@ -0,0 +1,14 @@
+package cc.uncarbon.module.sys.mapper;
+
+import cc.uncarbon.module.sys.entity.SysRoleEntity;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 后台角色
+ * @author Uncarbon
+ */
+@Mapper
+public interface SysRoleMapper extends BaseMapper<SysRoleEntity> {
+	
+}

+ 14 - 0
src/main/java/cc/uncarbon/module/sys/mapper/SysRoleMenuRelationMapper.java

@@ -0,0 +1,14 @@
+package cc.uncarbon.module.sys.mapper;
+
+import cc.uncarbon.module.sys.entity.SysRoleMenuRelationEntity;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 后台角色-可见菜单关联
+ * @author Uncarbon
+ */
+@Mapper
+public interface SysRoleMenuRelationMapper extends BaseMapper<SysRoleMenuRelationEntity> {
+	
+}

+ 14 - 0
src/main/java/cc/uncarbon/module/sys/mapper/SysTenantMapper.java

@@ -0,0 +1,14 @@
+package cc.uncarbon.module.sys.mapper;
+
+import cc.uncarbon.module.sys.entity.SysTenantEntity;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 系统租户
+ * @author Uncarbon
+ */
+@Mapper
+public interface SysTenantMapper extends BaseMapper<SysTenantEntity> {
+	
+}

+ 14 - 0
src/main/java/cc/uncarbon/module/sys/mapper/SysUserDeptRelationMapper.java

@@ -0,0 +1,14 @@
+package cc.uncarbon.module.sys.mapper;
+
+import cc.uncarbon.module.sys.entity.SysUserDeptRelationEntity;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 后台用户-部门关联
+ * @author Uncarbon
+ */
+@Mapper
+public interface SysUserDeptRelationMapper extends BaseMapper<SysUserDeptRelationEntity> {
+	
+}

+ 25 - 0
src/main/java/cc/uncarbon/module/sys/mapper/SysUserMapper.java

@@ -0,0 +1,25 @@
+package cc.uncarbon.module.sys.mapper;
+
+import cc.uncarbon.module.sys.entity.SysUserEntity;
+import cc.uncarbon.module.sys.model.response.SysUserBaseInfoBO;
+import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.time.LocalDateTime;
+
+/**
+ * 后台用户
+ *
+ * @author Uncarbon
+ */
+@Mapper
+public interface SysUserMapper extends BaseMapper<SysUserEntity> {
+
+    @InterceptorIgnore(tenantLine = "true")
+    void updateLastLoginAt(Long userId, LocalDateTime lastLoginAt);
+
+    @InterceptorIgnore(tenantLine = "true")
+    SysUserBaseInfoBO getBaseInfoByUserId(Long userId);
+
+}

+ 14 - 0
src/main/java/cc/uncarbon/module/sys/mapper/SysUserRoleRelationMapper.java

@@ -0,0 +1,14 @@
+package cc.uncarbon.module.sys.mapper;
+
+import cc.uncarbon.module.sys.entity.SysUserRoleRelationEntity;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 后台用户-角色关联
+ * @author Uncarbon
+ */
+@Mapper
+public interface SysUserRoleRelationMapper extends BaseMapper<SysUserRoleRelationEntity> {
+	
+}

+ 33 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminBindUserRoleRelationDTO.java

@@ -0,0 +1,33 @@
+package cc.uncarbon.module.sys.model.request;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+import java.util.List;
+
+
+/**
+ * 后台管理-绑定用户与角色关联关系
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminBindUserRoleRelationDTO implements Serializable {
+
+    @ApiModelProperty(value = "用户ID", required = true)
+    @NotNull(message = "用户ID不能为空")
+    private Long userId;
+
+    @ApiModelProperty(value = "角色Ids(空=清理关联关系后不再绑定任何角色)")
+    private List<Long> roleIds;
+
+}

+ 58 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysDataDictDTO.java

@@ -0,0 +1,58 @@
+package cc.uncarbon.module.sys.model.request;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+
+/**
+ * 后台管理-新增/编辑数据字典
+ *
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminInsertOrUpdateSysDataDictDTO implements Serializable {
+
+    @ApiModelProperty(value = "主键ID", hidden = true, notes = "仅更新时使用")
+    private Long id;
+
+    @ApiModelProperty(value = "驼峰式键名", required = true)
+    @NotBlank(message = "驼峰式键名不能为空")
+    private String camelCaseKey;
+
+    @ApiModelProperty(value = "下划线式键名", required = true)
+    @NotBlank(message = "下划线式键名不能为空")
+    private String underCaseKey;
+
+    @ApiModelProperty(value = "帕斯卡式键名", required = true)
+    @NotBlank(message = "帕斯卡式键名不能为空")
+    private String pascalCaseKey;
+
+    @ApiModelProperty(value = "键值", required = true)
+    @NotBlank(message = "键值不能为空")
+    private String value;
+
+    @ApiModelProperty(value = "参数描述", required = true)
+    @NotBlank(message = "参数描述不能为空")
+    private String description;
+
+    @ApiModelProperty(value = "单位")
+    private String unit;
+
+    @ApiModelProperty(value = "取值范围")
+    private String valueRange;
+
+    @ApiModelProperty(value = "别称键名")
+    private String aliasKey;
+
+}

+ 45 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysDeptDTO.java

@@ -0,0 +1,45 @@
+package cc.uncarbon.module.sys.model.request;
+
+import cc.uncarbon.module.sys.enums.GenericStatusEnum;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+
+/**
+ * 后台管理-新增/编辑部门
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminInsertOrUpdateSysDeptDTO implements Serializable {
+
+    @ApiModelProperty(value = "主键ID", hidden = true, notes = "仅更新时使用")
+    private Long id;
+
+    @ApiModelProperty(value = "所属租户ID", hidden = true, notes = "仅新增时使用")
+    private Long tenantId;
+        
+    @ApiModelProperty(value = "名称", required = true)
+    @NotBlank(message = "名称不能为空")
+    private String title;
+        
+    @ApiModelProperty(value = "上级ID(无上级节点设置为0)")
+    private Long parentId;
+        
+    @ApiModelProperty(value = "排序")
+    private Integer sort;
+
+    @ApiModelProperty(value = "状态(0=禁用 1=启用)")
+    private GenericStatusEnum status;
+    
+}

+ 63 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysMenuDTO.java

@@ -0,0 +1,63 @@
+package cc.uncarbon.module.sys.model.request;
+
+import cc.uncarbon.module.sys.enums.GenericStatusEnum;
+import cc.uncarbon.module.sys.enums.SysMenuTypeEnum;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+
+/**
+ * 后台管理-新增/编辑后台菜单
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminInsertOrUpdateSysMenuDTO implements Serializable {
+
+    @ApiModelProperty(value = "主键ID", hidden = true, notes = "仅更新时使用")
+    private Long id;
+
+    @ApiModelProperty(value = "所属租户ID", hidden = true, notes = "仅新增时使用")
+    private Long tenantId;
+
+    @ApiModelProperty(value = "名称", required = true)
+    @NotBlank(message = "名称不能为空")
+    private String title;
+
+    @ApiModelProperty(value = "上级菜单ID(无上级节点设置为0)")
+    private Long parentId;
+
+    @ApiModelProperty(value = "菜单类型", required = true)
+    @NotNull(message = "菜单类型不能为空")
+    private SysMenuTypeEnum type;
+
+    @ApiModelProperty(value = "组件")
+    private String component;
+
+    @ApiModelProperty(value = "权限标识")
+    private String permission;
+
+    @ApiModelProperty(value = "图标")
+    private String icon;
+
+    @ApiModelProperty(value = "排序")
+    private Integer sort;
+
+    @ApiModelProperty(value = "状态(0=禁用 1=启用)")
+    private GenericStatusEnum status;
+
+    @ApiModelProperty(value = "外链地址")
+    private String externalLink;
+
+}

+ 43 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysParamDTO.java

@@ -0,0 +1,43 @@
+package cc.uncarbon.module.sys.model.request;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+
+/**
+ * 后台管理-新增/编辑系统参数
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminInsertOrUpdateSysParamDTO implements Serializable {
+
+    @ApiModelProperty(value = "主键ID", hidden = true, notes = "仅更新时使用")
+    private Long id;
+
+    @ApiModelProperty(value = "所属租户ID", hidden = true, notes = "仅新增时使用")
+    private Long tenantId;
+
+    @ApiModelProperty(value = "键名", required = true)
+    @NotBlank(message = "键名不能为空")
+    private String key;
+
+    @ApiModelProperty(value = "值", required = true)
+    @NotBlank(message = "值不能为空")
+    private String value;
+
+    @ApiModelProperty(value = "描述", required = true)
+    @NotBlank(message = "描述不能为空")
+    private String description;
+
+}

+ 43 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysRoleDTO.java

@@ -0,0 +1,43 @@
+package cc.uncarbon.module.sys.model.request;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotBlank;
+import java.io.Serializable;
+import java.util.List;
+
+
+/**
+ * 后台管理-新增/编辑后台角色
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminInsertOrUpdateSysRoleDTO implements Serializable {
+
+    @ApiModelProperty(value = "主键ID", hidden = true, notes = "仅更新时使用")
+    private Long id;
+
+    @ApiModelProperty(value = "所属租户ID", hidden = true, notes = "仅新增时使用")
+    private Long tenantId;
+        
+    @ApiModelProperty(value = "名称", required = true)
+    @NotBlank(message = "名称不能为空")
+    private String title;
+        
+    @ApiModelProperty(value = "值", required = true)
+    @NotBlank(message = "值不能为空")
+    private String value;
+
+    @ApiModelProperty(value = "菜单Ids(空=清理关联关系后不再绑定任何菜单)")
+    private List<Long> menuIds;
+
+}

+ 68 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysUserDTO.java

@@ -0,0 +1,68 @@
+package cc.uncarbon.module.sys.model.request;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.module.sys.enums.GenderEnum;
+import cc.uncarbon.module.sys.enums.SysUserStatusEnum;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import java.io.Serializable;
+
+
+/**
+ * 后台管理-新增/编辑后台用户
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminInsertOrUpdateSysUserDTO implements Serializable {
+
+    @ApiModelProperty(value = "主键ID", hidden = true, notes = "仅更新时使用")
+    private Long id;
+
+    @ApiModelProperty(value = "所属租户ID", hidden = true, notes = "仅新增时使用")
+    private Long tenantId;
+
+    @ApiModelProperty(value = "账号", required = true)
+    @NotBlank(message = "账号不能为空")
+    private String username;
+
+    @ApiModelProperty(value = "密码字符串(仅注册时有效)")
+    private String passwordOfNewUser;
+
+    @ApiModelProperty(value = "昵称", required = true)
+    @NotBlank(message = "昵称不能为空")
+    private String nickname;
+
+    @ApiModelProperty(value = "状态", dataType = "Integer", required = true)
+    @NotNull(message = "状态不能为空")
+    private SysUserStatusEnum status;
+
+    @ApiModelProperty(value = "性别", dataType = "Integer", required = true)
+    @NotNull(message = "性别不能为空")
+    private GenderEnum gender;
+
+    @ApiModelProperty(value = "邮箱", required = true)
+    @Pattern(message = "邮箱格式有误", regexp = HelioConstant.Regex.EMAIL)
+    @NotBlank(message = "邮箱不能为空")
+    private String email;
+
+    @ApiModelProperty(value = "手机号", required = true)
+    @Pattern(message = "手机号格式有误", regexp = HelioConstant.Regex.CHINA_MAINLAND_PHONE_NO)
+    @NotBlank(message = "手机号不能为空")
+    private String phoneNo;
+
+    @ApiModelProperty(value = "所属部门ID")
+    private Long deptId;
+
+}

+ 44 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertSysTenantDTO.java

@@ -0,0 +1,44 @@
+package cc.uncarbon.module.sys.model.request;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Pattern;
+
+
+/**
+ * 后台管理-新增系统租户
+ * @author Uncarbon
+ */
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminInsertSysTenantDTO extends AdminUpdateSysTenantDTO {
+
+    @ApiModelProperty(value = "管理员账号", required = true)
+    @NotBlank(message = "管理员账号不能为空")
+    private String tenantAdminUsername;
+
+    @ApiModelProperty(value = "管理员密码", required = true)
+    @NotBlank(message = "管理员密码不能为空")
+    private String tenantAdminPassword;
+
+    @ApiModelProperty(value = "管理员邮箱", required = true)
+    @Pattern(message = "邮箱格式不正确", regexp = HelioConstant.Regex.EMAIL)
+    @NotBlank(message = "管理员邮箱不能为空")
+    private String tenantAdminEmail;
+
+    @ApiModelProperty(value = "管理员手机号", required = true)
+    @Pattern(message = "手机号格式不正确", regexp = HelioConstant.Regex.CHINA_MAINLAND_PHONE_NO)
+    @NotBlank(message = "管理员手机号不能为空")
+    private String tenantAdminPhoneNo;
+
+}

+ 29 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminKickOutSysUserDTO.java

@@ -0,0 +1,29 @@
+package cc.uncarbon.module.sys.model.request;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+
+/**
+ * 后台管理-踢某用户下线
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminKickOutSysUserDTO implements Serializable {
+
+    @ApiModelProperty(value = "用户ID", required = true)
+    @NotNull(message = "用户ID不能为空")
+    private Long userId;
+
+}

+ 28 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysDataDictDTO.java

@@ -0,0 +1,28 @@
+package cc.uncarbon.module.sys.model.request;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+
+/**
+ * 后台管理-分页列表数据字典
+ *
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminListSysDataDictDTO implements Serializable {
+
+    @ApiModelProperty(value = "参数描述(关键词)")
+    private String description;
+
+}

+ 30 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysDeptDTO.java

@@ -0,0 +1,30 @@
+package cc.uncarbon.module.sys.model.request;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+
+/**
+ * 后台管理-分页列表部门
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminListSysDeptDTO implements Serializable {
+
+    @ApiModelProperty(value = "名称(关键词)")
+    private String title;
+
+    @ApiModelProperty(value = "上级ID(无上级节点设置为0)")
+    private Long parentId;
+
+}

+ 48 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysLogDTO.java

@@ -0,0 +1,48 @@
+package cc.uncarbon.module.sys.model.request;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.module.sys.enums.SysLogStatusEnum;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+
+/**
+ * 后台管理-分页列表后台操作日志
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminListSysLogDTO implements Serializable {
+
+    @ApiModelProperty(value = "用户账号")
+    private String username;
+
+    @ApiModelProperty(value = "操作内容")
+    private String operation;
+
+    @ApiModelProperty(value = "状态")
+    private SysLogStatusEnum status;
+
+    @ApiModelProperty(value = "开始时间区间")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    private LocalDateTime beginAt;
+
+    @ApiModelProperty(value = "结束时间区间")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    private LocalDateTime endAt;
+
+}

+ 35 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysMenuDTO.java

@@ -0,0 +1,35 @@
+package cc.uncarbon.module.sys.model.request;
+
+import cc.uncarbon.module.sys.enums.SysMenuTypeEnum;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.List;
+
+
+/**
+ * 后台管理-列表后台菜单
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminListSysMenuDTO implements Serializable {
+
+    @ApiModelProperty(value = "无上级节点名称(关键词)")
+    private String title;
+
+    @ApiModelProperty(value = "上级ID(无上级节点设置为0)")
+    private Long parentId;
+
+    @ApiModelProperty(value = "【内部使用】菜单类型", hidden = true)
+    private List<SysMenuTypeEnum> menuTypes;
+
+}

+ 30 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysParamDTO.java

@@ -0,0 +1,30 @@
+package cc.uncarbon.module.sys.model.request;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+
+/**
+ * 后台管理-分页列表系统参数
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminListSysParamDTO implements Serializable {
+
+    @ApiModelProperty(value = "英文键名")
+    private String key;
+
+    @ApiModelProperty(value = "描述")
+    private String description;
+
+}

+ 44 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysRoleDTO.java

@@ -0,0 +1,44 @@
+package cc.uncarbon.module.sys.model.request;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+
+/**
+ * 后台管理-分页列表后台角色
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminListSysRoleDTO implements Serializable {
+
+    @ApiModelProperty(value = "名称(关键词)")
+    private String title;
+
+    @ApiModelProperty(value = "值(关键词)")
+    private String value;
+
+    @ApiModelProperty(value = "开始时间区间")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    private LocalDateTime beginAt;
+
+    @ApiModelProperty(value = "结束时间区间")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    private LocalDateTime endAt;
+
+}

+ 48 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysTenantDTO.java

@@ -0,0 +1,48 @@
+package cc.uncarbon.module.sys.model.request;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.module.sys.enums.GenericStatusEnum;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+
+/**
+ * 后台管理-分页列表系统租户
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminListSysTenantDTO implements Serializable {
+
+    @ApiModelProperty(value = "租户名(关键词)")
+    private String tenantName;
+
+    @ApiModelProperty(value = "租户ID(纯数字)")
+    private Long tenantId;
+
+    @ApiModelProperty(value = "状态")
+    private GenericStatusEnum status;
+
+    @ApiModelProperty(value = "开始时间区间")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    private LocalDateTime beginAt;
+
+    @ApiModelProperty(value = "结束时间区间")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    private LocalDateTime endAt;
+
+}

+ 41 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysUserDTO.java

@@ -0,0 +1,41 @@
+package cc.uncarbon.module.sys.model.request;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+
+/**
+ * 后台管理-分页列表后台用户
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminListSysUserDTO implements Serializable {
+
+    @ApiModelProperty(value = "手机号(关键词)")
+    private String phoneNo;
+
+    @ApiModelProperty(value = "开始时间区间")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    private LocalDateTime beginAt;
+
+    @ApiModelProperty(value = "结束时间区间")
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    private LocalDateTime endAt;
+
+}

+ 36 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminResetSysUserPasswordDTO.java

@@ -0,0 +1,36 @@
+package cc.uncarbon.module.sys.model.request;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.io.Serializable;
+
+
+/**
+ * 后台管理-重置某用户密码
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminResetSysUserPasswordDTO implements Serializable {
+
+    @ApiModelProperty(value = "用户ID", required = true)
+    @NotNull(message = "用户ID不能为空")
+    private Long userId;
+
+    @ApiModelProperty(value = "随机新密码", required = true)
+    @Size(min = 16, max = 64, message = "随机新密码须为16-64位")
+    @NotBlank(message = "随机新密码不能为空")
+    private String randomPassword;
+
+}

+ 40 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminUpdateCurrentSysUserPasswordDTO.java

@@ -0,0 +1,40 @@
+package cc.uncarbon.module.sys.model.request;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+import java.io.Serializable;
+
+
+/**
+ * 后台管理-修改当前用户密码
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminUpdateCurrentSysUserPasswordDTO implements Serializable {
+
+    @ApiModelProperty(value = "原密码", required = true)
+    @NotBlank(message = "原密码不能为空")
+    private String oldPassword;
+
+    @ApiModelProperty(value = "新密码", required = true)
+    @Size(min = 8, max = 20, message = "密码须为8-20位")
+    @NotBlank(message = "密码不能为空")
+    private String newPassword;
+
+    @ApiModelProperty(value = "确认新密码", required = true)
+    @Size(min = 8, max = 20, message = "确认密码须为8-20位")
+    @NotBlank(message = "确认密码不能为空")
+    private String confirmNewPassword;
+
+}

+ 45 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminUpdateSysTenantDTO.java

@@ -0,0 +1,45 @@
+package cc.uncarbon.module.sys.model.request;
+
+import cc.uncarbon.module.sys.enums.GenericStatusEnum;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.io.Serializable;
+
+
+/**
+ * 后台管理-编辑系统租户
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class AdminUpdateSysTenantDTO implements Serializable {
+
+    @ApiModelProperty(value = "主键ID", hidden = true, notes = "仅更新时使用")
+    private Long id;
+
+    @ApiModelProperty(value = "租户名", required = true)
+    @NotBlank(message = "租户名不能为空")
+    private String tenantName;
+
+    @ApiModelProperty(value = "租户ID(纯数字)", required = true)
+    @NotNull(message = "租户ID不能为空")
+    private Long tenantId;
+
+    @ApiModelProperty(value = "状态", required = true)
+    @NotNull(message = "状态不能为空")
+    private GenericStatusEnum status;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+
+}

+ 41 - 0
src/main/java/cc/uncarbon/module/sys/model/request/SysUserLoginDTO.java

@@ -0,0 +1,41 @@
+package cc.uncarbon.module.sys.model.request;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.io.Serializable;
+
+
+/**
+ * 后台管理-后台用户登录
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class SysUserLoginDTO implements Serializable {
+
+    @ApiModelProperty(value = "账号", required = true)
+    @Size(min = 5, max = 16, message = "账号须为5-16位")
+    @NotBlank(message = "账号不能为空")
+    private String username;
+
+    @ApiModelProperty(value = "密码", required = true)
+    @Size(min = 5, max = 64, message = "密码须为5-64位")
+    @NotBlank(message = "密码不能为空")
+    private String password;
+
+    @ApiModelProperty(value = "记住我", required = true)
+    @NotNull(message = "记住我不能为空")
+    private Boolean rememberMe;
+
+}

+ 68 - 0
src/main/java/cc/uncarbon/module/sys/model/response/SysDataDictBO.java

@@ -0,0 +1,68 @@
+package cc.uncarbon.module.sys.model.response;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+
+/**
+ * 数据字典BO
+ *
+ * @author Uncarbon
+ */
+@ApiModel(value = "数据字典")
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class SysDataDictBO implements Serializable {
+
+    @ApiModelProperty(value = "主键ID")
+    private Long id;
+
+    @ApiModelProperty(value = "创建时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    private LocalDateTime createdAt;
+
+    @ApiModelProperty(value = "更新时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    private LocalDateTime updatedAt;
+
+    @ApiModelProperty(value = "驼峰式键名")
+    private String camelCaseKey;
+
+    @ApiModelProperty(value = "下划线式键名")
+    private String underCaseKey;
+
+    @ApiModelProperty(value = "帕斯卡式键名")
+    private String pascalCaseKey;
+
+    @ApiModelProperty(value = "键值")
+    private String value;
+
+    @ApiModelProperty(value = "参数描述")
+    private String description;
+
+    @ApiModelProperty(value = "单位")
+    private String unit;
+
+    @ApiModelProperty(value = "取值范围")
+    private String valueRange;
+
+    @ApiModelProperty(value = "别称键名")
+    private String aliasKey;
+
+}

+ 60 - 0
src/main/java/cc/uncarbon/module/sys/model/response/SysDeptBO.java

@@ -0,0 +1,60 @@
+package cc.uncarbon.module.sys.model.response;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.module.sys.enums.GenericStatusEnum;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.List;
+
+
+/**
+ * 部门BO
+ * @author Uncarbon
+ */
+@ApiModel(value = "部门")
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class SysDeptBO implements Serializable {
+
+    @ApiModelProperty(value = "主键ID")
+    private Long id;
+
+    @ApiModelProperty(value = "创建时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    private LocalDateTime createdAt;
+
+    @ApiModelProperty(value = "更新时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    private LocalDateTime updatedAt;
+
+    @ApiModelProperty(value = "名称")
+    private String title;
+
+    @ApiModelProperty(value = "上级ID(无上级节点设置为0)")
+    private Long parentId;
+
+    @ApiModelProperty(value = "排序")
+    private Integer sort;
+
+    @ApiModelProperty(value = "状态(0=禁用 1=启用)")
+    private GenericStatusEnum status;
+
+    @ApiModelProperty(value = "子项数据")
+    private List<SysDeptBO> children;
+
+}

+ 48 - 0
src/main/java/cc/uncarbon/module/sys/model/response/SysLogBO.java

@@ -0,0 +1,48 @@
+package cc.uncarbon.module.sys.model.response;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.module.sys.enums.SysLogStatusEnum;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+
+/**
+ * 后台操作日志BO
+ * @author Uncarbon
+ */
+@ApiModel(value = "后台操作日志")
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class SysLogBO implements Serializable {
+
+    @ApiModelProperty(value = "创建时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    private LocalDateTime createdAt;
+
+    @ApiModelProperty(value = "用户账号")
+    private String username;
+
+    @ApiModelProperty(value = "操作内容")
+    private String operation;
+
+    @ApiModelProperty(value = "IP地址")
+    private String ip;
+
+    @ApiModelProperty(value = "状态(0=未执行 1=成功)")
+    private SysLogStatusEnum status;
+
+}

+ 88 - 0
src/main/java/cc/uncarbon/module/sys/model/response/SysMenuBO.java

@@ -0,0 +1,88 @@
+package cc.uncarbon.module.sys.model.response;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.module.sys.enums.GenericStatusEnum;
+import cc.uncarbon.module.sys.enums.SysMenuTypeEnum;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.List;
+
+
+/**
+ * 后台菜单BO
+ * @author Uncarbon
+ */
+@ApiModel(value = "后台菜单")
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class SysMenuBO implements Serializable {
+
+    @ApiModelProperty(value = "主键ID")
+    private Long id;
+
+    @ApiModelProperty(value = "创建时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    private LocalDateTime createdAt;
+
+    @ApiModelProperty(value = "更新时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    private LocalDateTime updatedAt;
+
+    @ApiModelProperty(value = "名称")
+    private String title;
+
+    @ApiModelProperty(value = "上级菜单ID")
+    private Long parentId;
+
+    @ApiModelProperty(value = "菜单类型(参考MenuTypeEnum)")
+    private SysMenuTypeEnum type;
+
+    @ApiModelProperty(value = "权限标识")
+    private String permission;
+
+    @ApiModelProperty(value = "图标")
+    private String icon;
+
+    @ApiModelProperty(value = "排序")
+    private Integer sort;
+
+    @ApiModelProperty(value = "状态(0=禁用 1=启用)")
+    private GenericStatusEnum status;
+
+    @ApiModelProperty(value = "组件")
+    private String component;
+
+    @ApiModelProperty(value = "外链地址")
+    private String externalLink;
+
+    @ApiModelProperty(value = "子项数据")
+    private List<SysMenuBO> children;
+
+    @ApiModelProperty(value = "【用于Vben Admin】路由地址", hidden = true)
+    private String path;
+
+    @ApiModelProperty(value = "【用于Vben Admin】菜单名(全局唯一, 不能重复)", hidden = true)
+    private String name;
+
+    @ApiModelProperty(value = "【用于Vben Admin】菜单详情", hidden = true)
+    private VbenAdminMenuMetaBO meta;
+
+    @ApiModelProperty(value = "【用于Vben Admin树状菜单】菜单主键ID字符串", hidden = true)
+    private String idStr;
+
+}

+ 52 - 0
src/main/java/cc/uncarbon/module/sys/model/response/SysParamBO.java

@@ -0,0 +1,52 @@
+package cc.uncarbon.module.sys.model.response;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+
+/**
+ * 系统参数BO
+ * @author Uncarbon
+ */
+@ApiModel(value = "系统参数")
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class SysParamBO implements Serializable {
+
+    @ApiModelProperty(value = "主键ID")
+    private Long id;
+
+    @ApiModelProperty(value = "创建时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    private LocalDateTime createdAt;
+
+    @ApiModelProperty(value = "更新时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    private LocalDateTime updatedAt;
+
+    @ApiModelProperty(value = "键名")
+    private String key;
+
+    @ApiModelProperty(value = "键值")
+    private String value;
+
+    @ApiModelProperty(value = "描述")
+    private String description;
+
+}

+ 53 - 0
src/main/java/cc/uncarbon/module/sys/model/response/SysRoleBO.java

@@ -0,0 +1,53 @@
+package cc.uncarbon.module.sys.model.response;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.List;
+
+
+/**
+ * 后台角色BO
+ * @author Uncarbon
+ */
+@ApiModel(value = "后台角色")
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class SysRoleBO implements Serializable {
+
+    @ApiModelProperty(value = "主键ID")
+    private Long id;
+
+    @ApiModelProperty(value = "创建时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    private LocalDateTime createdAt;
+
+    @ApiModelProperty(value = "更新时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    private LocalDateTime updatedAt;
+        
+    @ApiModelProperty(value = "名称")
+    private String title;
+        
+    @ApiModelProperty(value = "值")
+    private String value;
+
+    @ApiModelProperty(value = "可见菜单Ids")
+    private List<Long> menuIds;
+    
+}

+ 59 - 0
src/main/java/cc/uncarbon/module/sys/model/response/SysTenantBO.java

@@ -0,0 +1,59 @@
+package cc.uncarbon.module.sys.model.response;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.module.sys.enums.GenericStatusEnum;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+
+/**
+ * 系统租户BO
+ * @author Uncarbon
+ */
+@ApiModel(value = "系统租户")
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class SysTenantBO implements Serializable {
+
+    @ApiModelProperty(value = "主键ID")
+    private Long id;
+
+    @ApiModelProperty(value = "创建时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    private LocalDateTime createdAt;
+
+    @ApiModelProperty(value = "更新时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    private LocalDateTime updatedAt;
+
+    @ApiModelProperty(value = "备注")
+    private String remark;
+
+    @ApiModelProperty(value = "租户名")
+    private String tenantName;
+
+    @ApiModelProperty(value = "租户ID")
+    private Long tenantId;
+
+    @ApiModelProperty(value = "状态")
+    private GenericStatusEnum status;
+
+    @ApiModelProperty(value = "租户管理员用户基本信息")
+    private SysUserBaseInfoBO tenantAdminUser;
+
+}

+ 82 - 0
src/main/java/cc/uncarbon/module/sys/model/response/SysUserBO.java

@@ -0,0 +1,82 @@
+package cc.uncarbon.module.sys.model.response;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.module.sys.enums.GenderEnum;
+import cc.uncarbon.module.sys.enums.SysUserStatusEnum;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.List;
+
+
+/**
+ * 后台用户BO
+ * @author Uncarbon
+ */
+@ApiModel(value = "后台用户")
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class SysUserBO implements Serializable {
+
+    @ApiModelProperty(value = "主键ID")
+    private Long id;
+
+    @ApiModelProperty(value = "创建时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    private LocalDateTime createdAt;
+
+    @ApiModelProperty(value = "更新时刻")
+    @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    private LocalDateTime updatedAt;
+
+    @ApiModelProperty(value = "账号")
+    private String username;
+
+    @ApiModelProperty(value = "昵称")
+    private String nickname;
+
+    @ApiModelProperty(value = "状态")
+    private SysUserStatusEnum status;
+
+    @ApiModelProperty(value = "性别")
+    private GenderEnum gender;
+
+    @ApiModelProperty(value = "邮箱")
+    private String email;
+
+    @ApiModelProperty(value = "手机号")
+    private String phoneNo;
+
+    @ApiModelProperty(value = "最后登录时刻")
+    private LocalDateTime lastLoginAt;
+
+    @ApiModelProperty(value = "拥有角色")
+    private List<SysRoleBO> roles;
+
+    @ApiModelProperty(value = "拥有角色Ids")
+    private List<Long> roleIds;
+
+    @ApiModelProperty(value = "拥有权限")
+    private List<String> permissions;
+
+    @ApiModelProperty(value = "所属部门ID")
+    private Long deptId;
+
+    @ApiModelProperty(value = "所属部门名称")
+    private String deptTitle;
+
+}

+ 47 - 0
src/main/java/cc/uncarbon/module/sys/model/response/SysUserBaseInfoBO.java

@@ -0,0 +1,47 @@
+package cc.uncarbon.module.sys.model.response;
+
+import cc.uncarbon.module.sys.enums.GenderEnum;
+import cc.uncarbon.module.sys.enums.SysUserStatusEnum;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+
+/**
+ * 后台用户BO
+ * 仅保留基本信息
+ * @author Uncarbon
+ */
+@ApiModel(value = "后台用户基本信息")
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class SysUserBaseInfoBO implements Serializable {
+
+    @ApiModelProperty(value = "账号")
+    private String username;
+
+    @ApiModelProperty(value = "昵称")
+    private String nickname;
+
+    @ApiModelProperty(value = "状态")
+    private SysUserStatusEnum status;
+
+    @ApiModelProperty(value = "性别")
+    private GenderEnum gender;
+
+    @ApiModelProperty(value = "邮箱")
+    private String email;
+
+    @ApiModelProperty(value = "手机号")
+    private String phoneNo;
+
+}

+ 48 - 0
src/main/java/cc/uncarbon/module/sys/model/response/SysUserLoginBO.java

@@ -0,0 +1,48 @@
+package cc.uncarbon.module.sys.model.response;
+
+import cc.uncarbon.framework.core.context.TenantContext;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+import java.util.List;
+
+
+/**
+ * 后台用户BO
+ * 用于登录后返回字段
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class SysUserLoginBO implements Serializable {
+
+    @ApiModelProperty(value = "用户ID")
+    private Long id;
+
+    @ApiModelProperty(value = "账号")
+    private String username;
+
+    @ApiModelProperty(value = "昵称")
+    private String nickname;
+
+    @ApiModelProperty(value = "手机号")
+    private String phoneNo;
+
+    @ApiModelProperty(value = "拥有角色")
+    private List<String> roles;
+
+    @ApiModelProperty(value = "拥有权限")
+    private List<String> permissions;
+
+    @ApiModelProperty("所属租户")
+    private TenantContext relationalTenant;
+
+}

+ 35 - 0
src/main/java/cc/uncarbon/module/sys/model/response/VbenAdminMenuMetaBO.java

@@ -0,0 +1,35 @@
+package cc.uncarbon.module.sys.model.response;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+
+/**
+ * 后台菜单子项详情 for VbenAdmin
+ * @author Uncarbon
+ */
+@ApiModel(value = "后台菜单子项详情")
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class VbenAdminMenuMetaBO implements Serializable {
+
+    @ApiModelProperty(value = "标题")
+    private String title;
+
+    @ApiModelProperty(value = "是否不可关闭")
+    private Boolean affix = false;
+
+    @ApiModelProperty(value = "图标")
+    private String icon;
+
+}

+ 32 - 0
src/main/java/cc/uncarbon/module/sys/model/response/VbenAdminUserInfoBO.java

@@ -0,0 +1,32 @@
+package cc.uncarbon.module.sys.model.response;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+import java.io.Serializable;
+
+
+/**
+ * 后台用户 for VbenAdmin
+ * @author Uncarbon
+ */
+@ApiModel(value = "后台用户")
+@Accessors(chain = true)
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class VbenAdminUserInfoBO implements Serializable {
+
+    @ApiModelProperty(value = "账号")
+    private String username;
+
+    @ApiModelProperty(value = "昵称")
+    private String nickname;
+
+}

+ 162 - 0
src/main/java/cc/uncarbon/module/sys/service/SysDataDictService.java

@@ -0,0 +1,162 @@
+package cc.uncarbon.module.sys.service;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.exception.BusinessException;
+import cc.uncarbon.framework.core.page.PageParam;
+import cc.uncarbon.framework.core.page.PageResult;
+import cc.uncarbon.framework.crud.service.impl.HelioBaseServiceImpl;
+import cc.uncarbon.module.sys.annotation.SysLog;
+import cc.uncarbon.module.sys.entity.SysDataDictEntity;
+import cc.uncarbon.module.sys.enums.SysErrorEnum;
+import cc.uncarbon.module.sys.mapper.SysDataDictMapper;
+import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysDataDictDTO;
+import cc.uncarbon.module.sys.model.request.AdminListSysDataDictDTO;
+import cc.uncarbon.module.sys.model.response.SysDataDictBO;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * 数据字典
+ *
+ * @author Uncarbon
+ */
+@Slf4j
+@Service
+public class SysDataDictService extends HelioBaseServiceImpl<SysDataDictMapper, SysDataDictEntity> {
+
+    /**
+     * 后台管理-分页列表
+     */
+    public PageResult<SysDataDictBO> adminList(PageParam pageParam, AdminListSysDataDictDTO dto) {
+        Page<SysDataDictEntity> entityPage = this.page(
+                new Page<>(pageParam.getPageNum(), pageParam.getPageSize()),
+                new QueryWrapper<SysDataDictEntity>()
+                        .lambda()
+                        // 参数描述
+                        .like(StrUtil.isNotBlank(dto.getDescription()), SysDataDictEntity::getDescription, StrUtil.cleanBlank(dto.getDescription()))
+                        // 排序
+                        .orderByDesc(SysDataDictEntity::getCreatedAt)
+        );
+
+        return this.entityPage2BOPage(entityPage);
+    }
+
+    /**
+     * 通用-详情
+     */
+    public SysDataDictBO getOneById(Long entityId) {
+        SysDataDictEntity entity = this.getById(entityId);
+        SysErrorEnum.INVALID_ID.assertNotNull(entity);
+
+        return this.entity2BO(entity);
+    }
+
+    /**
+     * 后台管理-新增
+     */
+    @SysLog(value = "新增数据字典")
+    @Transactional(rollbackFor = Exception.class)
+    public Long adminInsert(AdminInsertOrUpdateSysDataDictDTO dto) {
+        this.checkExist(dto);
+
+        dto.setId(null);
+        SysDataDictEntity entity = new SysDataDictEntity();
+        BeanUtil.copyProperties(dto, entity);
+
+        this.save(entity);
+
+        return entity.getId();
+    }
+
+    /**
+     * 后台管理-编辑
+     */
+    @SysLog(value = "编辑数据字典")
+    @Transactional(rollbackFor = Exception.class)
+    public void adminUpdate(AdminInsertOrUpdateSysDataDictDTO dto) {
+        this.checkExist(dto);
+
+        SysDataDictEntity entity = new SysDataDictEntity();
+        BeanUtil.copyProperties(dto, entity);
+
+        this.updateById(entity);
+    }
+
+    /**
+     * 后台管理-删除
+     */
+    @SysLog(value = "删除数据字典")
+    @Transactional(rollbackFor = Exception.class)
+    public void adminDelete(List<Long> ids) {
+        this.removeByIds(ids);
+    }
+
+
+    /*
+    私有方法
+    ------------------------------------------------------------------------------------------------
+     */
+
+    private SysDataDictBO entity2BO(SysDataDictEntity entity) {
+        if (entity == null) {
+            return null;
+        }
+
+        SysDataDictBO bo = new SysDataDictBO();
+        BeanUtil.copyProperties(entity, bo);
+
+        // 可以在此处为BO填充字段
+
+        return bo;
+    }
+
+    private List<SysDataDictBO> entityList2BOs(List<SysDataDictEntity> entityList) {
+        // 深拷贝
+        List<SysDataDictBO> ret = new ArrayList<>(entityList.size());
+        entityList.forEach(
+                entity -> ret.add(this.entity2BO(entity))
+        );
+
+        return ret;
+    }
+
+    private PageResult<SysDataDictBO> entityPage2BOPage(Page<SysDataDictEntity> entityPage) {
+        PageResult<SysDataDictBO> ret = new PageResult<>();
+        BeanUtil.copyProperties(entityPage, ret);
+        ret.setRecords(this.entityList2BOs(entityPage.getRecords()));
+
+        return ret;
+    }
+
+    /**
+     * 检查是否已存在同名数据
+     *
+     * @param dto DTO
+     */
+    private void checkExist(AdminInsertOrUpdateSysDataDictDTO dto) {
+        SysDataDictEntity existEntity = this.getOne(
+                new QueryWrapper<SysDataDictEntity>()
+                        .lambda()
+                        .eq(SysDataDictEntity::getCamelCaseKey, dto.getCamelCaseKey())
+                        .or()
+                        .eq(SysDataDictEntity::getPascalCaseKey, dto.getPascalCaseKey())
+                        .or()
+                        .eq(SysDataDictEntity::getUnderCaseKey, dto.getUnderCaseKey())
+                        .last(HelioConstant.CRUD.SQL_LIMIT_1)
+        );
+
+        if (existEntity != null && !existEntity.getId().equals(dto.getId())) {
+            throw new BusinessException(400, "已存在相同数据字典,请重新输入");
+        }
+    }
+
+}

+ 199 - 0
src/main/java/cc/uncarbon/module/sys/service/SysDeptService.java

@@ -0,0 +1,199 @@
+package cc.uncarbon.module.sys.service;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.exception.BusinessException;
+import cc.uncarbon.framework.crud.service.impl.HelioBaseServiceImpl;
+import cc.uncarbon.module.sys.annotation.SysLog;
+import cc.uncarbon.module.sys.constant.SysConstant;
+import cc.uncarbon.module.sys.entity.SysDeptEntity;
+import cc.uncarbon.module.sys.entity.SysUserDeptRelationEntity;
+import cc.uncarbon.module.sys.enums.SysErrorEnum;
+import cc.uncarbon.module.sys.mapper.SysDeptMapper;
+import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysDeptDTO;
+import cc.uncarbon.module.sys.model.request.AdminListSysDeptDTO;
+import cc.uncarbon.module.sys.model.response.SysDeptBO;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * 部门
+ * @author Uncarbon
+ */
+@Slf4j
+@Service
+public class SysDeptService extends HelioBaseServiceImpl<SysDeptMapper, SysDeptEntity> {
+
+    @Resource
+    private SysUserDeptRelationService sysUserDeptRelationService;
+
+
+    /**
+     * 后台管理-列表
+     */
+    public List<SysDeptBO> adminList(AdminListSysDeptDTO dto) {
+        List<SysDeptEntity> entityList = this.list(
+                new QueryWrapper<SysDeptEntity>()
+                        .lambda()
+                        // 名称
+                        .like(StrUtil.isNotBlank(dto.getTitle()), SysDeptEntity::getTitle, StrUtil.cleanBlank(dto.getTitle()))
+                        // 上级ID
+                        .eq(ObjectUtil.isNotNull(dto.getParentId()), SysDeptEntity::getParentId, dto.getParentId())
+                        // 排序
+                        .orderByAsc(SysDeptEntity::getSort)
+        );
+
+        return this.entityList2BOs(entityList);
+    }
+
+    /**
+     * 通用-详情
+     */
+    public SysDeptBO getOneById(Long entityId) {
+        SysDeptEntity entity = this.getById(entityId);
+        SysErrorEnum.INVALID_ID.assertNotNull(entity);
+
+        return this.entity2BO(entity, false);
+    }
+
+    /**
+     * 后台管理-新增
+     */
+    @SysLog(value = "新增部门")
+    @Transactional(rollbackFor = Exception.class)
+    public Long adminInsert(AdminInsertOrUpdateSysDeptDTO dto) {
+        this.checkExist(dto);
+
+        if (ObjectUtil.isNull(dto.getParentId())) {
+            dto.setParentId(0L);
+        }
+
+        dto.setId(null);
+        SysDeptEntity entity = new SysDeptEntity();
+        BeanUtil.copyProperties(dto, entity);
+
+        this.save(entity);
+
+        return entity.getId();
+    }
+
+    /**
+     * 后台管理-编辑
+     */
+    @SysLog(value = "编辑部门")
+    @Transactional(rollbackFor = Exception.class)
+    public void adminUpdate(AdminInsertOrUpdateSysDeptDTO dto) {
+        this.checkExist(dto);
+
+        if (ObjectUtil.isNull(dto.getParentId())) {
+            dto.setParentId(0L);
+        }
+
+        SysDeptEntity entity = new SysDeptEntity();
+        BeanUtil.copyProperties(dto, entity);
+
+        this.updateById(entity);
+    }
+
+    /**
+     * 后台管理-删除
+     */
+    @SysLog(value = "删除部门")
+    @Transactional(rollbackFor = Exception.class)
+    public void adminDelete(List<Long> ids) {
+        this.removeByIds(ids);
+    }
+
+    /**
+     * 取所属部门简易信息
+     * @param userId 用户ID
+     */
+    public SysDeptBO getPlainDeptByUserId(Long userId) {
+        SysUserDeptRelationEntity relationEntity = sysUserDeptRelationService.getOne(
+                new QueryWrapper<SysUserDeptRelationEntity>()
+                        .lambda()
+                        .eq(SysUserDeptRelationEntity::getUserId, userId)
+                        .last(HelioConstant.CRUD.SQL_LIMIT_1)
+        );
+
+        if (relationEntity == null) {
+            return null;
+        }
+
+        SysDeptEntity entity = this.getById(relationEntity.getDeptId());
+        return this.entity2BO(entity, false);
+    }
+
+    
+    /*
+    私有方法
+    ------------------------------------------------------------------------------------------------
+     */
+
+    private SysDeptBO entity2BO(SysDeptEntity entity, boolean traverseChildren) {
+        if (entity == null) {
+            return null;
+        }
+
+        SysDeptBO bo = new SysDeptBO();
+        BeanUtil.copyProperties(entity, bo);
+
+        // 可以在此处为BO填充字段
+        if (SysConstant.ROOT_PARENT_ID.equals(bo.getParentId())) {
+            bo.setParentId(null);
+        }
+
+        if (traverseChildren) {
+            List<SysDeptBO> children = this.adminList(
+                    AdminListSysDeptDTO.builder()
+                            .parentId(bo.getId())
+                            .build()
+            );
+            if (CollUtil.isEmpty(children)) {
+                children = null;
+            }
+
+            bo.setChildren(children);
+        }
+
+        return bo;
+    }
+
+    private List<SysDeptBO> entityList2BOs(List<SysDeptEntity> entityList) {
+        // 深拷贝
+        List<SysDeptBO> ret = new ArrayList<>(entityList.size());
+        entityList.forEach(
+                entity -> ret.add(this.entity2BO(entity, true))
+        );
+
+        return ret;
+    }
+
+
+    /**
+     * 检查是否已存在同名数据
+     * @param dto DTO
+     */
+    private void checkExist(AdminInsertOrUpdateSysDeptDTO dto) {
+        SysDeptEntity existEntity = this.getOne(
+                new QueryWrapper<SysDeptEntity>()
+                        .lambda()
+                        .eq(SysDeptEntity::getTitle, dto.getTitle())
+                        .last(HelioConstant.CRUD.SQL_LIMIT_1)
+        );
+
+        if (existEntity != null && !existEntity.getId().equals(dto.getId())) {
+            throw new BusinessException(400, "已存在相同部门,请重新输入");
+        }
+    }
+}

+ 103 - 0
src/main/java/cc/uncarbon/module/sys/service/SysLogService.java

@@ -0,0 +1,103 @@
+package cc.uncarbon.module.sys.service;
+
+import cc.uncarbon.framework.core.page.PageParam;
+import cc.uncarbon.framework.core.page.PageResult;
+import cc.uncarbon.framework.crud.service.impl.HelioBaseServiceImpl;
+import cc.uncarbon.module.sys.entity.SysLogEntity;
+import cc.uncarbon.module.sys.enums.SysErrorEnum;
+import cc.uncarbon.module.sys.mapper.SysLogMapper;
+import cc.uncarbon.module.sys.model.request.AdminListSysLogDTO;
+import cc.uncarbon.module.sys.model.response.SysLogBO;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * 后台操作日志
+ * @author Uncarbon
+ */
+@Slf4j
+@Service
+public class SysLogService extends HelioBaseServiceImpl<SysLogMapper, SysLogEntity> {
+
+    /**
+     * 后台管理-分页列表
+     */
+    public PageResult<SysLogBO> adminList(PageParam pageParam, AdminListSysLogDTO dto) {
+        Page<SysLogEntity> entityPage = this.page(
+                new Page<>(pageParam.getPageNum(), pageParam.getPageSize()),
+                new QueryWrapper<SysLogEntity>()
+                        .lambda()
+                        // 用户账号
+                        .like(StrUtil.isNotBlank(dto.getUsername()), SysLogEntity::getUsername, StrUtil.cleanBlank(dto.getUsername()))
+                        // 操作内容
+                        .like(StrUtil.isNotBlank(dto.getOperation()), SysLogEntity::getOperation, StrUtil.cleanBlank(dto.getOperation()))
+                        // 状态
+                        .eq(ObjectUtil.isNotNull(dto.getStatus()), SysLogEntity::getStatus, dto.getStatus())
+                        // 时间区间
+                        .between(ObjectUtil.isNotNull(dto.getBeginAt()) && ObjectUtil.isNotNull(dto.getEndAt()), SysLogEntity::getCreatedAt, dto.getBeginAt(), dto.getEndAt())
+                        // 排序
+                        .orderByDesc(SysLogEntity::getCreatedAt)
+        );
+
+        return this.entityPage2BOPage(entityPage);
+    }
+
+    /**
+     * 通用-详情
+     */
+    public SysLogBO getOneById(Long entityId) {
+        SysLogEntity entity = this.getById(entityId);
+        SysErrorEnum.INVALID_ID.assertNotNull(entity);
+
+        return this.entity2BO(entity);
+    }
+
+
+
+
+    /*
+    私有方法
+    ------------------------------------------------------------------------------------------------
+     */
+
+    private SysLogBO entity2BO(SysLogEntity entity) {
+        if (entity == null) {
+            return null;
+        }
+
+        SysLogBO bo = new SysLogBO();
+        BeanUtil.copyProperties(entity, bo);
+
+        // 可以在此处为BO填充字段
+
+        return bo;
+    }
+
+    private List<SysLogBO> entityList2BOs(List<SysLogEntity> entityList) {
+        // 深拷贝
+        List<SysLogBO> ret = new ArrayList<>(entityList.size());
+        entityList.forEach(
+                entity -> ret.add(this.entity2BO(entity))
+        );
+
+        return ret;
+    }
+
+    private PageResult<SysLogBO> entityPage2BOPage(Page<SysLogEntity> entityPage) {
+        PageResult<SysLogBO> ret = new PageResult<>();
+        BeanUtil.copyProperties(entityPage, ret);
+        ret.setRecords(this.entityList2BOs(entityPage.getRecords()));
+
+        return ret;
+    }
+
+}

+ 495 - 0
src/main/java/cc/uncarbon/module/sys/service/SysMenuService.java

@@ -0,0 +1,495 @@
+package cc.uncarbon.module.sys.service;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cc.uncarbon.framework.core.exception.BusinessException;
+import cc.uncarbon.framework.crud.service.impl.HelioBaseServiceImpl;
+import cc.uncarbon.module.sys.annotation.SysLog;
+import cc.uncarbon.module.sys.constant.SysConstant;
+import cc.uncarbon.module.sys.entity.SysMenuEntity;
+import cc.uncarbon.module.sys.enums.GenericStatusEnum;
+import cc.uncarbon.module.sys.enums.SysErrorEnum;
+import cc.uncarbon.module.sys.enums.SysMenuTypeEnum;
+import cc.uncarbon.module.sys.mapper.SysMenuMapper;
+import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysMenuDTO;
+import cc.uncarbon.module.sys.model.request.AdminListSysMenuDTO;
+import cc.uncarbon.module.sys.model.response.SysMenuBO;
+import cc.uncarbon.module.sys.model.response.VbenAdminMenuMetaBO;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Snowflake;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+
+/**
+ * 后台菜单
+ * @author Uncarbon
+ */
+@Slf4j
+@Service
+public class SysMenuService extends HelioBaseServiceImpl<SysMenuMapper, SysMenuEntity> {
+
+    @Resource
+    private SysRoleMenuRelationService sysRoleMenuRelationService;
+
+    @Resource
+    private SysUserRoleRelationService sysUserRoleRelationService;
+
+    @Resource
+    private StringRedisTemplate stringRedisTemplate;
+
+    private static final Snowflake SNOWFLAKE = IdUtil.getSnowflake(0L, 0L);
+
+
+    /**
+     * 后台管理-列表
+     */
+    public List<SysMenuBO> adminList(AdminListSysMenuDTO dto) {
+        List<SysMenuEntity> entityList = this.list(
+                new QueryWrapper<SysMenuEntity>()
+                        .lambda()
+                        // 标题
+                        .like(StrUtil.isNotBlank(dto.getTitle()), SysMenuEntity::getTitle, StrUtil.cleanBlank(dto.getTitle()))
+                        // 上级ID
+                        .eq(SysMenuEntity::getParentId, ObjectUtil.isNull(dto.getParentId()) ? SysConstant.ROOT_PARENT_ID : dto.getParentId())
+                        // 菜单类型
+                        .in(CollUtil.isNotEmpty(dto.getMenuTypes()), SysMenuEntity::getType, dto.getMenuTypes())
+                        // 排序
+                        .orderByAsc(SysMenuEntity::getSort)
+        );
+
+        return this.entityList2BOs(entityList, true);
+    }
+
+    /**
+     * 通用-详情
+     */
+    public SysMenuBO getOneById(Long entityId) {
+        SysMenuEntity entity = this.getById(entityId);
+        SysErrorEnum.INVALID_ID.assertNotNull(entity);
+
+        return this.entity2BO(entity, false, null);
+    }
+
+    /**
+     * 后台管理-新增
+     * @return 主键ID
+     */
+    @SysLog(value = "新增后台菜单")
+    @Transactional(rollbackFor = Exception.class)
+    public Long adminInsert(AdminInsertOrUpdateSysMenuDTO dto) {
+        this.checkExist(dto);
+
+        if (ObjectUtil.isNull(dto.getParentId())) {
+            dto.setParentId(0L);
+        }
+
+        dto.setId(null);
+
+        SysMenuEntity entity = new SysMenuEntity();
+        BeanUtil.copyProperties(dto, entity);
+
+        this.save(entity);
+
+        return entity.getId();
+    }
+
+    /**
+     * 后台管理-编辑
+     */
+    @SysLog(value = "编辑后台菜单")
+    @Transactional(rollbackFor = Exception.class)
+    public void adminUpdate(AdminInsertOrUpdateSysMenuDTO dto) {
+        this.checkExist(dto);
+
+        if (ObjectUtil.isNull(dto.getParentId())) {
+            dto.setParentId(0L);
+        }
+
+        SysMenuEntity entity = new SysMenuEntity();
+        BeanUtil.copyProperties(dto, entity);
+
+        this.updateById(entity);
+    }
+
+    /**
+     * 后台管理-删除
+     */
+    @SysLog(value = "删除后台菜单")
+    @Transactional(rollbackFor = Exception.class)
+    public void adminDelete(List<Long> ids) {
+        this.removeByIds(ids);
+    }
+
+    /**
+     * 后台管理-根据角色Ids取权限串List
+     */
+    public List<String> adminListPermissionByRoleIds(List<Long> roleIds) {
+        if (CollUtil.isEmpty(roleIds)) {
+            return CollUtil.newArrayList();
+        }
+
+        // 超级管理员直接允许所有权限
+        if (roleIds.contains(SysConstant.SUPER_ADMIN_ROLE_ID)) {
+            return this.list().stream().map(SysMenuEntity::getPermission).collect(Collectors.toList());
+        }
+
+        List<Long> menuIds = sysRoleMenuRelationService.listMenuIdByRoleIds(roleIds);
+
+        if (CollUtil.isEmpty(menuIds)) {
+            return CollUtil.newArrayList();
+        }
+
+        return this.list(
+                new QueryWrapper<SysMenuEntity>()
+                        .select(" DISTINCT permission ")
+                        .lambda()
+                        .in(SysMenuEntity::getId, menuIds)
+                        .ne(SysMenuEntity::getPermission, "")
+        ).stream().map(SysMenuEntity::getPermission).collect(Collectors.toList());
+    }
+
+    /**
+     * 后台管理-取当前账号可见侧边菜单
+     */
+    public List<SysMenuBO> adminListSideMenu() {
+        List<Long> visibleMenuIds = this.listCurrentUserVisibleMenuId();
+
+        // 3. 取出无上级节点菜单
+        List<SysMenuTypeEnum> requiredMenuTypes = CollUtil.newArrayList(SysMenuTypeEnum.DIR, SysMenuTypeEnum.MENU, SysMenuTypeEnum.EXTERNAL_LINK);
+        List<SysMenuBO> allMenus = this.listMenuByParentId(visibleMenuIds, requiredMenuTypes, SysConstant.ROOT_PARENT_ID);
+        log.debug("[后台管理][取当前账号可见侧边菜单] 取出无上级节点菜单 >> {}", allMenus);
+
+        // 4.递归查询子节点, 孙节点, 曾孙节点...
+        this.recursiveFindChildren(visibleMenuIds, requiredMenuTypes, allMenus);
+        log.debug("[后台管理][取当前账号可见侧边菜单] 递归查询子节点, 孙节点, 曾孙节点... >> {}", allMenus);
+
+        return allMenus;
+    }
+
+    /**
+     * 后台管理-取当前账号所有可见菜单(包括按钮类型)
+     */
+    public List<SysMenuBO> adminListVisibleMenu() {
+        List<Long> visibleMenuIds = this.listCurrentUserVisibleMenuId();
+
+        // 3. 取出无上级节点菜单
+        List<SysMenuTypeEnum> requiredMenuTypes = CollUtil.newArrayList(SysMenuTypeEnum.DIR, SysMenuTypeEnum.MENU, SysMenuTypeEnum.EXTERNAL_LINK, SysMenuTypeEnum.BUTTON);
+        List<SysMenuBO> allMenus = this.listMenuByParentId(visibleMenuIds, requiredMenuTypes, SysConstant.ROOT_PARENT_ID);
+        log.debug("[后台管理][取当前账号所有可见菜单] 取出无上级节点菜单:{}", allMenus);
+
+        // 4.递归查询子节点, 孙节点, 曾孙节点...
+        this.recursiveFindChildren(visibleMenuIds, requiredMenuTypes, allMenus);
+        log.debug("[后台管理][取当前账号所有可见菜单] 递归查询子节点, 孙节点, 曾孙节点...:{}", allMenus);
+
+        return allMenus;
+    }
+
+    /**
+     * 内部-过滤被禁用的菜单ID
+     *
+     * @param visibleMenuIds 待过滤的可见的菜单ID列表
+     * @return 过滤后的菜单ID列表
+     */
+    public List<Long> filterDisabledIds(List<Long> visibleMenuIds) {
+        if (CollUtil.isEmpty(visibleMenuIds)) {
+            return visibleMenuIds;
+        }
+
+        /*
+        1. 找出目前被禁用的菜单ID
+         */
+        List<Long> disabledMenuIds = this.list(
+                new QueryWrapper<SysMenuEntity>()
+                        .select(" id ")
+                        .lambda()
+                        .eq(SysMenuEntity::getStatus, GenericStatusEnum.DISABLED)
+        ).stream().map(SysMenuEntity::getId).collect(Collectors.toList());
+
+
+        /*
+        2. 提取出符合以下条件的菜单ID:
+            启用状态 && 父菜单未被禁用
+         */
+        return this.list(
+                new QueryWrapper<SysMenuEntity>()
+                        .select(" id ")
+                        .lambda()
+                        .and(
+                                wrapper -> wrapper
+                                        .eq(SysMenuEntity::getStatus, GenericStatusEnum.ENABLED)
+                                        .notIn(CollUtil.isNotEmpty(disabledMenuIds), SysMenuEntity::getParentId, disabledMenuIds)
+                        )
+                        .in(SysMenuEntity::getId, visibleMenuIds)
+        ).stream().map(SysMenuEntity::getId).collect(Collectors.toList());
+    }
+
+    /**
+     * 通用-清除Redis中所有菜单缓存
+     */
+    public void cleanMenuCacheInRedis() {
+        this.cleanMenuCacheInRedis(null);
+    }
+
+    /**
+     * 通用-根据用户ID,清除Redis中相关菜单缓存
+     * @param userId 用户ID;为null则清除所有
+     */
+    public void cleanMenuCacheInRedis(Long userId) {
+        String redisKey;
+        if (ObjectUtil.isNull(userId)) {
+            redisKey = "*";
+        } else {
+            redisKey = "userId_" + StrUtil.toString(userId);
+        }
+
+        log.info("[清除Redis中相关菜单缓存] >> redisKey={}", redisKey);
+
+        Set<String> keys = stringRedisTemplate.keys(SysConstant.REDIS_KEY_SIDE_MENU + redisKey);
+        if (keys != null && !keys.isEmpty()) {
+            stringRedisTemplate.delete(keys);
+        }
+
+        keys = stringRedisTemplate.keys(SysConstant.REDIS_KEY_VISIBLE_MENU + redisKey);
+        if (keys != null && !keys.isEmpty()) {
+            stringRedisTemplate.delete(keys);
+        }
+    }
+    
+
+    /*
+    私有方法
+    ------------------------------------------------------------------------------------------------
+     */
+
+    private SysMenuBO entity2BO(SysMenuEntity entity, boolean traverseChildren, List<SysMenuTypeEnum> menuTypes) {
+        if (entity == null) {
+            return null;
+        }
+
+        SysMenuBO bo = new SysMenuBO();
+        BeanUtil.copyProperties(entity, bo);
+
+        // 可以在此处为BO填充字段
+        if (SysConstant.ROOT_PARENT_ID.equals(bo.getParentId())) {
+            bo.setParentId(null);
+        }
+
+        String snowflakeIdStr = SNOWFLAKE.nextIdStr();
+        bo
+                .setIdStr(StrUtil.toString(bo.getId()))
+                .setName(snowflakeIdStr)
+                .setMeta(
+                        VbenAdminMenuMetaBO.builder()
+                                .title(bo.getTitle())
+                                .affix(false)
+                                .icon(bo.getIcon())
+                                .build()
+                );
+
+        // 这里是兼容JDK8的写法,使用JDK15可使用语法糖免写break
+        switch (bo.getType()) {
+            case DIR:
+            case BUTTON: {
+                bo
+                        .setComponent(SysConstant.VBEN_ADMIN_BLANK_VIEW)
+                        .setExternalLink(null)
+                        .setPath("/" + snowflakeIdStr)
+                ;
+                break;
+            }
+            case MENU: {
+                bo
+                        .setExternalLink(null)
+                        .setPath(bo.getComponent())
+                ;
+                // 防止用户忘记加了, 主动补充/
+                if (StrUtil.isNotBlank(bo.getPath()) && !bo.getPath().startsWith("/")) {
+                    bo.setPath("/" + bo.getPath());
+                }
+                break;
+            }
+            case EXTERNAL_LINK: {
+                bo
+                        .setComponent(bo.getExternalLink())
+                        .setPath(bo.getExternalLink())
+                ;
+                break;
+            }
+        }
+
+        // 遍历子级菜单
+        if (traverseChildren) {
+            List<SysMenuBO> children = this.adminList(
+                    AdminListSysMenuDTO.builder()
+                            .parentId(bo.getId())
+                            .menuTypes(menuTypes)
+                            .build()
+            );
+            if (CollUtil.isEmpty(children)) {
+                children = null;
+            }
+
+            bo.setChildren(children);
+        }
+
+        return bo;
+    }
+
+    private List<SysMenuBO> entityList2BOs(List<SysMenuEntity> entityList, Boolean traverseChildren) {
+        // 深拷贝
+        List<SysMenuBO> ret = new ArrayList<>(entityList.size());
+        entityList.forEach(
+                entity -> ret.add(this.entity2BO(entity, traverseChildren, null))
+        );
+
+        return ret;
+    }
+
+    /**
+     * 取当前账号可见菜单Ids
+     * @return 菜单Ids
+     */
+    private List<Long> listCurrentUserVisibleMenuId() {
+        // 1. 取当前账号拥有角色Ids
+        List<Long> roleIds = sysUserRoleRelationService.listRoleIdByUserId(UserContextHolder.getUserId());
+        log.debug("[后台管理][取当前账号可见菜单Ids] 当前账号拥有角色Ids >> {}", roleIds);
+        if (CollUtil.isEmpty(roleIds)) {
+            throw new BusinessException(SysErrorEnum.NO_ROLE_AVAILABLE_FOR_CURRENT_USER);
+        }
+
+        // 2-1. 超级管理员直接允许所有菜单,即使是禁用状态
+        if (roleIds.contains(SysConstant.SUPER_ADMIN_ROLE_ID)) {
+            return this.list(
+                    new QueryWrapper<SysMenuEntity>()
+                            .select(" id ")
+            ).stream().map(SysMenuEntity::getId).collect(Collectors.toList());
+        }
+
+        // 2-2. 根据角色Ids取菜单Ids
+        List<Long> menuIds = sysRoleMenuRelationService.listMenuIdByRoleIds(roleIds);
+        log.debug("[后台管理][取当前账号可见菜单Ids] 根据角色Ids取菜单Ids >> {}", menuIds);
+        if (CollUtil.isEmpty(menuIds)) {
+            throw new BusinessException(SysErrorEnum.NO_MENU_AVAILABLE_FOR_CURRENT_ROLE);
+        }
+
+        // 3. 临时补充上级菜单ID
+        // 这个Ant-Design的Tree组件有个吊诡的地方, 只选一部分子级的话, 父级菜单不算勾选
+        // 只能在代码里补上了
+        List<Long> missingParentIds = this.listMissingParentId(menuIds);
+        menuIds.addAll(missingParentIds);
+
+        return menuIds;
+    }
+
+    private List<SysMenuBO> listMenuByParentId(List<Long> visibleMenuIds, List<SysMenuTypeEnum> requiredMenuTypes, Long parentId) throws IllegalArgumentException {
+        if (CollUtil.isEmpty(visibleMenuIds)) {
+            throw new IllegalArgumentException("visibleMenuIds不能为空");
+        }
+
+        if (CollUtil.isEmpty(requiredMenuTypes)) {
+            throw new IllegalArgumentException("requiredMenuTypes不能为空");
+        }
+
+        List<SysMenuEntity> entityList = this.list(
+                new QueryWrapper<SysMenuEntity>()
+                        .lambda()
+                        .in(SysMenuEntity::getId, visibleMenuIds)
+                        .in(SysMenuEntity::getType, requiredMenuTypes)
+                        .eq(SysMenuEntity::getParentId, parentId)
+                        .orderByAsc(SysMenuEntity::getSort)
+        );
+
+        if (CollUtil.isEmpty(entityList)) {
+            return CollUtil.newArrayList();
+        }
+
+        List<SysMenuBO> ret = new ArrayList<>(entityList.size());
+        entityList.forEach(
+                each -> ret.add(this.entity2BO(each, false, requiredMenuTypes))
+        );
+
+        return ret;
+    }
+
+    /**
+     * 递归查询子节点
+     */
+    private void recursiveFindChildren(List<Long> visibleMenuIds, List<SysMenuTypeEnum> requiredMenuTypes, List<SysMenuBO> menuBOs) {
+        menuBOs.forEach(
+                menu -> {
+                    // 查询子节点
+                    List<SysMenuBO> itsChildren = this.listMenuByParentId(visibleMenuIds, requiredMenuTypes, menu.getId());
+                    if (CollUtil.isNotEmpty(itsChildren)) {
+                        menu.setChildren(itsChildren);
+                        // 递归查询孙节点
+                        this.recursiveFindChildren(visibleMenuIds, requiredMenuTypes, menu.getChildren());
+                    }
+                }
+        );
+    }
+
+    /**
+     * 检查是否已存在同名数据
+     * @param dto DTO
+     */
+    private void checkExist(AdminInsertOrUpdateSysMenuDTO dto) {
+        if (StrUtil.isNotBlank(dto.getPermission())) {
+            dto.setPermission(StrUtil.cleanBlank(dto.getPermission()));
+
+            SysMenuEntity existEntity = this.getOne(
+                    new QueryWrapper<SysMenuEntity>()
+                            .lambda()
+                            .eq(SysMenuEntity::getPermission, dto.getPermission())
+                            .last(HelioConstant.CRUD.SQL_LIMIT_1)
+            );
+
+            if (existEntity != null && !existEntity.getId().equals(dto.getId())) {
+                throw new BusinessException(400, "已存在相同权限标识,请重新输入");
+            }
+        }
+    }
+
+    /**
+     * 补充遗漏的上级菜单ID
+     */
+    private List<Long> listMissingParentId(List<Long> menuIds) {
+        // 返回值
+        List<Long> ret = new ArrayList<>(32);
+        // 单次循环返回值
+        List<Long> loopRet = menuIds;
+
+        for (int i = 0; i < 5; i++) {
+            // 最多上溯5层
+            loopRet = this.list(
+                    new QueryWrapper<SysMenuEntity>()
+                            .select(" DISTINCT parent_id ")
+                            .lambda()
+                            .ne(SysMenuEntity::getParentId, SysConstant.ROOT_PARENT_ID)
+                            .in(SysMenuEntity::getId, loopRet)
+                            .notIn(SysMenuEntity::getParentId, loopRet)
+            ).stream().map(SysMenuEntity::getParentId).collect(Collectors.toList());
+
+            if (loopRet.isEmpty()) {
+                break;
+            } else {
+                ret.addAll(loopRet);
+            }
+        }
+
+        return ret;
+    }
+}

+ 190 - 0
src/main/java/cc/uncarbon/module/sys/service/SysParamService.java

@@ -0,0 +1,190 @@
+package cc.uncarbon.module.sys.service;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.exception.BusinessException;
+import cc.uncarbon.framework.core.page.PageParam;
+import cc.uncarbon.framework.core.page.PageResult;
+import cc.uncarbon.framework.crud.service.impl.HelioBaseServiceImpl;
+import cc.uncarbon.module.sys.annotation.SysLog;
+import cc.uncarbon.module.sys.entity.SysParamEntity;
+import cc.uncarbon.module.sys.enums.SysErrorEnum;
+import cc.uncarbon.module.sys.mapper.SysParamMapper;
+import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysParamDTO;
+import cc.uncarbon.module.sys.model.request.AdminListSysParamDTO;
+import cc.uncarbon.module.sys.model.response.SysParamBO;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * 系统参数
+ * @author Uncarbon
+ */
+@Slf4j
+@Service
+public class SysParamService extends HelioBaseServiceImpl<SysParamMapper, SysParamEntity> {
+
+    /**
+     * 后台管理-分页列表
+     */
+    public PageResult<SysParamBO> adminList(PageParam pageParam, AdminListSysParamDTO dto) {
+        Page<SysParamEntity> entityPage = this.page(
+                new Page<>(pageParam.getPageNum(), pageParam.getPageSize()),
+                new QueryWrapper<SysParamEntity>()
+                        .lambda()
+                        // 键名
+                        .like(StrUtil.isNotBlank(dto.getKey()), SysParamEntity::getKey, StrUtil.cleanBlank(dto.getKey()))
+                        // 描述
+                        .like(StrUtil.isNotBlank(dto.getDescription()), SysParamEntity::getDescription, StrUtil.cleanBlank(dto.getDescription()))
+                        // 排序
+                        .orderByDesc(SysParamEntity::getCreatedAt)
+        );
+
+        return this.entityPage2BOPage(entityPage);
+    }
+
+    /**
+     * 通用-详情
+     */
+    public SysParamBO getOneById(Long entityId) {
+        SysParamEntity entity = this.getById(entityId);
+        SysErrorEnum.INVALID_ID.assertNotNull(entity);
+
+        return this.entity2BO(entity);
+    }
+
+    /**
+     * 后台管理-新增
+     */
+    @SysLog(value = "新增系统参数")
+    @Transactional(rollbackFor = Exception.class)
+    public Long adminInsert(AdminInsertOrUpdateSysParamDTO dto) {
+        this.checkExist(dto);
+
+        dto.setId(null);
+        SysParamEntity entity = new SysParamEntity();
+        BeanUtil.copyProperties(dto, entity);
+
+        this.save(entity);
+
+        return entity.getId();
+    }
+
+    /**
+     * 后台管理-编辑
+     */
+    @SysLog(value = "编辑系统参数")
+    @Transactional(rollbackFor = Exception.class)
+    public void adminUpdate(AdminInsertOrUpdateSysParamDTO dto) {
+        this.checkExist(dto);
+
+        SysParamEntity entity = new SysParamEntity();
+        BeanUtil.copyProperties(dto, entity);
+
+        this.updateById(entity);
+    }
+
+    /**
+     * 后台管理-删除
+     */
+    @SysLog(value = "删除系统参数")
+    @Transactional(rollbackFor = Exception.class)
+    public void adminDelete(List<Long> ids) {
+        this.removeByIds(ids);
+    }
+
+    /**
+     * 根据键名取值
+     * @param key 键名
+     * @return 成功返回值, 失败返回null
+     */
+    public String getParamValueByKey(String key) {
+        SysParamEntity sysParamEntity = this.getOne(
+                new QueryWrapper<SysParamEntity>()
+                        .select(" value ")
+                        .lambda()
+                        .eq(SysParamEntity::getKey, key)
+                        .last(HelioConstant.CRUD.SQL_LIMIT_1)
+        );
+        if (sysParamEntity == null) {
+            return null;
+        }
+
+        return sysParamEntity.getValue();
+    }
+
+    /**
+     * 根据键名取值
+     *
+     * @param key          键名
+     * @param defaultValue 默认值
+     * @return 成功返回值, 失败返回defaultValue
+     */
+    public String getParamValueByKey(String key, String defaultValue) {
+        String value = this.getParamValueByKey(key);
+        if (value == null) {
+            return defaultValue;
+        }
+
+        return value;
+    }
+
+
+    /*
+    私有方法
+    ------------------------------------------------------------------------------------------------
+     */
+
+    private SysParamBO entity2BO(SysParamEntity entity) {
+        if (entity == null) {
+            return null;
+        }
+
+        SysParamBO bo = new SysParamBO();
+        BeanUtil.copyProperties(entity, bo);
+
+        // 可以在此处为BO填充字段
+
+        return bo;
+    }
+
+    private PageResult<SysParamBO> entityPage2BOPage(Page<SysParamEntity> entityPage) {
+        // 深拷贝
+        List<SysParamBO> boRecords = new ArrayList<>(entityPage.getRecords().size());
+        entityPage.getRecords().forEach(
+                entity -> boRecords.add(this.entity2BO(entity))
+        );
+
+        PageResult<SysParamBO> ret = new PageResult<>();
+        BeanUtil.copyProperties(entityPage, ret);
+        ret.setRecords(boRecords);
+        return ret;
+    }
+
+    /**
+     * 检查是否已存在同名数据
+     * @param dto DTO
+     */
+    private void checkExist(AdminInsertOrUpdateSysParamDTO dto) {
+        SysParamEntity existEntity = this.getOne(
+                new QueryWrapper<SysParamEntity>()
+                        .lambda()
+                        .eq(SysParamEntity::getDescription, dto.getDescription())
+                        .or()
+                        .eq(SysParamEntity::getKey, dto.getKey())
+                        .last(HelioConstant.CRUD.SQL_LIMIT_1)
+        );
+
+        if (existEntity != null && !existEntity.getId().equals(dto.getId())) {
+            throw new BusinessException(400, "已存在相同系统参数,请重新输入");
+        }
+    }
+}

+ 75 - 0
src/main/java/cc/uncarbon/module/sys/service/SysRoleMenuRelationService.java

@@ -0,0 +1,75 @@
+package cc.uncarbon.module.sys.service;
+
+import cc.uncarbon.framework.crud.service.impl.HelioBaseServiceImpl;
+import cc.uncarbon.module.sys.entity.SysRoleMenuRelationEntity;
+import cc.uncarbon.module.sys.mapper.SysRoleMenuRelationMapper;
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.stream.Collectors;
+
+
+/**
+ * 后台角色-可见菜单关联
+ *
+ * @author Uncarbon
+ */
+@Slf4j
+@Service
+public class SysRoleMenuRelationService extends HelioBaseServiceImpl<SysRoleMenuRelationMapper, SysRoleMenuRelationEntity> {
+
+    @Resource
+    private SysMenuService sysMenuService;
+
+
+    /**
+     * 根据角色Ids取菜单Ids
+     *
+     * @param roleIds 角色Ids
+     * @return 菜单Ids
+     */
+    public List<Long> listMenuIdByRoleIds(List<Long> roleIds) throws IllegalArgumentException {
+        if (CollUtil.isEmpty(roleIds)) {
+            throw new IllegalArgumentException("roleIds不能为空");
+        }
+
+        List<Long> menuIds = this.list(
+                new QueryWrapper<SysRoleMenuRelationEntity>()
+                        .select(" DISTINCT menu_id ")
+                        .lambda()
+                        .in(SysRoleMenuRelationEntity::getRoleId, roleIds)
+        ).stream().map(SysRoleMenuRelationEntity::getMenuId).collect(Collectors.toList());
+
+        return sysMenuService.filterDisabledIds(menuIds);
+    }
+
+    /**
+     * 先清理角色ID所有关联关系, 再绑定角色ID与菜单ID
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void cleanAndBind(Long roleId, List<Long> menuIds) {
+        this.remove(
+                new QueryWrapper<SysRoleMenuRelationEntity>()
+                        .lambda()
+                        .eq(SysRoleMenuRelationEntity::getRoleId, roleId)
+        );
+
+        if (CollUtil.isNotEmpty(menuIds)) {
+            // 需要绑定菜单
+
+            menuIds.forEach(
+                    menuId -> this.save(
+                            SysRoleMenuRelationEntity.builder()
+                                    .roleId(roleId)
+                                    .menuId(menuId)
+                                    .build()
+                    )
+            );
+        }
+    }
+}

+ 194 - 0
src/main/java/cc/uncarbon/module/sys/service/SysRoleService.java

@@ -0,0 +1,194 @@
+package cc.uncarbon.module.sys.service;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.exception.BusinessException;
+import cc.uncarbon.framework.core.page.PageParam;
+import cc.uncarbon.framework.core.page.PageResult;
+import cc.uncarbon.framework.crud.service.impl.HelioBaseServiceImpl;
+import cc.uncarbon.module.sys.annotation.SysLog;
+import cc.uncarbon.module.sys.entity.SysRoleEntity;
+import cc.uncarbon.module.sys.enums.SysErrorEnum;
+import cc.uncarbon.module.sys.mapper.SysRoleMapper;
+import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysRoleDTO;
+import cc.uncarbon.module.sys.model.request.AdminListSysRoleDTO;
+import cc.uncarbon.module.sys.model.response.SysRoleBO;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * 后台角色
+ * @author Uncarbon
+ */
+@Slf4j
+@Service
+public class SysRoleService extends HelioBaseServiceImpl<SysRoleMapper, SysRoleEntity> {
+
+    @Resource
+    private SysUserRoleRelationService sysUserRoleRelationService;
+
+    @Resource
+    private SysRoleMenuRelationService sysRoleMenuRelationService;
+
+
+    /**
+     * 后台管理-分页列表
+     */
+    public PageResult<SysRoleBO> adminList(PageParam pageParam, AdminListSysRoleDTO dto) {
+        Page<SysRoleEntity> entityPage = this.page(
+                new Page<>(pageParam.getPageNum(), pageParam.getPageSize()),
+                new QueryWrapper<SysRoleEntity>()
+                        .lambda()
+                        // 名称
+                        .like(StrUtil.isNotBlank(dto.getTitle()), SysRoleEntity::getTitle, StrUtil.cleanBlank(dto.getTitle()))
+                        // 值
+                        .like(StrUtil.isNotBlank(dto.getValue()), SysRoleEntity::getValue, StrUtil.cleanBlank(dto.getValue()))
+                        // 时间区间
+                        .between(ObjectUtil.isNotNull(dto.getBeginAt()) && ObjectUtil.isNotNull(dto.getEndAt()), SysRoleEntity::getCreatedAt, dto.getBeginAt(), dto.getEndAt())
+                        // 排序
+                        .orderByDesc(SysRoleEntity::getCreatedAt)
+        );
+
+        return this.entityPage2BOPage(entityPage);
+    }
+
+    /**
+     * 通用-详情
+     */
+    public SysRoleBO getOneById(Long entityId) {
+        SysRoleEntity entity = this.getById(entityId);
+        SysErrorEnum.INVALID_ID.assertNotNull(entity);
+
+        return this.entity2BO(entity);
+    }
+
+    /**
+     * 后台管理-新增
+     * @return 主键ID
+     */
+    @SysLog(value = "新增后台角色")
+    @Transactional(rollbackFor = Exception.class)
+    public Long adminInsert(AdminInsertOrUpdateSysRoleDTO dto) {
+        this.checkExist(dto);
+
+        dto.setId(null);
+        SysRoleEntity entity = new SysRoleEntity();
+        BeanUtil.copyProperties(dto, entity);
+
+        this.save(entity);
+
+        sysRoleMenuRelationService.cleanAndBind(entity.getId(), dto.getMenuIds());
+
+        return entity.getId();
+    }
+
+    /**
+     * 后台管理-编辑
+     */
+    @SysLog(value = "编辑后台角色")
+    @Transactional(rollbackFor = Exception.class)
+    public void adminUpdate(AdminInsertOrUpdateSysRoleDTO dto) {
+        this.checkExist(dto);
+
+        SysRoleEntity entity = new SysRoleEntity();
+        BeanUtil.copyProperties(dto, entity);
+
+        sysRoleMenuRelationService.cleanAndBind(dto.getId(), dto.getMenuIds());
+
+        this.updateById(entity);
+    }
+
+    /**
+     * 后台管理-删除
+     */
+    @SysLog(value = "删除后台角色")
+    @Transactional(rollbackFor = Exception.class)
+    public void adminDelete(List<Long> ids) {
+        this.removeByIds(ids);
+    }
+
+    /**
+     * 取拥有角色列表
+     * @param userId 用户ID
+     * @return 失败返回空列表
+     */
+    public List<SysRoleBO> listRoleByUserId(Long userId) {
+        List<Long> roleIds = sysUserRoleRelationService.listRoleIdByUserId(userId);
+
+        if (CollUtil.isEmpty(roleIds)) {
+            return CollUtil.newArrayList();
+        }
+
+        // 根据角色Ids取BO
+        List<SysRoleEntity> entityList = this.listByIds(roleIds);
+
+        return this.entityList2BOs(entityList);
+    }
+
+
+    /*
+    私有方法
+    ------------------------------------------------------------------------------------------------
+     */
+
+    private SysRoleBO entity2BO(SysRoleEntity entity) {
+        if (entity == null) {
+            return null;
+        }
+
+        SysRoleBO bo = new SysRoleBO();
+        BeanUtil.copyProperties(entity, bo);
+
+        // 可以在此处为BO填充字段
+        bo.setMenuIds(sysRoleMenuRelationService.listMenuIdByRoleIds(
+           CollUtil.newArrayList(bo.getId())
+        ));
+        return bo;
+    }
+
+    private List<SysRoleBO> entityList2BOs(List<SysRoleEntity> entityList) {
+        // 深拷贝
+        List<SysRoleBO> ret = new ArrayList<>(entityList.size());
+        entityList.forEach(
+                entity -> ret.add(this.entity2BO(entity))
+        );
+
+        return ret;
+    }
+
+    private PageResult<SysRoleBO> entityPage2BOPage(Page<SysRoleEntity> entityPage) {
+        PageResult<SysRoleBO> ret = new PageResult<>();
+        BeanUtil.copyProperties(entityPage, ret);
+        ret.setRecords(this.entityList2BOs(entityPage.getRecords()));
+
+        return ret;
+    }
+
+    /**
+     * 检查是否已存在同名数据
+     * @param dto DTO
+     */
+    private void checkExist(AdminInsertOrUpdateSysRoleDTO dto) {
+        SysRoleEntity existEntity = this.getOne(
+                new QueryWrapper<SysRoleEntity>()
+                        .lambda()
+                        .eq(SysRoleEntity::getTitle, dto.getTitle())
+                        .last(HelioConstant.CRUD.SQL_LIMIT_1)
+        );
+
+        if (existEntity != null && !existEntity.getId().equals(dto.getId())) {
+            throw new BusinessException(400, "已存在相同后台角色,请重新输入");
+        }
+    }
+}

+ 251 - 0
src/main/java/cc/uncarbon/module/sys/service/SysTenantService.java

@@ -0,0 +1,251 @@
+package cc.uncarbon.module.sys.service;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cc.uncarbon.framework.core.exception.BusinessException;
+import cc.uncarbon.framework.core.page.PageParam;
+import cc.uncarbon.framework.core.page.PageResult;
+import cc.uncarbon.framework.crud.service.impl.HelioBaseServiceImpl;
+import cc.uncarbon.module.sys.annotation.SysLog;
+import cc.uncarbon.module.sys.entity.SysTenantEntity;
+import cc.uncarbon.module.sys.entity.SysUserRoleRelationEntity;
+import cc.uncarbon.module.sys.enums.SysErrorEnum;
+import cc.uncarbon.module.sys.mapper.SysTenantMapper;
+import cc.uncarbon.module.sys.model.request.*;
+import cc.uncarbon.module.sys.model.response.SysTenantBO;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * 系统租户
+ * @author Uncarbon
+ */
+@Slf4j
+@Service
+public class SysTenantService extends HelioBaseServiceImpl<SysTenantMapper, SysTenantEntity> {
+
+    @Resource
+    private SysRoleService sysRoleService;
+
+    @Resource
+    private SysUserService sysUserService;
+
+    @Resource
+    private SysUserRoleRelationService sysUserRoleRelationService;
+
+
+    /**
+     * 后台管理-分页列表
+     */
+    public PageResult<SysTenantBO> adminList(PageParam pageParam, AdminListSysTenantDTO dto) {
+        Page<SysTenantEntity> entityPage = this.page(
+                new Page<>(pageParam.getPageNum(), pageParam.getPageSize()),
+                new QueryWrapper<SysTenantEntity>()
+                        .lambda()
+                        // 租户名
+                        .like(StrUtil.isNotBlank(dto.getTenantName()), SysTenantEntity::getTenantName, StrUtil.cleanBlank(dto.getTenantName()))
+                        // 租户ID
+                        .eq(ObjectUtil.isNotNull(dto.getTenantId()), SysTenantEntity::getTenantId, dto.getTenantId())
+                        // 状态
+                        .eq(ObjectUtil.isNotNull(dto.getStatus()), SysTenantEntity::getStatus, dto.getStatus())
+                        // 时间区间
+                        .between(ObjectUtil.isNotNull(dto.getBeginAt()) && ObjectUtil.isNotNull(dto.getEndAt()), SysTenantEntity::getCreatedAt, dto.getBeginAt(), dto.getEndAt())
+                        // 排序
+                        .orderByDesc(SysTenantEntity::getCreatedAt)
+        );
+
+        return this.entityPage2BOPage(entityPage);
+    }
+
+    /**
+     * 通用-详情
+     */
+    public SysTenantBO getOneById(Long entityId) {
+        SysTenantEntity entity = this.getById(entityId);
+        SysErrorEnum.INVALID_ID.assertNotNull(entity);
+
+        return this.entity2BO(entity);
+    }
+
+    /**
+     * 后台管理-新增
+     */
+    @SysLog(value = "新增系统租户")
+    @Transactional(rollbackFor = Exception.class)
+    public Long adminInsert(AdminInsertSysTenantDTO dto) {
+        this.checkExist(dto);
+
+        // 1. 加入一个新租户(tenant)
+        //    这里是直接顺带创建管理员账号了, 你可以根据业务需要决定是否创建
+
+        dto.setId(null);
+        SysTenantEntity entity = new SysTenantEntity();
+        BeanUtil.copyProperties(dto, entity);
+
+        this.save(entity);
+
+        Long newTenantEntityId = entity.getId();
+        Long newTenantId = entity.getTenantId();
+
+        // 2. 创建一个新角色(role)
+        //    注意:这里并没有指派其可见菜单
+        Long newRoleId = sysRoleService.adminInsert(
+                AdminInsertOrUpdateSysRoleDTO.builder()
+                        .tenantId(newTenantId)
+                        .title(dto.getTenantName() + "管理员")
+                        .value("Admin")
+                        .build()
+        );
+
+        // 3. 创建一个新用户
+        Long newUserId = sysUserService.adminInsert(
+                AdminInsertOrUpdateSysUserDTO.builder()
+                        .tenantId(newTenantId)
+                        .username(dto.getTenantAdminUsername())
+                        .passwordOfNewUser(dto.getTenantAdminPassword())
+                        .nickname(dto.getTenantName() + "管理员")
+                        .email(dto.getTenantAdminEmail())
+                        .phoneNo(dto.getTenantAdminPhoneNo())
+                        .build()
+        );
+
+        // 4. 将新用户绑定至新角色上
+        sysUserRoleRelationService.save(
+                SysUserRoleRelationEntity.builder()
+                        .tenantId(newTenantId)
+                        .userId(newUserId)
+                        .roleId(newRoleId)
+                        // 可能不会自动填充字段,手动补上
+                        .createdAt(LocalDateTime.now())
+                        .createdBy(UserContextHolder.getUserContext().getUserName())
+                        .updatedAt(LocalDateTime.now())
+                        .updatedBy(UserContextHolder.getUserContext().getUserName())
+                        .build()
+        );
+
+        entity = new SysTenantEntity();
+        entity
+                .setTenantAdminUserId(newUserId)
+                .setId(newTenantEntityId);
+
+        // 5. 把管理员账号更新进库
+        this.updateById(entity);
+
+        return newTenantId;
+    }
+
+    /**
+     * 后台管理-编辑
+     */
+    @SysLog(value = "编辑系统租户")
+    @Transactional(rollbackFor = Exception.class)
+    public void adminUpdate(AdminUpdateSysTenantDTO dto) {
+        this.checkExist(dto);
+
+        SysTenantEntity entity = new SysTenantEntity();
+        BeanUtil.copyProperties(dto, entity);
+
+        this.updateById(entity);
+    }
+
+    /**
+     * 后台管理-删除
+     */
+    @SysLog(value = "删除系统租户")
+    @Transactional(rollbackFor = Exception.class)
+    public void adminDelete(List<Long> ids) {
+        this.removeByIds(ids);
+    }
+
+    /**
+     * 通用-根据租户ID(非主键ID)查询
+     */
+    public SysTenantBO getTenantByTenantId(Long tenantId) {
+        SysTenantEntity sysTenantEntity = this.getOne(
+                new QueryWrapper<SysTenantEntity>()
+                        .lambda()
+                        .eq(SysTenantEntity::getTenantId, tenantId)
+                        .last(HelioConstant.CRUD.SQL_LIMIT_1)
+        );
+
+        if (sysTenantEntity == null) {
+            return null;
+        }
+
+        return this.entity2BO(sysTenantEntity);
+    }
+
+
+    /*
+    私有方法
+    ------------------------------------------------------------------------------------------------
+     */
+
+    private SysTenantBO entity2BO(SysTenantEntity entity) {
+        if (entity == null) {
+            return null;
+        }
+
+        SysTenantBO bo = new SysTenantBO();
+        BeanUtil.copyProperties(entity, bo);
+
+        // 可以在此处为BO填充字段
+        if (ObjectUtil.isNotNull(entity.getTenantAdminUserId())) {
+            bo
+                    .setTenantAdminUser(sysUserService.getBaseInfoById(entity.getTenantAdminUserId()))
+            ;
+        }
+
+        return bo;
+    }
+
+    private List<SysTenantBO> entityList2BOs(List<SysTenantEntity> entityList) {
+        // 深拷贝
+        List<SysTenantBO> ret = new ArrayList<>(entityList.size());
+        entityList.forEach(
+                entity -> ret.add(this.entity2BO(entity))
+        );
+
+        return ret;
+    }
+
+    private PageResult<SysTenantBO> entityPage2BOPage(Page<SysTenantEntity> entityPage) {
+        PageResult<SysTenantBO> ret = new PageResult<>();
+        BeanUtil.copyProperties(entityPage, ret);
+        ret.setRecords(this.entityList2BOs(entityPage.getRecords()));
+
+        return ret;
+    }
+
+    /**
+     * 检查是否已存在同名数据
+     * @param dto DTO
+     */
+    private void checkExist(AdminUpdateSysTenantDTO dto) {
+        SysTenantEntity existEntity = this.getOne(
+                new QueryWrapper<SysTenantEntity>()
+                        .lambda()
+                        .eq(SysTenantEntity::getTenantId, dto.getTenantId())
+                        .or()
+                        .eq(SysTenantEntity::getTenantName, dto.getTenantName())
+                        .last(HelioConstant.CRUD.SQL_LIMIT_1)
+        );
+
+        if (existEntity != null && !existEntity.getId().equals(dto.getId())) {
+            throw new BusinessException(400, "已存在相同系统租户,请重新输入");
+        }
+    }
+
+}

+ 42 - 0
src/main/java/cc/uncarbon/module/sys/service/SysUserDeptRelationService.java

@@ -0,0 +1,42 @@
+package cc.uncarbon.module.sys.service;
+
+import cc.uncarbon.framework.crud.service.impl.HelioBaseServiceImpl;
+import cc.uncarbon.module.sys.entity.SysUserDeptRelationEntity;
+import cc.uncarbon.module.sys.mapper.SysUserDeptRelationMapper;
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+
+/**
+ * 后台用户-部门关联
+ * @author Uncarbon
+ */
+@Slf4j
+@Service
+public class SysUserDeptRelationService extends HelioBaseServiceImpl<SysUserDeptRelationMapper, SysUserDeptRelationEntity> {
+
+    /**
+     * 先清理用户ID所有关联关系, 再绑定用户ID与部门ID
+     */
+    public void cleanAndBind(Long userId, Long deptId) {
+        this.remove(
+                new QueryWrapper<SysUserDeptRelationEntity>()
+                        .lambda()
+                        .eq(SysUserDeptRelationEntity::getUserId, userId)
+        );
+
+        if (ObjectUtil.isNotNull(deptId)) {
+            // 需要绑定部门
+            this.save(
+                    SysUserDeptRelationEntity.builder()
+                            .userId(userId)
+                            .deptId(deptId)
+                            .build()
+            );
+        }
+
+    }
+
+}

+ 81 - 0
src/main/java/cc/uncarbon/module/sys/service/SysUserRoleRelationService.java

@@ -0,0 +1,81 @@
+package cc.uncarbon.module.sys.service;
+
+import cc.uncarbon.framework.crud.service.impl.HelioBaseServiceImpl;
+import cc.uncarbon.module.sys.entity.SysUserRoleRelationEntity;
+import cc.uncarbon.module.sys.mapper.SysUserRoleRelationMapper;
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+
+/**
+ * 后台用户-角色关联
+ * @author Uncarbon
+ */
+@Slf4j
+@Service
+public class SysUserRoleRelationService extends HelioBaseServiceImpl<SysUserRoleRelationMapper, SysUserRoleRelationEntity> {
+
+    /**
+     * 先清理用户ID所有关联关系, 再绑定用户ID与角色ID
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public void cleanAndBind(Long userId, List<Long> roleIds) {
+        this.remove(
+                new QueryWrapper<SysUserRoleRelationEntity>()
+                        .lambda()
+                        .eq(SysUserRoleRelationEntity::getUserId, userId)
+        );
+
+        if (CollUtil.isNotEmpty(roleIds)) {
+            // 需要绑定角色
+            roleIds.forEach(
+                    roleId -> this.save(
+                            SysUserRoleRelationEntity.builder()
+                                    .userId(userId)
+                                    .roleId(roleId)
+                                    .build()
+                    )
+            );
+        }
+    }
+
+    /**
+     * 根据角色Ids取用户Ids
+     */
+    public List<Long> listUserIdByRoleIds(List<Long> roleIds) throws IllegalArgumentException {
+        if (CollUtil.isEmpty(roleIds)) {
+            throw new IllegalArgumentException("roleIds不能为空");
+        }
+
+        return this.list(
+                new QueryWrapper<SysUserRoleRelationEntity>()
+                        .select(" user_id ")
+                        .lambda()
+                        .in(SysUserRoleRelationEntity::getRoleId, roleIds)
+        ).stream().map(SysUserRoleRelationEntity::getUserId).collect(Collectors.toList());
+    }
+
+    /**
+     * 取拥有角色Ids
+     * @param userId 用户ID
+     * @return 失败返回空列表
+     */
+    public List<Long> listRoleIdByUserId(Long userId) throws IllegalArgumentException {
+        if (userId == null) {
+            throw new IllegalArgumentException("userId不能为空");
+        }
+
+        return this.list(
+                new QueryWrapper<SysUserRoleRelationEntity>()
+                        .select(" role_id ")
+                        .lambda()
+                        .eq(SysUserRoleRelationEntity::getUserId, userId)
+        ).stream().map(SysUserRoleRelationEntity::getRoleId).collect(Collectors.toList());
+    }
+}

+ 337 - 0
src/main/java/cc/uncarbon/module/sys/service/SysUserService.java

@@ -0,0 +1,337 @@
+package cc.uncarbon.module.sys.service;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.context.TenantContext;
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cc.uncarbon.framework.core.exception.BusinessException;
+import cc.uncarbon.framework.core.page.PageParam;
+import cc.uncarbon.framework.core.page.PageResult;
+import cc.uncarbon.framework.crud.service.impl.HelioBaseServiceImpl;
+import cc.uncarbon.module.sys.annotation.SysLog;
+import cc.uncarbon.module.sys.entity.SysUserEntity;
+import cc.uncarbon.module.sys.enums.GenericStatusEnum;
+import cc.uncarbon.module.sys.enums.SysErrorEnum;
+import cc.uncarbon.module.sys.enums.SysUserStatusEnum;
+import cc.uncarbon.module.sys.mapper.SysUserMapper;
+import cc.uncarbon.module.sys.model.request.*;
+import cc.uncarbon.module.sys.model.response.*;
+import cc.uncarbon.module.sys.util.PwdUtil;
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.date.LocalDateTimeUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+
+/**
+ * 后台用户
+ * @author Uncarbon
+ */
+@Slf4j
+@Service
+public class SysUserService extends HelioBaseServiceImpl<SysUserMapper, SysUserEntity> {
+
+    @Resource
+    private SysRoleService sysRoleService;
+
+    @Resource
+    private SysDeptService sysDeptService;
+
+    @Resource
+    private SysMenuService sysMenuService;
+
+    @Resource
+    private SysTenantService sysTenantService;
+
+    @Resource
+    private SysUserDeptRelationService sysUserDeptRelationService;
+
+    @Resource
+    private SysUserRoleRelationService sysUserRoleRelationService;
+
+
+    /**
+     * 后台管理-分页列表
+     */
+    public PageResult<SysUserBO> adminList(PageParam pageParam, AdminListSysUserDTO dto) {
+        Page<SysUserEntity> entityPage = this.page(
+                new Page<>(pageParam.getPageNum(), pageParam.getPageSize()),
+                new QueryWrapper<SysUserEntity>()
+                        .lambda()
+                        // 手机号
+                        .like(StrUtil.isNotBlank(dto.getPhoneNo()), SysUserEntity::getPhoneNo, StrUtil.cleanBlank(dto.getPhoneNo()))
+                        // 排序
+                        .orderByDesc(SysUserEntity::getCreatedAt)
+        );
+
+        return this.entityPage2BOPage(entityPage);
+    }
+
+    /**
+     * 通用-详情
+     */
+    public SysUserBO getOneById(Long entityId) {
+        SysUserEntity entity = this.getById(entityId);
+        SysErrorEnum.INVALID_ID.assertNotNull(entity);
+
+        return this.entity2BO(entity, true);
+    }
+
+    /**
+     * 后台管理-新增
+     * @return 主键ID
+     */
+    @SysLog(value = "新增后台用户")
+    @Transactional(rollbackFor = Exception.class)
+    public Long adminInsert(AdminInsertOrUpdateSysUserDTO dto) {
+        this.checkExist(dto);
+
+        dto.setId(null);
+        SysUserEntity entity = new SysUserEntity();
+        BeanUtil.copyProperties(dto, entity);
+
+        String salt = IdUtil.randomUUID();
+        entity
+                .setSalt(salt)
+                .setPin(dto.getUsername())
+                .setPwd(PwdUtil.encrypt(dto.getPasswordOfNewUser(), salt))
+        ;
+
+        this.save(entity);
+
+        sysUserDeptRelationService.cleanAndBind(entity.getId(), dto.getDeptId());
+
+        return entity.getId();
+    }
+
+    /**
+     * 后台管理-编辑
+     */
+    @SysLog(value = "编辑后台用户")
+    @Transactional(rollbackFor = Exception.class)
+    public void adminUpdate(AdminInsertOrUpdateSysUserDTO dto) {
+        this.checkExist(dto);
+
+        SysUserEntity updateEntity = new SysUserEntity();
+        BeanUtil.copyProperties(dto, updateEntity);
+
+        sysUserDeptRelationService.cleanAndBind(dto.getId(), dto.getDeptId());
+
+        this.updateById(updateEntity);
+    }
+
+    /**
+     * 后台管理-删除
+     */
+    @SysLog(value = "删除后台用户")
+    @Transactional(rollbackFor = Exception.class)
+    public void adminDelete(List<Long> ids) {
+        this.removeByIds(ids);
+    }
+
+    /**
+     * 后台管理-登录
+     */
+    @SysLog(value = "登录后台用户")
+    public SysUserLoginBO adminLogin(SysUserLoginDTO dto) {
+        // 主动清空用户上下文,避免残留租户ID导致的尴尬
+        UserContextHolder.setUserContext(null);
+
+        SysUserEntity sysUserEntity = this.getUserByPin(dto.getUsername());
+        if (sysUserEntity == null) {
+            throw new BusinessException(SysErrorEnum.USER_NOT_EXISTS);
+        }
+
+        if (!PwdUtil.encrypt(dto.getPassword(), sysUserEntity.getSalt()).equals(sysUserEntity.getPwd())) {
+            throw new BusinessException(SysErrorEnum.INCORRECT_USER_PASSWORD);
+        }
+
+        if (SysUserStatusEnum.BANNED.equals(sysUserEntity.getStatus())) {
+            throw new BusinessException(SysErrorEnum.BANNED_USER);
+        }
+
+        // 查询所属租户是否有效
+        SysTenantBO tenantInfo = sysTenantService.getTenantByTenantId(sysUserEntity.getTenantId());
+        if (tenantInfo == null) {
+            throw new BusinessException(SysErrorEnum.INVALID_TENANT);
+        }
+
+        if (GenericStatusEnum.DISABLED.equals(tenantInfo.getStatus())) {
+            throw new BusinessException(SysErrorEnum.DISABLED_TENANT);
+        }
+
+        /*
+        以上为有效性校验, 进入实际业务逻辑
+        ---------------------------------------------------
+         */
+
+        // 将所属租户写入到用户上下文,使得 Mybatis-Plus 多租户拦截器可以正确执行到对应租户ID
+        TenantContext currentTenantContext = TenantContext.builder()
+                .tenantId(tenantInfo.getTenantId())
+                .tenantName(tenantInfo.getTenantName())
+                .build();
+        UserContextHolder.setRelationalTenant(currentTenantContext);
+
+        try {
+            this.getBaseMapper().updateLastLoginAt(sysUserEntity.getId(), LocalDateTimeUtil.now());
+        } catch (Exception ignored) {
+            // 实际开发环境请删除本try-catch块
+        }
+
+
+        // 取账号完整BO
+        SysUserBO sysUserBO = this.getOneById(sysUserEntity.getId());
+
+        SysUserLoginBO ret = new SysUserLoginBO();
+        BeanUtil.copyProperties(sysUserBO, ret);
+
+        // 因字段类型不一致, 单独转换
+        ret
+                .setRoles(sysUserBO.getRoles().stream().map(SysRoleBO::getValue).collect(Collectors.toList()))
+                .setPermissions(sysUserBO.getPermissions())
+                .setRelationalTenant(currentTenantContext)
+        ;
+
+        return ret;
+    }
+
+    /**
+     * 后台管理-取当前用户信息
+     */
+    public VbenAdminUserInfoBO adminGetCurrentUserInfo() {
+        SysUserBO sysUserBO = getOneById(UserContextHolder.getUserId());
+        return VbenAdminUserInfoBO.builder()
+                .username(sysUserBO.getUsername())
+                .nickname(sysUserBO.getNickname())
+                .build();
+    }
+
+    /**
+     * 后台管理-重置某用户密码
+     */
+    @SysLog(value = "重置某用户密码")
+    public void adminResetUserPassword(AdminResetSysUserPasswordDTO dto) {
+        SysUserEntity sysUserEntity = this.getById(dto.getUserId());
+
+        SysUserEntity templateEntity = new SysUserEntity();
+        templateEntity
+                .setPwd(PwdUtil.encrypt(dto.getRandomPassword(), sysUserEntity.getSalt()))
+                .setId(dto.getUserId())
+                ;
+
+        this.updateById(templateEntity);
+    }
+
+    /**
+     * 后台管理-修改当前用户密码
+     */
+    @SysLog(value = "修改当前用户密码")
+    public void adminUpdateCurrentUserPassword(AdminUpdateCurrentSysUserPasswordDTO dto) {
+        SysUserEntity sysUserEntity = this.getById(UserContextHolder.getUserId());
+        if (sysUserEntity == null || !sysUserEntity.getPwd().equals(PwdUtil.encrypt(dto.getOldPassword(), sysUserEntity.getSalt()))) {
+            throw new BusinessException(SysErrorEnum.INCORRECT_OLD_PASSWORD);
+        }
+
+        sysUserEntity
+                .setPwd(PwdUtil.encrypt(dto.getConfirmNewPassword(), sysUserEntity.getSalt()))
+                .setId(UserContextHolder.getUserId())
+        ;
+
+        this.updateById(sysUserEntity);
+    }
+
+    /**
+     * 通用-根据用户账号查询
+     */
+    public SysUserEntity getUserByPin(String pin) {
+        return this.getOne(
+                new QueryWrapper<SysUserEntity>()
+                        .lambda()
+                        .eq(SysUserEntity::getPin, pin)
+                        .last(HelioConstant.CRUD.SQL_LIMIT_1)
+        );
+    }
+
+    /**
+     * 通用-取用户基本信息
+     */
+    public SysUserBaseInfoBO getBaseInfoById(Long entityId) {
+        return this.getBaseMapper().getBaseInfoByUserId(entityId);
+    }
+
+    /**
+     * 后台管理-绑定用户与角色关联关系
+     */
+    public void adminBindRoles(AdminBindUserRoleRelationDTO dto) {
+        sysUserRoleRelationService.cleanAndBind(dto.getUserId(), dto.getRoleIds());
+    }
+
+    /*
+    私  有  方  法
+    ------------------------------------------------------------------------------------------------
+     */
+
+    private SysUserBO entity2BO(SysUserEntity entity, boolean joinFullRolesAndPermissions) {
+        if (entity == null) {
+            return null;
+        }
+
+        SysUserBO bo = new SysUserBO();
+        BeanUtil.copyProperties(entity, bo);
+
+        // 可以在此处为BO填充字段
+        bo.setRoleIds(sysUserRoleRelationService.listRoleIdByUserId(bo.getId()));
+
+        SysDeptBO dept = sysDeptService.getPlainDeptByUserId(bo.getId());
+        if (dept != null) {
+            bo
+                    .setDeptId(dept.getId())
+                    .setDeptTitle(dept.getTitle())
+            ;
+        }
+
+        if (joinFullRolesAndPermissions) {
+            List<SysRoleBO> roleBOs = sysRoleService.listRoleByUserId(bo.getId());
+            bo
+                    .setRoles(roleBOs)
+                    .setPermissions(sysMenuService.adminListPermissionByRoleIds(roleBOs.stream().map(SysRoleBO::getId).collect(Collectors.toList())))
+            ;
+        }
+
+        return bo;
+    }
+
+    private PageResult<SysUserBO> entityPage2BOPage(Page<SysUserEntity> entityPage) {
+        // 深拷贝
+        List<SysUserBO> boRecords = new ArrayList<>(entityPage.getRecords().size());
+        entityPage.getRecords().forEach(
+                entity -> boRecords.add(this.entity2BO(entity, false))
+        );
+
+        PageResult<SysUserBO> ret = new PageResult<>();
+        BeanUtil.copyProperties(entityPage, ret);
+        ret.setRecords(boRecords);
+        return ret;
+    }
+
+    /**
+     * 检查是否已存在同名数据
+     * @param dto DTO
+     */
+    private void checkExist(AdminInsertOrUpdateSysUserDTO dto) {
+        SysUserEntity existEntity = this.getUserByPin(dto.getUsername());
+
+        if (existEntity != null && !existEntity.getId().equals(dto.getId())) {
+            throw new BusinessException(400, "已存在相同账号,请重新输入");
+        }
+    }
+}

+ 769 - 0
src/main/java/cc/uncarbon/module/sys/util/AdminStpUtil.java

@@ -0,0 +1,769 @@
+package cc.uncarbon.module.sys.util;
+
+import cn.dev33.satoken.SaManager;
+import cn.dev33.satoken.fun.SaFunction;
+import cn.dev33.satoken.session.SaSession;
+import cn.dev33.satoken.stp.SaLoginModel;
+import cn.dev33.satoken.stp.SaTokenInfo;
+import cn.dev33.satoken.stp.StpLogic;
+import cn.dev33.satoken.stp.StpUtil;
+import lombok.experimental.UtilityClass;
+
+import java.util.List;
+
+/**
+ * 自定义的StpUtil,用于后台管理
+ * @author Uncarbon
+ */
+@UtilityClass
+public class AdminStpUtil {
+
+    /**
+     * 账号类型标识
+     */
+    public static final String TYPE = "AdminStpUtil";
+
+    /**
+     * 底层的 StpLogic 对象
+     */
+    public static final StpLogic stpLogic = new StpLogic(TYPE);
+
+    /**
+     * 获取当前 StpLogic 的账号类型
+     * @return See Note
+     */
+    public static String getLoginType(){
+        return stpLogic.getLoginType();
+    }
+
+    /**
+     * 重置 StpLogic 对象
+     * @param stpLogic /
+     */
+    public static void setStpLogic(StpLogic stpLogic) {
+        StpUtil.stpLogic = stpLogic;
+        // 防止自定义 stpLogic 被覆盖
+        SaManager.putStpLogic(stpLogic);
+    }
+
+
+    // =================== 获取token 相关 ===================
+
+    /**
+     * 返回token名称
+     * @return 此StpLogic的token名称
+     */
+    public static String getTokenName() {
+        return stpLogic.getTokenName();
+    }
+
+    /**
+     * 在当前会话写入当前TokenValue
+     * @param tokenValue token值
+     * @param cookieTimeout Cookie存活时间(秒)
+     */
+    public static void setTokenValue(String tokenValue, int cookieTimeout){
+        stpLogic.setTokenValue(tokenValue, cookieTimeout);
+    }
+
+    /**
+     * 获取当前TokenValue
+     * @return 当前tokenValue
+     */
+    public static String getTokenValue() {
+        return stpLogic.getTokenValue();
+    }
+
+    /**
+     * 获取当前会话的Token信息
+     * @return token信息
+     */
+    public static SaTokenInfo getTokenInfo() {
+        return stpLogic.getTokenInfo();
+    }
+
+
+    // =================== 登录相关操作 ===================
+
+    // --- 登录
+
+    /**
+     * 会话登录
+     * @param id 账号id,建议的类型:(long | int | String)
+     */
+    public static void login(Object id) {
+        stpLogic.login(id);
+    }
+
+    /**
+     * 会话登录,并指定登录设备
+     * @param id 账号id,建议的类型:(long | int | String)
+     * @param device 设备标识
+     */
+    public static void login(Object id, String device) {
+        stpLogic.login(id, device);
+    }
+
+    /**
+     * 会话登录,并指定是否 [记住我]
+     * @param id 账号id,建议的类型:(long | int | String)
+     * @param isLastingCookie 是否为持久Cookie
+     */
+    public static void login(Object id, boolean isLastingCookie) {
+        stpLogic.login(id, isLastingCookie);
+    }
+
+    /**
+     * 会话登录,并指定所有登录参数Model
+     * @param id 登录id,建议的类型:(long | int | String)
+     * @param loginModel 此次登录的参数Model
+     */
+    public static void login(Object id, SaLoginModel loginModel) {
+        stpLogic.login(id, loginModel);
+    }
+
+    // --- 注销
+
+    /**
+     * 会话注销
+     */
+    public static void logout() {
+        stpLogic.logout();
+    }
+
+    /**
+     * 会话注销,根据账号id
+     * @param loginId 账号id
+     */
+    public static void logout(Object loginId) {
+        stpLogic.logout(loginId);
+    }
+
+    /**
+     * 会话注销,根据账号id 和 设备标识
+     *
+     * @param loginId 账号id
+     * @param device 设备标识 (填null代表所有注销设备)
+     */
+    public static void logout(Object loginId, String device) {
+        stpLogic.logout(loginId, device);
+    }
+
+    /**
+     * 会话注销,根据指定 Token
+     *
+     * @param tokenValue 指定token
+     */
+    public static void logoutByTokenValue(String tokenValue) {
+        stpLogic.logoutByTokenValue(tokenValue);
+    }
+
+    /**
+     * 踢人下线,根据账号id
+     * <p> 当对方再次访问系统时,会抛出NotLoginException异常,场景值=-5 </p>
+     *
+     * @param loginId 账号id
+     */
+    public static void kickout(Object loginId) {
+        stpLogic.kickout(loginId);
+    }
+
+    /**
+     * 踢人下线,根据账号id 和 设备标识
+     * <p> 当对方再次访问系统时,会抛出NotLoginException异常,场景值=-5 </p>
+     *
+     * @param loginId 账号id
+     * @param device 设备标识 (填null代表踢出所有设备)
+     */
+    public static void kickout(Object loginId, String device) {
+        stpLogic.kickout(loginId, device);
+    }
+
+    /**
+     * 踢人下线,根据指定 Token
+     * <p> 当对方再次访问系统时,会抛出NotLoginException异常,场景值=-5 </p>
+     *
+     * @param tokenValue 指定token
+     */
+    public static void kickoutByTokenValue(String tokenValue) {
+        stpLogic.kickoutByTokenValue(tokenValue);
+    }
+
+    /**
+     * 顶人下线,根据账号id 和 设备标识
+     * <p> 当对方再次访问系统时,会抛出NotLoginException异常,场景值=-4 </p>
+     *
+     * @param loginId 账号id
+     * @param device 设备标识 (填null代表顶替所有设备)
+     */
+    public static void replaced(Object loginId, String device) {
+        stpLogic.replaced(loginId, device);
+    }
+
+
+    // 查询相关
+
+    /**
+     * 当前会话是否已经登录
+     * @return 是否已登录
+     */
+    public static boolean isLogin() {
+        return stpLogic.isLogin();
+    }
+
+    /**
+     * 检验当前会话是否已经登录,如未登录,则抛出异常
+     */
+    public static void checkLogin() {
+        stpLogic.checkLogin();
+    }
+
+    /**
+     * 获取当前会话账号id, 如果未登录,则抛出异常
+     * @return 账号id
+     */
+    public static Object getLoginId() {
+        return stpLogic.getLoginId();
+    }
+
+    /**
+     * 获取当前会话账号id, 如果未登录,则返回默认值
+     * @param <T> 返回类型
+     * @param defaultValue 默认值
+     * @return 登录id
+     */
+    public static <T> T getLoginId(T defaultValue) {
+        return stpLogic.getLoginId(defaultValue);
+    }
+
+    /**
+     * 获取当前会话账号id, 如果未登录,则返回null
+     * @return 账号id
+     */
+    public static Object getLoginIdDefaultNull() {
+        return stpLogic.getLoginIdDefaultNull();
+    }
+
+    /**
+     * 获取当前会话账号id, 并转换为String类型
+     * @return 账号id
+     */
+    public static String getLoginIdAsString() {
+        return stpLogic.getLoginIdAsString();
+    }
+
+    /**
+     * 获取当前会话账号id, 并转换为int类型
+     * @return 账号id
+     */
+    public static int getLoginIdAsInt() {
+        return stpLogic.getLoginIdAsInt();
+    }
+
+    /**
+     * 获取当前会话账号id, 并转换为long类型
+     * @return 账号id
+     */
+    public static long getLoginIdAsLong() {
+        return stpLogic.getLoginIdAsLong();
+    }
+
+    /**
+     * 获取指定Token对应的账号id,如果未登录,则返回 null
+     * @param tokenValue token
+     * @return 账号id
+     */
+    public static Object getLoginIdByToken(String tokenValue) {
+        return stpLogic.getLoginIdByToken(tokenValue);
+    }
+
+
+    // =================== User-Session 相关 ===================
+
+    /**
+     * 获取指定账号id的Session, 如果Session尚未创建,isCreate=是否新建并返回
+     * @param loginId 账号id
+     * @param isCreate 是否新建
+     * @return Session对象
+     */
+    public static SaSession getSessionByLoginId(Object loginId, boolean isCreate) {
+        return stpLogic.getSessionByLoginId(loginId, isCreate);
+    }
+
+    /**
+     * 获取指定key的Session, 如果Session尚未创建,则返回null
+     * @param sessionId SessionId
+     * @return Session对象
+     */
+    public static SaSession getSessionBySessionId(String sessionId) {
+        return stpLogic.getSessionBySessionId(sessionId);
+    }
+
+    /**
+     * 获取指定账号id的Session,如果Session尚未创建,则新建并返回
+     * @param loginId 账号id
+     * @return Session对象
+     */
+    public static SaSession getSessionByLoginId(Object loginId) {
+        return stpLogic.getSessionByLoginId(loginId);
+    }
+
+    /**
+     * 获取当前会话的Session, 如果Session尚未创建,isCreate=是否新建并返回
+     * @param isCreate 是否新建
+     * @return Session对象
+     */
+    public static SaSession getSession(boolean isCreate) {
+        return stpLogic.getSession(isCreate);
+    }
+
+    /**
+     * 获取当前会话的Session,如果Session尚未创建,则新建并返回
+     * @return Session对象
+     */
+    public static SaSession getSession() {
+        return stpLogic.getSession();
+    }
+
+
+    // =================== Token-Session 相关 ===================
+
+    /**
+     * 获取指定Token-Session,如果Session尚未创建,则新建并返回
+     * @param tokenValue Token值
+     * @return Session对象
+     */
+    public static SaSession getTokenSessionByToken(String tokenValue) {
+        return stpLogic.getTokenSessionByToken(tokenValue);
+    }
+
+    /**
+     * 获取当前Token-Session,如果Session尚未创建,则新建并返回
+     * @return Session对象
+     */
+    public static SaSession getTokenSession() {
+        return stpLogic.getTokenSession();
+    }
+
+
+    // =================== [临时过期] 验证相关 ===================
+
+    /**
+     * 检查当前token 是否已经[临时过期],如果已经过期则抛出异常
+     */
+    public static void checkActivityTimeout() {
+        stpLogic.checkActivityTimeout();
+    }
+
+    /**
+     * 续签当前token:(将 [最后操作时间] 更新为当前时间戳)
+     * <h1>请注意: 即时token已经 [临时过期] 也可续签成功,
+     * 如果此场景下需要提示续签失败,可在此之前调用 checkActivityTimeout() 强制检查是否过期即可 </h1>
+     */
+    public static void updateLastActivityToNow() {
+        stpLogic.updateLastActivityToNow();
+    }
+
+
+    // =================== 过期时间相关 ===================
+
+    /**
+     * 获取当前登录者的 token 剩余有效时间 (单位: 秒)
+     * @return token剩余有效时间
+     */
+    public static long getTokenTimeout() {
+        return stpLogic.getTokenTimeout();
+    }
+
+    /**
+     * 获取当前登录者的 User-Session 剩余有效时间 (单位: 秒)
+     * @return token剩余有效时间
+     */
+    public static long getSessionTimeout() {
+        return stpLogic.getSessionTimeout();
+    }
+
+    /**
+     * 获取当前 Token-Session 剩余有效时间 (单位: 秒)
+     * @return token剩余有效时间
+     */
+    public static long getTokenSessionTimeout() {
+        return stpLogic.getTokenSessionTimeout();
+    }
+
+    /**
+     * 获取当前 token [临时过期] 剩余有效时间 (单位: 秒)
+     * @return token [临时过期] 剩余有效时间
+     */
+    public static long getTokenActivityTimeout() {
+        return stpLogic.getTokenActivityTimeout();
+    }
+
+
+
+    // =================== 角色验证操作 ===================
+
+    /**
+     * 获取:当前账号的角色集合
+     * @return /
+     */
+    public static List<String> getRoleList() {
+        return stpLogic.getRoleList();
+    }
+
+    /**
+     * 获取:指定账号的角色集合
+     * @param loginId 指定账号id
+     * @return /
+     */
+    public static List<String> getRoleList(Object loginId) {
+        return stpLogic.getRoleList(loginId);
+    }
+
+    /**
+     * 判断:当前账号是否拥有指定角色, 返回true或false
+     * @param role 角色标识
+     * @return 是否含有指定角色标识
+     */
+    public static boolean hasRole(String role) {
+        return stpLogic.hasRole(role);
+    }
+
+    /**
+     * 判断:指定账号是否含有指定角色标识, 返回true或false
+     * @param loginId 账号id
+     * @param role 角色标识
+     * @return 是否含有指定角色标识
+     */
+    public static boolean hasRole(Object loginId, String role) {
+        return stpLogic.hasRole(loginId, role);
+    }
+
+    /**
+     * 判断:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
+     * @param roleArray 角色标识数组
+     * @return true或false
+     */
+    public static boolean hasRoleAnd(String... roleArray){
+        return stpLogic.hasRoleAnd(roleArray);
+    }
+
+    /**
+     * 判断:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
+     * @param roleArray 角色标识数组
+     * @return true或false
+     */
+    public static boolean hasRoleOr(String... roleArray){
+        return stpLogic.hasRoleOr(roleArray);
+    }
+
+    /**
+     * 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
+     * @param role 角色标识
+     */
+    public static void checkRole(String role) {
+        stpLogic.checkRole(role);
+    }
+
+    /**
+     * 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
+     * @param roleArray 角色标识数组
+     */
+    public static void checkRoleAnd(String... roleArray){
+        stpLogic.checkRoleAnd(roleArray);
+    }
+
+    /**
+     * 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
+     * @param roleArray 角色标识数组
+     */
+    public static void checkRoleOr(String... roleArray){
+        stpLogic.checkRoleOr(roleArray);
+    }
+
+
+    // =================== 权限验证操作 ===================
+
+    /**
+     * 获取:当前账号的权限码集合
+     * @return /
+     */
+    public static List<String> getPermissionList() {
+        return stpLogic.getPermissionList();
+    }
+
+    /**
+     * 获取:指定账号的权限码集合
+     * @param loginId 指定账号id
+     * @return /
+     */
+    public static List<String> getPermissionList(Object loginId) {
+        return stpLogic.getPermissionList(loginId);
+    }
+
+    /**
+     * 判断:当前账号是否含有指定权限, 返回true或false
+     * @param permission 权限码
+     * @return 是否含有指定权限
+     */
+    public static boolean hasPermission(String permission) {
+        return stpLogic.hasPermission(permission);
+    }
+
+    /**
+     * 判断:指定账号id是否含有指定权限, 返回true或false
+     * @param loginId 账号id
+     * @param permission 权限码
+     * @return 是否含有指定权限
+     */
+    public static boolean hasPermission(Object loginId, String permission) {
+        return stpLogic.hasPermission(loginId, permission);
+    }
+
+    /**
+     * 判断:当前账号是否含有指定权限, [指定多个,必须全部具有]
+     * @param permissionArray 权限码数组
+     * @return true 或 false
+     */
+    public static boolean hasPermissionAnd(String... permissionArray){
+        return stpLogic.hasPermissionAnd(permissionArray);
+    }
+
+    /**
+     * 判断:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
+     * @param permissionArray 权限码数组
+     * @return true 或 false
+     */
+    public static boolean hasPermissionOr(String... permissionArray){
+        return stpLogic.hasPermissionOr(permissionArray);
+    }
+
+    /**
+     * 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException
+     * @param permission 权限码
+     */
+    public static void checkPermission(String permission) {
+        stpLogic.checkPermission(permission);
+    }
+
+    /**
+     * 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
+     * @param permissionArray 权限码数组
+     */
+    public static void checkPermissionAnd(String... permissionArray) {
+        stpLogic.checkPermissionAnd(permissionArray);
+    }
+
+    /**
+     * 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
+     * @param permissionArray 权限码数组
+     */
+    public static void checkPermissionOr(String... permissionArray) {
+        stpLogic.checkPermissionOr(permissionArray);
+    }
+
+
+    // =================== id 反查token 相关操作 ===================
+
+    /**
+     * 获取指定账号id的tokenValue
+     * <p> 在配置为允许并发登录时,此方法只会返回队列的最后一个token,
+     * 如果你需要返回此账号id的所有token,请调用 getTokenValueListByLoginId
+     * @param loginId 账号id
+     * @return token值
+     */
+    public static String getTokenValueByLoginId(Object loginId) {
+        return stpLogic.getTokenValueByLoginId(loginId);
+    }
+
+    /**
+     * 获取指定账号id指定设备端的tokenValue
+     * <p> 在配置为允许并发登录时,此方法只会返回队列的最后一个token,
+     * 如果你需要返回此账号id的所有token,请调用 getTokenValueListByLoginId
+     * @param loginId 账号id
+     * @param device 设备标识
+     * @return token值
+     */
+    public static String getTokenValueByLoginId(Object loginId, String device) {
+        return stpLogic.getTokenValueByLoginId(loginId, device);
+    }
+
+    /**
+     * 获取指定账号id的tokenValue集合
+     * @param loginId 账号id
+     * @return 此loginId的所有相关token
+     */
+    public static List<String> getTokenValueListByLoginId(Object loginId) {
+        return stpLogic.getTokenValueListByLoginId(loginId);
+    }
+
+    /**
+     * 获取指定账号id指定设备端的tokenValue 集合
+     * @param loginId 账号id
+     * @param device 设备标识
+     * @return 此loginId的所有相关token
+     */
+    public static List<String> getTokenValueListByLoginId(Object loginId, String device) {
+        return stpLogic.getTokenValueListByLoginId(loginId, device);
+    }
+
+    /**
+     * 返回当前会话的登录设备
+     * @return 当前令牌的登录设备
+     */
+    public static String getLoginDevice() {
+        return stpLogic.getLoginDevice();
+    }
+
+
+    // =================== 会话管理 ===================
+
+    /**
+     * 根据条件查询Token
+     * @param keyword 关键字
+     * @param start 开始处索引 (-1代表查询所有)
+     * @param size 获取数量
+     * @return token集合
+     */
+    public static List<String> searchTokenValue(String keyword, int start, int size) {
+        return stpLogic.searchTokenValue(keyword, start, size);
+    }
+
+    /**
+     * 根据条件查询SessionId
+     * @param keyword 关键字
+     * @param start 开始处索引 (-1代表查询所有)
+     * @param size 获取数量
+     * @return sessionId集合
+     */
+    public static List<String> searchSessionId(String keyword, int start, int size) {
+        return stpLogic.searchSessionId(keyword, start, size);
+    }
+
+    /**
+     * 根据条件查询Token专属Session的Id
+     * @param keyword 关键字
+     * @param start 开始处索引 (-1代表查询所有)
+     * @param size 获取数量
+     * @return sessionId集合
+     */
+    public static List<String> searchTokenSessionId(String keyword, int start, int size) {
+        return stpLogic.searchTokenSessionId(keyword, start, size);
+    }
+
+
+    // ------------------- 账号封禁 -------------------
+
+    /**
+     * 封禁指定账号
+     * <p> 此方法不会直接将此账号id踢下线,而是在对方再次登录时抛出`DisableLoginException`异常
+     * @param loginId 指定账号id
+     * @param disableTime 封禁时间, 单位: 秒 (-1=永久封禁)
+     */
+    public static void disable(Object loginId, long disableTime) {
+        stpLogic.disable(loginId, disableTime);
+    }
+
+    /**
+     * 指定账号是否已被封禁 (true=已被封禁, false=未被封禁)
+     * @param loginId 账号id
+     * @return see note
+     */
+    public static boolean isDisable(Object loginId) {
+        return stpLogic.isDisable(loginId);
+    }
+
+    /**
+     * 获取指定账号剩余封禁时间,单位:秒(-1=永久封禁,-2=未被封禁)
+     * @param loginId 账号id
+     * @return see note
+     */
+    public static long getDisableTime(Object loginId) {
+        return stpLogic.getDisableTime(loginId);
+    }
+
+    /**
+     * 解封指定账号
+     * @param loginId 账号id
+     */
+    public static void untieDisable(Object loginId) {
+        stpLogic.untieDisable(loginId);
+    }
+
+
+    // =================== 身份切换 ===================
+
+    /**
+     * 临时切换身份为指定账号id
+     * @param loginId 指定loginId
+     */
+    public static void switchTo(Object loginId) {
+        stpLogic.switchTo(loginId);
+    }
+
+    /**
+     * 结束临时切换身份
+     */
+    public static void endSwitch() {
+        stpLogic.endSwitch();
+    }
+
+    /**
+     * 当前是否正处于[身份临时切换]中
+     * @return 是否正处于[身份临时切换]中
+     */
+    public static boolean isSwitch() {
+        return stpLogic.isSwitch();
+    }
+
+    /**
+     * 在一个代码段里方法内,临时切换身份为指定账号id
+     * @param loginId 指定账号id
+     * @param function 要执行的方法
+     */
+    public static void switchTo(Object loginId, SaFunction function) {
+        stpLogic.switchTo(loginId, function);
+    }
+
+
+    // ------------------- 二级认证 -------------------
+
+    /**
+     * 在当前会话 开启二级认证
+     * @param safeTime 维持时间 (单位: 秒)
+     */
+    public static void openSafe(long safeTime) {
+        stpLogic.openSafe(safeTime);
+    }
+
+    /**
+     * 当前会话 是否处于二级认证时间内
+     * @return true=二级认证已通过, false=尚未进行二级认证或认证已超时
+     */
+    public static boolean isSafe() {
+        return stpLogic.isSafe();
+    }
+
+    /**
+     * 检查当前会话是否已通过二级认证,如未通过则抛出异常
+     */
+    public static void checkSafe() {
+        stpLogic.checkSafe();
+    }
+
+    /**
+     * 获取当前会话的二级认证剩余有效时间 (单位: 秒, 返回-2代表尚未通过二级认证)
+     * @return 剩余有效时间
+     */
+    public static long getSafeTime() {
+        return stpLogic.getSafeTime();
+    }
+
+    /**
+     * 在当前会话 结束二级认证
+     */
+    public static void closeSafe() {
+        stpLogic.closeSafe();
+    }
+
+}

+ 29 - 0
src/main/java/cc/uncarbon/module/sys/util/PwdUtil.java

@@ -0,0 +1,29 @@
+package cc.uncarbon.module.sys.util;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+import lombok.experimental.UtilityClass;
+
+/**
+ * 密码加密工具类
+ * 可以根据自己的业务需要, 修改加密算法(如加盐等)
+ * @author Uncarbon
+ */
+@UtilityClass
+public class PwdUtil {
+    public static String encrypt(String str, String salt) {
+        if (StrUtil.isEmpty(str)){
+            return "";
+        }
+
+        // 第一步: 2次MD5, 这一步可以放前端完成
+        String step1 = SecureUtil.md5(SecureUtil.md5(str));
+
+        // 第二步: 拼接salt
+        String step2 = salt + step1 + salt;
+
+        // 第三步: SHA256
+        return SecureUtil.sha256(step2);
+    }
+
+}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio