Prechádzať zdrojové kódy

feat: 发布1.5.0

* chore: 升级至 Boot 2.6 && feat: 字段注入改为构造器注入

* style: 优化 banner 打印颜色

* wip: 从请求头解析租户信息

* wip: 多租户上下文切换

* wip: 配置迁移

* feat: web 日志记录

* wip: 后台用户登录

* pref: 登录业务优化;API签名调整;方法顺序调整

* wip: 角色、权限放缓存,减少查库

* pref: 角色串还是放上下文里

* wip: 菜单功能优化

* wip: 根据角色ID缓存权限;变更时删除缓存

* wip: 权限单独放在 Redis

* wip: 角色对应菜单放缓存

* refactor: controller 改名 web; 签名顺序调整

* chore: 调节日志级别、Web 访问日志切面开关

* pref: 预留更大的集合容量

* chore: 调整 Web 切面日志配置节

* fix: 启用多租户功能后,部分 SQL 执行不正确

* chore: 日志滚动策略、banner 字体颜色

* chore: 优化 logback 配置 XML

* chore: 优化 logback 配置 XML

* feat: 多租户优化升级

* feat: 去除时区相关
Uncarbon 4 rokov pred
rodič
commit
14b64c96a0
67 zmenil súbory, kde vykonal 1513 pridanie a 973 odobranie
  1. 3 3
      pom.xml
  2. 8 5
      src/main/java/cc/uncarbon/config/AdminSaTokenExtendConfiguration.java
  3. 3 15
      src/main/java/cc/uncarbon/config/CustomInterceptorConfiguration.java
  4. 62 0
      src/main/java/cc/uncarbon/helper/CaptchaHelper.java
  5. 83 0
      src/main/java/cc/uncarbon/helper/RolePermissionCacheHelper.java
  6. 19 11
      src/main/java/cc/uncarbon/interceptor/AdminSaTokenParseInterceptor.java
  7. 2 1
      src/main/java/cc/uncarbon/interceptor/AppSaTokenRouteInterceptor.java
  8. 1 1
      src/main/java/cc/uncarbon/module/app/web/AppAuthController.java
  9. 6 8
      src/main/java/cc/uncarbon/module/sys/aspect/SysLogAspect.java
  10. 15 0
      src/main/java/cc/uncarbon/module/sys/constant/SysConstant.java
  11. 8 0
      src/main/java/cc/uncarbon/module/sys/entity/SysDeptEntity.java
  12. 2 2
      src/main/java/cc/uncarbon/module/sys/entity/SysRoleMenuRelationEntity.java
  13. 2 2
      src/main/java/cc/uncarbon/module/sys/entity/SysUserDeptRelationEntity.java
  14. 2 2
      src/main/java/cc/uncarbon/module/sys/entity/SysUserRoleRelationEntity.java
  15. 3 0
      src/main/java/cc/uncarbon/module/sys/enums/SysErrorEnum.java
  16. 17 0
      src/main/java/cc/uncarbon/module/sys/facade/SysTenantFacade.java
  17. 117 0
      src/main/java/cc/uncarbon/module/sys/facade/impl/SysTenantFacadeImpl.java
  18. 13 4
      src/main/java/cc/uncarbon/module/sys/mapper/SysUserMapper.java
  19. 32 0
      src/main/java/cc/uncarbon/module/sys/model/request/AdminBindRoleMenuRelationDTO.java
  20. 4 5
      src/main/java/cc/uncarbon/module/sys/model/request/AdminBindUserRoleRelationDTO.java
  21. 4 9
      src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysRoleDTO.java
  22. 2 2
      src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysLogDTO.java
  23. 3 12
      src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysMenuDTO.java
  24. 2 2
      src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysRoleDTO.java
  25. 2 2
      src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysTenantDTO.java
  26. 2 2
      src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysUserDTO.java
  27. 10 0
      src/main/java/cc/uncarbon/module/sys/model/request/SysUserLoginDTO.java
  28. 2 2
      src/main/java/cc/uncarbon/module/sys/model/response/SysDataDictBO.java
  29. 2 2
      src/main/java/cc/uncarbon/module/sys/model/response/SysDeptBO.java
  30. 1 1
      src/main/java/cc/uncarbon/module/sys/model/response/SysLogBO.java
  31. 5 10
      src/main/java/cc/uncarbon/module/sys/model/response/SysMenuBO.java
  32. 2 2
      src/main/java/cc/uncarbon/module/sys/model/response/SysParamBO.java
  33. 7 7
      src/main/java/cc/uncarbon/module/sys/model/response/SysRoleBO.java
  34. 2 2
      src/main/java/cc/uncarbon/module/sys/model/response/SysTenantBO.java
  35. 4 15
      src/main/java/cc/uncarbon/module/sys/model/response/SysUserBO.java
  36. 17 13
      src/main/java/cc/uncarbon/module/sys/model/response/SysUserLoginBO.java
  37. 38 0
      src/main/java/cc/uncarbon/module/sys/model/response/SysUserLoginVO.java
  38. 1 1
      src/main/java/cc/uncarbon/module/sys/model/response/VbenAdminMenuMetaVO.java
  39. 1 1
      src/main/java/cc/uncarbon/module/sys/model/response/VbenAdminUserInfoVO.java
  40. 41 15
      src/main/java/cc/uncarbon/module/sys/service/SysDataDictService.java
  41. 63 35
      src/main/java/cc/uncarbon/module/sys/service/SysDeptService.java
  42. 40 16
      src/main/java/cc/uncarbon/module/sys/service/SysLogService.java
  43. 167 230
      src/main/java/cc/uncarbon/module/sys/service/SysMenuService.java
  44. 51 21
      src/main/java/cc/uncarbon/module/sys/service/SysParamService.java
  45. 83 26
      src/main/java/cc/uncarbon/module/sys/service/SysRoleMenuRelationService.java
  46. 84 39
      src/main/java/cc/uncarbon/module/sys/service/SysRoleService.java
  47. 62 129
      src/main/java/cc/uncarbon/module/sys/service/SysTenantService.java
  48. 1 1
      src/main/java/cc/uncarbon/module/sys/service/SysUserDeptRelationService.java
  49. 6 22
      src/main/java/cc/uncarbon/module/sys/service/SysUserRoleRelationService.java
  50. 156 111
      src/main/java/cc/uncarbon/module/sys/service/SysUserService.java
  51. 48 22
      src/main/java/cc/uncarbon/module/sys/web/auth/AdminAuthController.java
  52. 4 4
      src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysDataDictController.java
  53. 4 4
      src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysDeptController.java
  54. 4 5
      src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysLogController.java
  55. 18 51
      src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysMenuController.java
  56. 4 4
      src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysParamController.java
  57. 32 31
      src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysRoleController.java
  58. 8 5
      src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysTenantController.java
  59. 10 10
      src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysUserController.java
  60. 4 0
      src/main/resources/application-dev.yml
  61. 6 3
      src/main/resources/application-prod.yml
  62. 5 1
      src/main/resources/application-test.yml
  63. 18 17
      src/main/resources/application.yml
  64. 1 1
      src/main/resources/banner.txt
  65. 31 13
      src/main/resources/logback-spring.xml
  66. 15 13
      src/main/resources/mapper/SysUserMapper.xml
  67. 38 0
      src/test/java/cc/uncarbon/test/InjectUnitTest.java

+ 3 - 3
pom.xml

@@ -7,17 +7,17 @@
     <parent>
         <groupId>cc.uncarbon.framework</groupId>
         <artifactId>helio-starters</artifactId>
-        <version>1.4.0</version>
+        <version>1.5.0-SNAPSHOT</version>
     </parent>
 
     <artifactId>helio-boot</artifactId>
-    <version>1.4.0</version>
+    <version>1.5.0-SNAPSHOT</version>
 
     <properties>
     </properties>
 
     <dependencies>
-        <!-- 脚手架starter -->
+        <!-- 脚手架 starters -->
         <dependency>
             <groupId>cc.uncarbon.framework</groupId>
             <artifactId>helio-core</artifactId>

+ 8 - 5
src/main/java/cc/uncarbon/config/AdminSaTokenExtendConfiguration.java

@@ -1,12 +1,11 @@
 package cc.uncarbon.config;
 
 import cc.uncarbon.framework.core.context.UserContextHolder;
+import cc.uncarbon.helper.RolePermissionCacheHelper;
 import cn.dev33.satoken.stp.StpInterface;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.stereotype.Component;
-
-import javax.annotation.Resource;
 import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
 
 
 /**
@@ -16,14 +15,18 @@ import java.util.List;
  * @author Uncarbon
  */
 @Component
+@RequiredArgsConstructor
 public class AdminSaTokenExtendConfiguration implements StpInterface {
 
+    private final RolePermissionCacheHelper rolePermissionCacheHelper;
+
+
     /**
      * 返回一个账号所拥有的权限码集合
      */
     @Override
     public List<String> getPermissionList(Object loginId, String loginType) {
-        return UserContextHolder.getUserContext().getPermissions();
+        return rolePermissionCacheHelper.getUserPermissions();
     }
 
     /**

+ 3 - 15
src/main/java/cc/uncarbon/config/CustomInterceptorConfiguration.java

@@ -7,12 +7,11 @@ 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 lombok.RequiredArgsConstructor;
 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;
-
 
 /**
  * 将自定义拦截器加入到拦截器队列中
@@ -20,10 +19,10 @@ import javax.annotation.Resource;
  * @author Uncarbon
  */
 @Configuration
+@RequiredArgsConstructor
 public class CustomInterceptorConfiguration implements WebMvcConfigurer {
 
-    @Resource
-    private HelioProperties helioProperties;
+    private final HelioProperties helioProperties;
 
 
     @Override
@@ -39,17 +38,6 @@ public class CustomInterceptorConfiguration implements WebMvcConfigurer {
                 .addInterceptor(new AdminSaTokenParseInterceptor())
                 .addPathPatterns(SysConstant.SYS_MODULE_CONTEXT_PATH + "/**");
 
-        /*
-        可选拦截器 - 从请求头解析租户信息
-        其他家后台管理登录时,账号上面可以手动填入租户ID,如"000000"
-        这个拦截器就是为了满足这种需要
-
-        注:若使用该拦截器,cc.uncarbon.module.sys.service.SysUserService#adminLogin 处的“主动清空用户上下文”代码需要删除
-         */
-        /* registry
-                .addInterceptor(new DefaultTenantParseInterceptor())
-                .addPathPatterns("/**");*/
-
         /*
         2. App-路由拦截器, 使几乎所有接口都需要登录
         放行接口请在配置文件的 helio.security.exclude-routes 中设置

+ 62 - 0
src/main/java/cc/uncarbon/helper/CaptchaHelper.java

@@ -0,0 +1,62 @@
+package cc.uncarbon.helper;
+
+import cn.hutool.captcha.CaptchaUtil;
+import cn.hutool.captcha.ShearCaptcha;
+import cn.hutool.core.util.StrUtil;
+import java.util.concurrent.TimeUnit;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+/**
+ * 验证码助手类;可将验证码答案缓存至 Redis
+ *
+ * @author Uncarbon
+ */
+@Component
+@RequiredArgsConstructor
+public class CaptchaHelper {
+
+    private final RedisTemplate<String, String> stringRedisTemplate;
+
+    private static final String CACHE_KEY_CAPTCHA_ANSWER = "Authorization:captcha:uuid_%s";
+
+
+    /**
+     * 生成一个验证码图片对象
+     *
+     * @param uuid UUID
+     * @return ShearCaptcha
+     */
+    public ShearCaptcha generate(String uuid) {
+        // 定义图形验证码的长、宽、验证码字符数、干扰线宽度
+        ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(196, 50, 4, 4);
+
+        // 将验证码答案保存至 redis , 有效期5分钟
+        stringRedisTemplate.opsForValue().set(String.format(CACHE_KEY_CAPTCHA_ANSWER, uuid), captcha.getCode(), 300, TimeUnit.SECONDS);
+
+        return captcha;
+    }
+
+    /**
+     * 校验验证码是否输入正确
+     *
+     * @param uuid          UUID
+     * @param captchaAnswer 验证码答案
+     * @return 是否正确
+     */
+    public boolean validate(String uuid, String captchaAnswer, boolean removeWhenEquals) {
+        if (StrUtil.hasBlank(uuid, captchaAnswer)) {
+            return false;
+        }
+
+        String cacheKey = String.format(CACHE_KEY_CAPTCHA_ANSWER, uuid);
+        String answerInRedis = stringRedisTemplate.opsForValue().get(cacheKey);
+        boolean equals = StrUtil.equalsIgnoreCase(answerInRedis, captchaAnswer);
+        if (equals && removeWhenEquals) {
+            stringRedisTemplate.delete(cacheKey);
+        }
+
+        return equals;
+    }
+}

+ 83 - 0
src/main/java/cc/uncarbon/helper/RolePermissionCacheHelper.java

@@ -0,0 +1,83 @@
+package cc.uncarbon.helper;
+
+import cc.uncarbon.framework.core.context.UserContextHolder;
+import cn.hutool.core.collection.CollUtil;
+
+import java.util.*;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+/**
+ * 将角色对应权限,缓存至 Redis
+ * 参考文章: https://sa-token.dev33.cn/doc/index.html#/fun/jur-cache
+ *
+ * @author Uncarbon
+ */
+@Component
+@RequiredArgsConstructor
+public class RolePermissionCacheHelper {
+
+    private final RedisTemplate<String, Set<String>> stringSetRedisTemplate;
+
+    private static final String CACHE_KEY_ROLE_PERMISSIONS = "Authorization:rolePermissions:roleId_%s";
+
+
+    /**
+     * 从缓存中取得当前用户拥有的所有权限名集合
+     *
+     * @return List<String>
+     */
+    public List<String> getUserPermissions() {
+        Set<Long> rolesIds = UserContextHolder.getUserContext().getRolesIds();
+        // aka * 64
+        List<String> ret = new ArrayList<>(rolesIds.size() << 6);
+
+        rolesIds.forEach(
+                roleId -> {
+                    String cacheKey = String.format(CACHE_KEY_ROLE_PERMISSIONS, roleId);
+                    ret.addAll(CollUtil.emptyIfNull(stringSetRedisTemplate.opsForValue().get(cacheKey)));
+                }
+        );
+
+        return ret;
+    }
+
+    /**
+     * 覆盖更新角色对应权限至 Redis
+     *
+     * @param map key=角色ID value=权限集合
+     */
+    public void putCache(Map<Long, Set<String>> map) {
+        Set<Map.Entry<Long, Set<String>>> entries = map.entrySet();
+        entries.forEach(
+                entry -> this.putCache(entry.getKey(), entry.getValue())
+        );
+    }
+
+    /**
+     * 覆盖更新角色对应权限至 Redis
+     *
+     * @param roleId 角色ID
+     * @param newPermissions 新权限名集合
+     */
+    public void putCache(Long roleId, Set<String> newPermissions) {
+        String cacheKey = String.format(CACHE_KEY_ROLE_PERMISSIONS, roleId);
+        stringSetRedisTemplate.opsForValue().set(cacheKey, newPermissions);
+    }
+
+    /**
+     * 删除角色ID对应的权限缓存
+     *
+     * @param roleIds 角色ID集合
+     */
+    public void deleteCache(Collection<Long> roleIds) {
+        roleIds.forEach(
+                roleId -> {
+                    String cacheKey = String.format(CACHE_KEY_ROLE_PERMISSIONS, roleId);
+                    stringSetRedisTemplate.delete(cacheKey);
+                }
+        );
+    }
+}

+ 19 - 11
src/main/java/cc/uncarbon/interceptor/AdminSaTokenParseInterceptor.java

@@ -1,16 +1,18 @@
 package cc.uncarbon.interceptor;
 
+import cc.uncarbon.framework.core.context.TenantContext;
+import cc.uncarbon.framework.core.context.TenantContextHolder;
 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 javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import lombok.NonNull;
 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;
-
 
 /**
  * 从请求头解析并赋值到用户上下文
@@ -21,24 +23,30 @@ import javax.servlet.http.HttpServletResponse;
 public class AdminSaTokenParseInterceptor implements AsyncHandlerInterceptor {
 
     @Override
-    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
 
         if (handler instanceof ResourceHttpRequestHandler) {
             // 直接放行静态资源
             return true;
         }
 
-        // 从请求头解析用户上下文
+        // SA-Token 会自动从请求头中解析 token,所以这里可以直接拿到对应 session,从而取出业务字段
         if (AdminStpUtil.isLogin()) {
-            UserContext currentUser = (UserContext) AdminStpUtil.getSession().get(UserContext.CAMEL_NAME);
-            log.debug("[SA-Token][Admin] 从请求头解析出用户上下文 >> {}", currentUser);
+            UserContext userContext = (UserContext) AdminStpUtil.getSession().get(UserContext.CAMEL_NAME);
+
+            // 获取用户公网IP
+            userContext.setClientIP(IPUtil.getClientIPAddress(request));
+            UserContextHolder.setUserContext(userContext);
+
+            log.debug("[SA-Token][Admin] 从请求头解析出用户上下文 >> {}", userContext);
+
+            // 赋值租户上下文
+            TenantContext tenantContext = (TenantContext) AdminStpUtil.getSession().get(TenantContext.CAMEL_NAME);
+            TenantContextHolder.setTenantContext(tenantContext);
 
-            currentUser
-                    .setClientIP(IPUtil.getClientIPAddress(request))
-            ;
-            UserContextHolder.setUserContext(currentUser);
         } else {
             UserContextHolder.setUserContext(null);
+            TenantContextHolder.setTenantContext(null);
         }
 
         return true;

+ 2 - 1
src/main/java/cc/uncarbon/interceptor/AppSaTokenRouteInterceptor.java

@@ -5,6 +5,7 @@ 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.NonNull;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.servlet.ModelAndView;
 
@@ -24,7 +25,7 @@ public class AppSaTokenRouteInterceptor extends SaRouteInterceptor {
 
 
     @Override
-    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
+    public void postHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, ModelAndView modelAndView) {
         /*
         App登录验证
         这里是按路由匹配,除了配置文件中指定的URI外,其他URI都需要登录才行

+ 1 - 1
src/main/java/cc/uncarbon/module/app/controller/AppAuthController.java → src/main/java/cc/uncarbon/module/app/web/AppAuthController.java

@@ -1,4 +1,4 @@
-package cc.uncarbon.module.app.controller;
+package cc.uncarbon.module.app.web;
 
 
 import cc.uncarbon.framework.core.constant.HelioConstant;

+ 6 - 8
src/main/java/cc/uncarbon/module/sys/aspect/SysLogAspect.java

@@ -6,7 +6,9 @@ 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 java.util.Arrays;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.annotation.Around;
@@ -16,10 +18,6 @@ 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
@@ -27,10 +25,10 @@ import java.util.stream.Collectors;
 @Aspect
 @Component
 @Slf4j
+@RequiredArgsConstructor
 public class SysLogAspect {
 
-    @Resource
-    private SysLogService sysLogService;
+    private final SysLogService sysLogService;
 
 
     @Pointcut("@annotation(cc.uncarbon.module.sys.annotation.SysLog)")
@@ -62,7 +60,7 @@ public class SysLogAspect {
         sysLogEntity.setParams(Arrays.stream(point.getArgs()).map(JSONUtil::toJsonStr).collect(Collectors.joining(",")));
 
         // IP地址
-        sysLogEntity.setIp(UserContextHolder.getUserContext().getClientIP());
+        sysLogEntity.setIp(UserContextHolder.getClientIP());
 
         // 状态
         sysLogEntity.setStatus(SysLogStatusEnum.SUCCESS);

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

@@ -48,4 +48,19 @@ public interface SysConstant {
      */
     String PARAM_KEY_CACHE_MENU_DURATION = "sys:cache-menu-duration";
 
+    /**
+     * 缓存键名前缀-用户业务缓存
+     */
+    String CACHE_KEY_USER_BIZ_CACHE = "sys-service:userBizCache:%s";
+
+    /**
+     * 缓存键名-用户拥有角色ID
+     */
+    String REDIS_KEY_USER_OWNED_ROLE_IDS = CACHE_KEY_USER_BIZ_CACHE + ":ownedRoleIds";
+
+    /**
+     * 一整天有多少秒
+     */
+    long ONE_DAY_SECONDS = 86400;
+
 }

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

@@ -26,6 +26,14 @@ import lombok.experimental.SuperBuilder;
 @TableName(value = "sys_dept")
 public class SysDeptEntity extends HelioBaseEntity<Long> {
 
+	/**
+	 * 乐观锁
+	 * 需自行加@Version注解才有效
+	 */
+	@ApiModelProperty(value = "乐观锁", notes = "需再次复制本字段,并自行加 @Version 注解才有效")
+	@TableField(value = "revision", exist = false)
+	private Long revision;
+
 	@ApiModelProperty(value = "名称")
 	@TableField(value = "title")
 	private String title;

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

@@ -1,6 +1,6 @@
 package cc.uncarbon.module.sys.entity;
 
-import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import cc.uncarbon.framework.crud.entity.HelioNoTenantBaseEntity;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableName;
 import io.swagger.annotations.ApiModelProperty;
@@ -23,7 +23,7 @@ import lombok.experimental.SuperBuilder;
 @NoArgsConstructor
 @Data
 @TableName(value = "sys_role_menu_relation")
-public class SysRoleMenuRelationEntity extends HelioBaseEntity<Long> {
+public class SysRoleMenuRelationEntity extends HelioNoTenantBaseEntity<Long> {
 
 	@ApiModelProperty(value = "角色ID")
 	@TableField(value = "role_id")

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

@@ -1,6 +1,6 @@
 package cc.uncarbon.module.sys.entity;
 
-import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import cc.uncarbon.framework.crud.entity.HelioNoTenantBaseEntity;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableName;
 import io.swagger.annotations.ApiModelProperty;
@@ -23,7 +23,7 @@ import lombok.experimental.SuperBuilder;
 @NoArgsConstructor
 @Data
 @TableName(value = "sys_user_dept_relation")
-public class SysUserDeptRelationEntity extends HelioBaseEntity<Long> {
+public class SysUserDeptRelationEntity extends HelioNoTenantBaseEntity<Long> {
 
 	@ApiModelProperty(value = "用户ID")
 	@TableField(value = "user_id")

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

@@ -1,6 +1,6 @@
 package cc.uncarbon.module.sys.entity;
 
-import cc.uncarbon.framework.crud.entity.HelioBaseEntity;
+import cc.uncarbon.framework.crud.entity.HelioNoTenantBaseEntity;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableName;
 import io.swagger.annotations.ApiModelProperty;
@@ -23,7 +23,7 @@ import lombok.experimental.SuperBuilder;
 @NoArgsConstructor
 @Data
 @TableName(value = "sys_user_role_relation")
-public class SysUserRoleRelationEntity extends HelioBaseEntity<Long> {
+public class SysUserRoleRelationEntity extends HelioNoTenantBaseEntity<Long> {
 
 	@ApiModelProperty(value = "用户ID")
 	@TableField(value = "user_id")

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

@@ -30,6 +30,9 @@ public enum SysErrorEnum implements HelioBaseEnum<Integer> {
     NO_ROLE_AVAILABLE_FOR_CURRENT_USER(400, "当前用户没有可用角色"),
 
     NO_MENU_AVAILABLE_FOR_CURRENT_ROLE(400, "当前角色没有可用菜单"),
+
+    UUID_CANNOT_BE_BLANK(400, "UUID不能为空"),
+
     ;
 
     private final Integer value;

+ 17 - 0
src/main/java/cc/uncarbon/module/sys/facade/SysTenantFacade.java

@@ -0,0 +1,17 @@
+package cc.uncarbon.module.sys.facade;
+
+import cc.uncarbon.module.sys.model.request.AdminInsertSysTenantDTO;
+
+/**
+ * 系统租户防腐层,用于解决循环依赖
+ *
+ * @author Uncarbon
+ */
+public interface SysTenantFacade {
+
+    /**
+     * 后台管理-新增
+     */
+    Long adminInsert(AdminInsertSysTenantDTO dto);
+
+}

+ 117 - 0
src/main/java/cc/uncarbon/module/sys/facade/impl/SysTenantFacadeImpl.java

@@ -0,0 +1,117 @@
+package cc.uncarbon.module.sys.facade.impl;
+
+import cc.uncarbon.framework.core.context.UserContextHolder;
+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.facade.SysTenantFacade;
+import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysRoleDTO;
+import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysUserDTO;
+import cc.uncarbon.module.sys.model.request.AdminInsertSysTenantDTO;
+import cc.uncarbon.module.sys.service.SysRoleService;
+import cc.uncarbon.module.sys.service.SysTenantService;
+import cc.uncarbon.module.sys.service.SysUserRoleRelationService;
+import cc.uncarbon.module.sys.service.SysUserService;
+import cn.hutool.core.bean.BeanUtil;
+import java.time.LocalDateTime;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * 系统租户防腐层,用于解决循环依赖
+ *
+ * @author Uncarbon
+ */
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class SysTenantFacadeImpl implements SysTenantFacade {
+
+    private final SysTenantService sysTenantService;
+
+    private final SysRoleService sysRoleService;
+
+    private final SysUserRoleRelationService sysUserRoleRelationService;
+
+    private final SysUserService sysUserService;
+
+
+    @Override
+    @SysLog(value = "新增系统租户")
+    @Transactional(rollbackFor = Exception.class)
+    public Long adminInsert(AdminInsertSysTenantDTO dto) {
+        log.info("[后台管理-新增系统租户] >> 入参={}", dto);
+        sysTenantService.checkExistence(dto);
+
+        /*
+        1. 加入一个新租户(tenant)
+        这里是直接顺带创建管理员账号了, 你可以根据业务需要决定是否创建
+         */
+
+        dto.setId(null);
+        SysTenantEntity entity = new SysTenantEntity();
+        BeanUtil.copyProperties(dto, entity);
+
+        sysTenantService.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. 把管理员账号更新进库
+         */
+        sysTenantService.updateById(entity);
+
+        return newTenantId;
+    }
+
+}

+ 13 - 4
src/main/java/cc/uncarbon/module/sys/mapper/SysUserMapper.java

@@ -5,8 +5,7 @@ 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;
+import org.apache.ibatis.annotations.Param;
 
 /**
  * 后台用户
@@ -16,10 +15,20 @@ import java.time.LocalDateTime;
 @Mapper
 public interface SysUserMapper extends BaseMapper<SysUserEntity> {
 
+    /**
+     * 取用户实体,忽略行级租户拦截器
+     * @param pin 账号
+     * @return SysUserEntity
+     */
     @InterceptorIgnore(tenantLine = "true")
-    void updateLastLoginAt(Long userId, LocalDateTime lastLoginAt);
+    SysUserEntity getUserByPin(@Param(value = "pin") String pin);
 
+    /**
+     * 取用户基本信息,忽略行级租户拦截器
+     * @param userId 用户ID
+     * @return SysUserBaseInfoBO
+     */
     @InterceptorIgnore(tenantLine = "true")
-    SysUserBaseInfoBO getBaseInfoByUserId(Long userId);
+    SysUserBaseInfoBO getBaseInfoByUserId(@Param(value = "userId") Long userId);
 
 }

+ 32 - 0
src/main/java/cc/uncarbon/module/sys/model/request/AdminBindRoleMenuRelationDTO.java

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

+ 4 - 5
src/main/java/cc/uncarbon/module/sys/model/request/AdminBindUserRoleRelationDTO.java

@@ -1,16 +1,15 @@
 package cc.uncarbon.module.sys.model.request;
 
 import io.swagger.annotations.ApiModelProperty;
+import java.io.Serializable;
+import java.util.Set;
+import javax.validation.constraints.NotNull;
 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;
-
 
 /**
  * 后台管理-绑定用户与角色关联关系
@@ -28,6 +27,6 @@ public class AdminBindUserRoleRelationDTO implements Serializable {
     private Long userId;
 
     @ApiModelProperty(value = "角色Ids(空=清理关联关系后不再绑定任何角色)")
-    private List<Long> roleIds;
+    private Set<Long> roleIds;
 
 }

+ 4 - 9
src/main/java/cc/uncarbon/module/sys/model/request/AdminInsertOrUpdateSysRoleDTO.java

@@ -1,16 +1,14 @@
 package cc.uncarbon.module.sys.model.request;
 
 import io.swagger.annotations.ApiModelProperty;
+import java.io.Serializable;
+import javax.validation.constraints.NotBlank;
 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;
-
 
 /**
  * 后台管理-新增/编辑后台角色
@@ -28,16 +26,13 @@ public class AdminInsertOrUpdateSysRoleDTO implements Serializable {
 
     @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;
-
 }

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

@@ -36,12 +36,12 @@ public class AdminListSysLogDTO implements Serializable {
     private SysLogStatusEnum status;
 
     @ApiModelProperty(value = "时间区间起")
-    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     @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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     private LocalDateTime endAt;
 

+ 3 - 12
src/main/java/cc/uncarbon/module/sys/model/request/AdminListSysMenuDTO.java

@@ -1,16 +1,13 @@
 package cc.uncarbon.module.sys.model.request;
 
-import cc.uncarbon.module.sys.enums.SysMenuTypeEnum;
 import io.swagger.annotations.ApiModelProperty;
+import java.io.Serializable;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 import lombok.experimental.Accessors;
 
-import java.io.Serializable;
-import java.util.List;
-
 
 /**
  * 后台管理-列表后台菜单
@@ -23,13 +20,7 @@ import java.util.List;
 @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;
+    @ApiModelProperty(value = "保留")
+    private String reserved;
 
 }

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

@@ -32,12 +32,12 @@ public class AdminListSysRoleDTO implements Serializable {
     private String value;
 
     @ApiModelProperty(value = "时间区间起")
-    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     @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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     private LocalDateTime endAt;
 

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

@@ -36,12 +36,12 @@ public class AdminListSysTenantDTO implements Serializable {
     private GenericStatusEnum status;
 
     @ApiModelProperty(value = "时间区间起")
-    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     @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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     private LocalDateTime endAt;
 

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

@@ -29,12 +29,12 @@ public class AdminListSysUserDTO implements Serializable {
     private String phoneNo;
 
     @ApiModelProperty(value = "时间区间起")
-    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     @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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     private LocalDateTime endAt;
 

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

@@ -15,6 +15,7 @@ import java.io.Serializable;
 
 /**
  * 后台管理-后台用户登录
+ *
  * @author Uncarbon
  */
 @Accessors(chain = true)
@@ -38,4 +39,13 @@ public class SysUserLoginDTO implements Serializable {
     @NotNull(message = "记住我不能为空")
     private Boolean rememberMe;
 
+    @ApiModelProperty(value = "租户ID(可选,启用多租户后有效)")
+    private Long tenantId;
+
+    @ApiModelProperty(value = "验证码图片UUID(可选,需自行对接业务逻辑)")
+    private String captchaUUID;
+
+    @ApiModelProperty(value = "验证码答案(可选,需自行对接业务逻辑)")
+    private String captchaAnswer;
+
 }

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

@@ -33,12 +33,12 @@ public class SysDataDictBO 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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     private LocalDateTime updatedAt;
 
     @ApiModelProperty(value = "驼峰式键名")

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

@@ -34,12 +34,12 @@ public class SysDeptBO 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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     private LocalDateTime updatedAt;
 
     @ApiModelProperty(value = "名称")

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

@@ -30,7 +30,7 @@ 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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     private LocalDateTime createdAt;
 
     @ApiModelProperty(value = "用户账号")

+ 5 - 10
src/main/java/cc/uncarbon/module/sys/model/response/SysMenuBO.java

@@ -6,6 +6,8 @@ import cc.uncarbon.module.sys.enums.SysMenuTypeEnum;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
+import java.io.Serializable;
+import java.time.LocalDateTime;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
@@ -13,10 +15,6 @@ 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
@@ -35,12 +33,12 @@ public class SysMenuBO 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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     private LocalDateTime updatedAt;
 
     @ApiModelProperty(value = "名称")
@@ -70,9 +68,6 @@ public class SysMenuBO implements Serializable {
     @ApiModelProperty(value = "外链地址")
     private String externalLink;
 
-    @ApiModelProperty(value = "子项数据")
-    private List<SysMenuBO> children;
-
     @ApiModelProperty(value = "【用于Vben Admin】路由地址", hidden = true)
     private String path;
 
@@ -80,7 +75,7 @@ public class SysMenuBO implements Serializable {
     private String name;
 
     @ApiModelProperty(value = "【用于Vben Admin】菜单详情", hidden = true)
-    private VbenAdminMenuMetaBO meta;
+    private VbenAdminMenuMetaVO meta;
 
     @ApiModelProperty(value = "【用于Vben Admin树状菜单】菜单主键ID字符串", hidden = true)
     private String idStr;

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

@@ -32,12 +32,12 @@ public class SysParamBO 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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     private LocalDateTime updatedAt;
 
     @ApiModelProperty(value = "键名")

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

@@ -13,7 +13,7 @@ import org.springframework.format.annotation.DateTimeFormat;
 
 import java.io.Serializable;
 import java.time.LocalDateTime;
-import java.util.List;
+import java.util.Collection;
 
 
 /**
@@ -33,21 +33,21 @@ public class SysRoleBO 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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     private LocalDateTime updatedAt;
-        
+
     @ApiModelProperty(value = "名称")
     private String title;
-        
+
     @ApiModelProperty(value = "值")
     private String value;
 
     @ApiModelProperty(value = "可见菜单Ids")
-    private List<Long> menuIds;
-    
+    private Collection<Long> menuIds;
+
 }

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

@@ -33,12 +33,12 @@ public class SysTenantBO 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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     private LocalDateTime updatedAt;
 
     @ApiModelProperty(value = "备注")

+ 4 - 15
src/main/java/cc/uncarbon/module/sys/model/response/SysUserBO.java

@@ -6,6 +6,8 @@ import cc.uncarbon.module.sys.enums.SysUserStatusEnum;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
+import java.io.Serializable;
+import java.time.LocalDateTime;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
@@ -13,10 +15,6 @@ 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
@@ -35,12 +33,12 @@ public class SysUserBO 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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     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)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     private LocalDateTime updatedAt;
 
     @ApiModelProperty(value = "账号")
@@ -64,15 +62,6 @@ public class SysUserBO implements Serializable {
     @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;
 

+ 17 - 13
src/main/java/cc/uncarbon/module/sys/model/response/SysUserLoginBO.java

@@ -2,23 +2,24 @@ package cc.uncarbon.module.sys.model.response;
 
 import cc.uncarbon.framework.core.context.TenantContext;
 import io.swagger.annotations.ApiModelProperty;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import lombok.AllArgsConstructor;
-import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 import lombok.experimental.Accessors;
-
-import java.io.Serializable;
-import java.util.List;
+import lombok.experimental.SuperBuilder;
 
 
 /**
- * 后台用户BO
- * 用于登录后返回字段
+ * 登录后返回的字段
+ * 用于内部 RPC 调用
  * @author Uncarbon
  */
 @Accessors(chain = true)
-@Builder
+@SuperBuilder
 @AllArgsConstructor
 @NoArgsConstructor
 @Data
@@ -37,15 +38,18 @@ public class SysUserLoginBO implements Serializable {
     private String phoneNo;
 
     @ApiModelProperty(value = "对应角色ID")
-    private List<Long> roleIds;
+    private Set<Long> roleIds;
 
-    @ApiModelProperty(value = "对应角色")
+    @ApiModelProperty(value = "对应角色")
     private List<String> roles;
 
-    @ApiModelProperty(value = "拥有权限")
-    private List<String> permissions;
+    @ApiModelProperty(value = "所有拥有权限名")
+    private Set<String> permissions;
+
+    @ApiModelProperty(value = "角色ID-对应权限名 Map")
+    private Map<Long, Set<String>> roleIdPermissionMap;
 
-    @ApiModelProperty(value = "所属租户")
-    private TenantContext relationalTenant;
+    @ApiModelProperty(value = "关联租户上下文")
+    private TenantContext tenantContext;
 
 }

+ 38 - 0
src/main/java/cc/uncarbon/module/sys/model/response/SysUserLoginVO.java

@@ -0,0 +1,38 @@
+package cc.uncarbon.module.sys.model.response;
+
+import io.swagger.annotations.ApiModelProperty;
+import java.io.Serializable;
+import java.util.Collection;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+
+/**
+ * 登录后返回的字段
+ * 用于返回给前端
+ *
+ * @author Uncarbon
+ */
+@Accessors(chain = true)
+@SuperBuilder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class SysUserLoginVO implements Serializable {
+
+    @ApiModelProperty(value = "token名称")
+    private String tokenName;
+
+    @ApiModelProperty(value = "token值")
+    private String tokenValue;
+
+    @ApiModelProperty(value = "对应角色")
+    private Collection<String> roles;
+
+    @ApiModelProperty(value = "拥有权限")
+    private Collection<String> permissions;
+
+}

+ 1 - 1
src/main/java/cc/uncarbon/module/sys/model/response/VbenAdminMenuMetaBO.java → src/main/java/cc/uncarbon/module/sys/model/response/VbenAdminMenuMetaVO.java

@@ -21,7 +21,7 @@ import java.io.Serializable;
 @AllArgsConstructor
 @NoArgsConstructor
 @Data
-public class VbenAdminMenuMetaBO implements Serializable {
+public class VbenAdminMenuMetaVO implements Serializable {
 
     @ApiModelProperty(value = "标题")
     private String title;

+ 1 - 1
src/main/java/cc/uncarbon/module/sys/model/response/VbenAdminUserInfoBO.java → src/main/java/cc/uncarbon/module/sys/model/response/VbenAdminUserInfoVO.java

@@ -22,7 +22,7 @@ import java.time.LocalDateTime;
 @AllArgsConstructor
 @NoArgsConstructor
 @Data
-public class VbenAdminUserInfoBO implements Serializable {
+public class VbenAdminUserInfoVO implements Serializable {
 
     @ApiModelProperty(value = "账号")
     private String username;

+ 41 - 15
src/main/java/cc/uncarbon/module/sys/service/SysDataDictService.java

@@ -13,12 +13,15 @@ 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.collection.CollUtil;
 import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -31,6 +34,7 @@ import org.springframework.transaction.annotation.Transactional;
  */
 @Slf4j
 @Service
+@RequiredArgsConstructor
 public class SysDataDictService extends HelioBaseServiceImpl<SysDataDictMapper, SysDataDictEntity> {
 
     /**
@@ -51,24 +55,24 @@ public class SysDataDictService extends HelioBaseServiceImpl<SysDataDictMapper,
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @deprecated 使用 getOneById(java.lang.Long, boolean) 替代
+     * @param id 主键ID
+     * @return null or BO
      */
-    @Deprecated
-    public SysDataDictBO getOneById(Long entityId) throws BusinessException {
-       return this.getOneById(entityId, true);
+    public SysDataDictBO getOneById(Long id) {
+        return this.getOneById(id, false);
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @param entityId 实体类主键ID
+     * @param id 主键ID
      * @param throwIfInvalidId 是否在 ID 无效时抛出异常
      * @return null or BO
      */
-    public SysDataDictBO getOneById(Long entityId, boolean throwIfInvalidId) throws BusinessException {
-        SysDataDictEntity entity = this.getById(entityId);
+    public SysDataDictBO getOneById(Long id, boolean throwIfInvalidId) throws BusinessException {
+        SysDataDictEntity entity = this.getById(id);
         if (throwIfInvalidId) {
             SysErrorEnum.INVALID_ID.assertNotNull(entity);
         }
@@ -82,7 +86,7 @@ public class SysDataDictService extends HelioBaseServiceImpl<SysDataDictMapper,
     @SysLog(value = "新增数据字典")
     @Transactional(rollbackFor = Exception.class)
     public Long adminInsert(AdminInsertOrUpdateSysDataDictDTO dto) {
-        log.info("[后台管理-新增数据字典] >> DTO={}", dto);
+        log.info("[后台管理-新增数据字典] >> 入参={}", dto);
         this.checkExistence(dto);
 
         dto.setId(null);
@@ -100,7 +104,7 @@ public class SysDataDictService extends HelioBaseServiceImpl<SysDataDictMapper,
     @SysLog(value = "编辑数据字典")
     @Transactional(rollbackFor = Exception.class)
     public void adminUpdate(AdminInsertOrUpdateSysDataDictDTO dto) {
-        log.info("[后台管理-编辑数据字典] >> DTO={}", dto);
+        log.info("[后台管理-编辑数据字典] >> 入参={}", dto);
         this.checkExistence(dto);
 
         SysDataDictEntity entity = new SysDataDictEntity();
@@ -115,16 +119,22 @@ public class SysDataDictService extends HelioBaseServiceImpl<SysDataDictMapper,
     @SysLog(value = "删除数据字典")
     @Transactional(rollbackFor = Exception.class)
     public void adminDelete(Collection<Long> ids) {
-        log.info("[后台管理-删除数据字典] >> ids={}", ids);
+        log.info("[后台管理-删除数据字典] >> 入参={}", ids);
         this.removeByIds(ids);
     }
 
-
     /*
-    私有方法
-    ------------------------------------------------------------------------------------------------
+    ----------------------------------------------------------------
+                        私有方法 private methods
+    ----------------------------------------------------------------
      */
 
+    /**
+     * 实体转 BO
+     *
+     * @param entity 实体
+     * @return BO
+     */
     private SysDataDictBO entity2BO(SysDataDictEntity entity) {
         if (entity == null) {
             return null;
@@ -138,7 +148,17 @@ public class SysDataDictService extends HelioBaseServiceImpl<SysDataDictMapper,
         return bo;
     }
 
+    /**
+     * 实体 List 转 BO List
+     *
+     * @param entityList 实体 List
+     * @return BO List
+     */
     private List<SysDataDictBO> entityList2BOs(List<SysDataDictEntity> entityList) {
+        if (CollUtil.isEmpty(entityList)) {
+            return Collections.emptyList();
+        }
+
         // 深拷贝
         List<SysDataDictBO> ret = new ArrayList<>(entityList.size());
         entityList.forEach(
@@ -148,6 +168,12 @@ public class SysDataDictService extends HelioBaseServiceImpl<SysDataDictMapper,
         return ret;
     }
 
+    /**
+     * 实体分页转 BO 分页
+     *
+     * @param entityPage 实体分页
+     * @return BO 分页
+     */
     private PageResult<SysDataDictBO> entityPage2BOPage(Page<SysDataDictEntity> entityPage) {
         PageResult<SysDataDictBO> ret = new PageResult<>();
         BeanUtil.copyProperties(entityPage, ret);

+ 63 - 35
src/main/java/cc/uncarbon/module/sys/service/SysDeptService.java

@@ -19,8 +19,9 @@ import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
-import javax.annotation.Resource;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -31,12 +32,12 @@ import org.springframework.transaction.annotation.Transactional;
  *
  * @author Uncarbon
  */
+@RequiredArgsConstructor
 @Slf4j
 @Service
 public class SysDeptService extends HelioBaseServiceImpl<SysDeptMapper, SysDeptEntity> {
 
-    @Resource
-    private SysUserDeptRelationService sysUserDeptRelationService;
+    private final SysUserDeptRelationService sysUserDeptRelationService;
 
 
     /**
@@ -59,29 +60,29 @@ public class SysDeptService extends HelioBaseServiceImpl<SysDeptMapper, SysDeptE
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @deprecated 使用 getOneById(java.lang.Long, boolean) 替代
+     * @param id 主键ID
+     * @return null or BO
      */
-    @Deprecated
-    public SysDeptBO getOneById(Long entityId) throws BusinessException {
-        return this.getOneById(entityId, true);
+    public SysDeptBO getOneById(Long id) {
+        return this.getOneById(id, false);
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @param entityId         实体类主键ID
+     * @param id 主键ID
      * @param throwIfInvalidId 是否在 ID 无效时抛出异常
      * @return null or BO
      */
-    public SysDeptBO getOneById(Long entityId, boolean throwIfInvalidId) throws BusinessException {
-        SysDeptEntity entity = this.getById(entityId);
+    public SysDeptBO getOneById(Long id, boolean throwIfInvalidId) throws BusinessException {
+        SysDeptEntity entity = this.getById(id);
         if (throwIfInvalidId) {
             SysErrorEnum.INVALID_ID.assertNotNull(entity);
         }
 
-        return this.entity2BO(entity, false);
+        return this.entity2BO(entity);
     }
 
     /**
@@ -90,7 +91,7 @@ public class SysDeptService extends HelioBaseServiceImpl<SysDeptMapper, SysDeptE
     @SysLog(value = "新增部门")
     @Transactional(rollbackFor = Exception.class)
     public Long adminInsert(AdminInsertOrUpdateSysDeptDTO dto) {
-        log.info("[后台管理-新增部门] >> DTO={}", dto);
+        log.info("[后台管理-新增部门] >> 入参={}", dto);
         this.checkExistence(dto);
 
         if (ObjectUtil.isNull(dto.getParentId())) {
@@ -112,7 +113,7 @@ public class SysDeptService extends HelioBaseServiceImpl<SysDeptMapper, SysDeptE
     @SysLog(value = "编辑部门")
     @Transactional(rollbackFor = Exception.class)
     public void adminUpdate(AdminInsertOrUpdateSysDeptDTO dto) {
-        log.info("[后台管理-编辑部门] >> DTO={}", dto);
+        log.info("[后台管理-编辑部门] >> 入参={}", dto);
         this.checkExistence(dto);
 
         if (ObjectUtil.isNull(dto.getParentId())) {
@@ -131,7 +132,7 @@ public class SysDeptService extends HelioBaseServiceImpl<SysDeptMapper, SysDeptE
     @SysLog(value = "删除部门")
     @Transactional(rollbackFor = Exception.class)
     public void adminDelete(Collection<Long> ids) {
-        log.info("[后台管理-删除部门] >> ids={}", ids);
+        log.info("[后台管理-删除部门] >> 入参={}", ids);
         this.removeByIds(ids);
     }
 
@@ -153,16 +154,23 @@ public class SysDeptService extends HelioBaseServiceImpl<SysDeptMapper, SysDeptE
         }
 
         SysDeptEntity entity = this.getById(relationEntity.getDeptId());
-        return this.entity2BO(entity, false);
+        return this.entity2BO(entity);
     }
 
 
     /*
-    私有方法
-    ------------------------------------------------------------------------------------------------
+    ----------------------------------------------------------------
+                        私有方法 private methods
+    ----------------------------------------------------------------
      */
 
-    private SysDeptBO entity2BO(SysDeptEntity entity, boolean traverseChildren) {
+    /**
+     * 实体转 BO
+     *
+     * @param entity 实体
+     * @return BO
+     */
+    private SysDeptBO entity2BO(SysDeptEntity entity) {
         if (entity == null) {
             return null;
         }
@@ -175,33 +183,32 @@ public class SysDeptService extends HelioBaseServiceImpl<SysDeptMapper, SysDeptE
             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;
     }
 
+    /**
+     * 实体 List 转 BO List
+     *
+     * @param entityList 实体 List
+     * @return BO List
+     */
     private List<SysDeptBO> entityList2BOs(List<SysDeptEntity> entityList) {
+        if (CollUtil.isEmpty(entityList)) {
+            return Collections.emptyList();
+        }
+
         // 深拷贝
         List<SysDeptBO> ret = new ArrayList<>(entityList.size());
         entityList.forEach(
-                entity -> ret.add(this.entity2BO(entity, true))
+                entity -> {
+                    SysDeptBO bo = this.entity2BO(entity);
+                    ret.add(this.traverseChildren(bo));
+                }
         );
 
         return ret;
     }
 
-
     /**
      * 检查是否已存在相同数据
      *
@@ -222,4 +229,25 @@ public class SysDeptService extends HelioBaseServiceImpl<SysDeptMapper, SysDeptE
             throw new BusinessException(400, "已存在相同部门,请重新输入");
         }
     }
+
+    /**
+     * 递归遍历子级
+     *
+     * @param bo BO
+     * @return SysDeptBO
+     */
+    private SysDeptBO traverseChildren(SysDeptBO bo) {
+        List<SysDeptBO> children = this.adminList(
+                AdminListSysDeptDTO.builder()
+                        .parentId(bo.getId())
+                        .build()
+        );
+        if (CollUtil.isEmpty(children)) {
+            children = null;
+        }
+
+        bo.setChildren(children);
+
+        return bo;
+    }
 }

+ 40 - 16
src/main/java/cc/uncarbon/module/sys/service/SysLogService.java

@@ -10,15 +10,17 @@ 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.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 java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
 
 
 /**
@@ -27,6 +29,7 @@ import java.util.List;
  */
 @Slf4j
 @Service
+@RequiredArgsConstructor
 public class SysLogService extends HelioBaseServiceImpl<SysLogMapper, SysLogEntity> {
 
     /**
@@ -53,24 +56,24 @@ public class SysLogService extends HelioBaseServiceImpl<SysLogMapper, SysLogEnti
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @deprecated 使用 getOneById(java.lang.Long, boolean) 替代
+     * @param id 主键ID
+     * @return null or BO
      */
-    @Deprecated
-    public SysLogBO getOneById(Long entityId) throws BusinessException {
-       return this.getOneById(entityId, true);
+    public SysLogBO getOneById(Long id) {
+       return this.getOneById(id, false);
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @param entityId 实体类主键ID
+     * @param id 主键ID
      * @param throwIfInvalidId 是否在 ID 无效时抛出异常
      * @return null or BO
      */
-    public SysLogBO getOneById(Long entityId, boolean throwIfInvalidId) throws BusinessException {
-        SysLogEntity entity = this.getById(entityId);
+    public SysLogBO getOneById(Long id, boolean throwIfInvalidId) throws BusinessException {
+        SysLogEntity entity = this.getById(id);
         if (throwIfInvalidId) {
             SysErrorEnum.INVALID_ID.assertNotNull(entity);
         }
@@ -78,13 +81,18 @@ public class SysLogService extends HelioBaseServiceImpl<SysLogMapper, SysLogEnti
         return this.entity2BO(entity);
     }
 
-
-
     /*
-    私有方法
-    ------------------------------------------------------------------------------------------------
+    ----------------------------------------------------------------
+                        私有方法 private methods
+    ----------------------------------------------------------------
      */
 
+    /**
+     * 实体转 BO
+     *
+     * @param entity 实体
+     * @return BO
+     */
     private SysLogBO entity2BO(SysLogEntity entity) {
         if (entity == null) {
             return null;
@@ -98,7 +106,17 @@ public class SysLogService extends HelioBaseServiceImpl<SysLogMapper, SysLogEnti
         return bo;
     }
 
+    /**
+     * 实体 List 转 BO List
+     *
+     * @param entityList 实体 List
+     * @return BO List
+     */
     private List<SysLogBO> entityList2BOs(List<SysLogEntity> entityList) {
+        if (CollUtil.isEmpty(entityList)) {
+            return Collections.emptyList();
+        }
+
         // 深拷贝
         List<SysLogBO> ret = new ArrayList<>(entityList.size());
         entityList.forEach(
@@ -108,6 +126,12 @@ public class SysLogService extends HelioBaseServiceImpl<SysLogMapper, SysLogEnti
         return ret;
     }
 
+    /**
+     * 实体分页转 BO 分页
+     *
+     * @param entityPage 实体分页
+     * @return BO 分页
+     */
     private PageResult<SysLogBO> entityPage2BOPage(Page<SysLogEntity> entityPage) {
         PageResult<SysLogBO> ret = new PageResult<>();
         BeanUtil.copyProperties(entityPage, ret);

+ 167 - 230
src/main/java/cc/uncarbon/module/sys/service/SysMenuService.java

@@ -14,45 +14,46 @@ 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 cc.uncarbon.module.sys.model.response.VbenAdminMenuMetaVO;
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.lang.Snowflake;
+import cn.hutool.core.text.StrPool;
 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 java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
+import java.util.function.BinaryOperator;
 import java.util.stream.Collectors;
-import javax.annotation.Resource;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 
 /**
  * 后台菜单
+ *
  * @author Uncarbon
  */
+@RequiredArgsConstructor
 @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);
 
+    private final SysRoleMenuRelationService sysRoleMenuRelationService;
+
 
     /**
      * 后台管理-列表
@@ -61,53 +62,48 @@ public class SysMenuService extends HelioBaseServiceImpl<SysMenuMapper, SysMenuE
         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);
+        return this.entityList2BOs(entityList);
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @deprecated 使用 getOneById(java.lang.Long, boolean) 替代
+     * @param id 主键ID
+     * @return null or BO
      */
-    @Deprecated
-    public SysMenuBO getOneById(Long entityId) throws BusinessException {
-        return this.getOneById(entityId, true);
+    public SysMenuBO getOneById(Long id) {
+        return this.getOneById(id, false);
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @param entityId 实体类主键ID
+     * @param id               主键ID
      * @param throwIfInvalidId 是否在 ID 无效时抛出异常
      * @return null or BO
      */
-    public SysMenuBO getOneById(Long entityId, boolean throwIfInvalidId) throws BusinessException {
-        SysMenuEntity entity = this.getById(entityId);
+    public SysMenuBO getOneById(Long id, boolean throwIfInvalidId) throws BusinessException {
+        SysMenuEntity entity = this.getById(id);
         if (throwIfInvalidId) {
             SysErrorEnum.INVALID_ID.assertNotNull(entity);
         }
 
-        return this.entity2BO(entity, false, null);
+        return this.entity2BO(entity);
     }
 
     /**
      * 后台管理-新增
+     *
      * @return 主键ID
      */
     @SysLog(value = "新增后台菜单")
     @Transactional(rollbackFor = Exception.class)
     public Long adminInsert(AdminInsertOrUpdateSysMenuDTO dto) {
-        log.info("[后台管理-新增后台菜单] >> DTO={}", dto);
+        log.info("[后台管理-新增后台菜单] >> 入参={}", dto);
         this.checkExistence(dto);
 
         if (ObjectUtil.isNull(dto.getParentId())) {
@@ -130,7 +126,7 @@ public class SysMenuService extends HelioBaseServiceImpl<SysMenuMapper, SysMenuE
     @SysLog(value = "编辑后台菜单")
     @Transactional(rollbackFor = Exception.class)
     public void adminUpdate(AdminInsertOrUpdateSysMenuDTO dto) {
-        log.info("[后台管理-编辑后台菜单] >> DTO={}", dto);
+        log.info("[后台管理-编辑后台菜单] >> 入参={}", dto);
         this.checkExistence(dto);
 
         if (ObjectUtil.isNull(dto.getParentId())) {
@@ -149,151 +145,113 @@ public class SysMenuService extends HelioBaseServiceImpl<SysMenuMapper, SysMenuE
     @SysLog(value = "删除后台菜单")
     @Transactional(rollbackFor = Exception.class)
     public void adminDelete(Collection<Long> ids) {
-        log.info("[后台管理-删除后台菜单] >> ids={}", ids);
+        log.info("[后台管理-删除后台菜单] >> 入参={}", 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).filter(StrUtil::isNotEmpty).distinct().collect(Collectors.toList());
-        }
-
-        List<Long> menuIds = sysRoleMenuRelationService.listMenuIdByRoleIds(roleIds);
-
-        if (CollUtil.isEmpty(menuIds)) {
-            return CollUtil.newArrayList();
-        }
-
-        return this.list(
-                new QueryWrapper<SysMenuEntity>()
-                        .lambda()
-                        .select(SysMenuEntity::getPermission)
-                        .in(SysMenuEntity::getId, menuIds)
-        ).stream().map(SysMenuEntity::getPermission).filter(StrUtil::isNotEmpty).distinct().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);
+        Set<Long> visibleMenuIds = this.listCurrentUserVisibleMenuId();
 
-        return allMenus;
+        List<SysMenuTypeEnum> requiredMenuTypes = CollUtil.newArrayList(SysMenuTypeEnum.DIR, SysMenuTypeEnum.MENU,
+                SysMenuTypeEnum.EXTERNAL_LINK);
+        return this.listByIds(visibleMenuIds, requiredMenuTypes);
     }
 
     /**
-     * 后台管理-取当前账号所有可见菜单(包括按钮类型)
+     * 后台管理-取所有可见菜单 包括按钮类型
      */
     public List<SysMenuBO> adminListVisibleMenu() {
-        List<Long> visibleMenuIds = this.listCurrentUserVisibleMenuId();
+        Set<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;
+        List<SysMenuTypeEnum> requiredMenuTypes = CollUtil.newArrayList(SysMenuTypeEnum.DIR, SysMenuTypeEnum.MENU,
+                SysMenuTypeEnum.EXTERNAL_LINK, SysMenuTypeEnum.BUTTON);
+        return this.listByIds(visibleMenuIds, requiredMenuTypes);
     }
 
     /**
-     * 内部-过滤被禁用的菜单ID
+     * 根据角色Ids,获取角色ID 对应的权限名 Map
      *
-     * @param visibleMenuIds 待过滤的可见的菜单ID列表
-     * @return 过滤后的菜单ID列表
+     * @return map key=角色ID value=权限名集合
      */
-    public List<Long> filterDisabledIds(List<Long> visibleMenuIds) {
-        if (CollUtil.isEmpty(visibleMenuIds)) {
-            return visibleMenuIds;
+    public Map<Long, Set<String>> getRoleIdPermissionMap(Collection<Long> roleIds) {
+        if (CollUtil.isEmpty(roleIds)) {
+            return Collections.emptyMap();
         }
 
-        /*
-        1. 找出目前被禁用的菜单ID
-         */
-        List<Long> disabledMenuIds = this.list(
-                new QueryWrapper<SysMenuEntity>()
-                        .select(HelioConstant.CRUD.SQL_COLUMN_ID)
-                        .lambda()
-                        .eq(SysMenuEntity::getStatus, GenericStatusEnum.DISABLED)
-        ).stream().map(SysMenuEntity::getId).collect(Collectors.toList());
-
+        HashMap<Long, Set<String>> ret = new HashMap<>(roleIds.size());
+
+        roleIds.forEach(
+                roleId -> {
+                    Set<String> permissions;
+
+                    if (SysConstant.SUPER_ADMIN_ROLE_ID.equals(roleId)) {
+                        // 超级管理员读取所有权限,不管有没有被禁用
+                        permissions = this.list().stream()
+                                .map(SysMenuEntity::getPermission)
+                                .filter(StrUtil::isNotEmpty)
+                                .collect(Collectors.toSet())
+                        ;
+                    } else {
+                        // 非超级管理员则通过角色ID,关联查询拥有的菜单,菜单上有权限名
+
+                        Set<Long> menuIds = sysRoleMenuRelationService.listMenuIdByRoleId(roleId);
+                        if (CollUtil.isEmpty(menuIds)) {
+                            permissions = Collections.emptySet();
+                        } else {
+                            permissions = this.list(
+                                            new QueryWrapper<SysMenuEntity>()
+                                                    .lambda()
+                                                    .select(SysMenuEntity::getPermission)
+                                                    .in(SysMenuEntity::getId, menuIds)
+                                                    .eq(SysMenuEntity::getStatus, GenericStatusEnum.ENABLED)
+                                    )
+                                    .stream()
+                                    .map(SysMenuEntity::getPermission)
+                                    .filter(StrUtil::isNotEmpty)
+                                    .collect(Collectors.toSet());
+                        }
+                    }
 
-        /*
-        2. 提取出符合以下条件的菜单ID:
-            启用状态 && 父菜单未被禁用
-         */
-        return this.list(
-                new QueryWrapper<SysMenuEntity>()
-                        .select(HelioConstant.CRUD.SQL_COLUMN_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());
-    }
+                    ret.put(roleId, permissions);
+                }
+        );
 
-    /**
-     * 通用-清除Redis中所有菜单缓存
-     */
-    public void cleanMenuCacheInRedis() {
-        this.cleanMenuCacheInRedis(null);
+        return ret;
     }
 
     /**
-     * 通用-根据用户ID,清除Redis中相关菜单缓存
-     * @param userId 用户ID;为null则清除所有
+     * 根据菜单ID集合,取权限名集合
      */
-    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);
+    public Set<String> listPermissionByMenuIds(Collection<Long> menuIds) {
+        if (CollUtil.isEmpty(menuIds)) {
+            return Collections.emptySet();
         }
 
-        keys = stringRedisTemplate.keys(SysConstant.REDIS_KEY_VISIBLE_MENU + redisKey);
-        if (keys != null && !keys.isEmpty()) {
-            stringRedisTemplate.delete(keys);
-        }
+        return this.list(
+                new QueryWrapper<SysMenuEntity>()
+                        .lambda()
+                        .select(SysMenuEntity::getPermission)
+                        .in(SysMenuEntity::getId, menuIds)
+        ).stream().map(SysMenuEntity::getPermission).filter(StrUtil::isNotEmpty).collect(Collectors.toSet());
     }
 
 
     /*
-    私有方法
-    ------------------------------------------------------------------------------------------------
+    ----------------------------------------------------------------
+                        私有方法 private methods
+    ----------------------------------------------------------------
      */
 
-    private SysMenuBO entity2BO(SysMenuEntity entity, boolean traverseChildren, List<SysMenuTypeEnum> menuTypes) {
+    /**
+     * 实体转 BO
+     *
+     * @param entity 实体
+     * @return BO
+     */
+    private SysMenuBO entity2BO(SysMenuEntity entity) {
         if (entity == null) {
             return null;
         }
@@ -311,7 +269,7 @@ public class SysMenuService extends HelioBaseServiceImpl<SysMenuMapper, SysMenuE
                 .setIdStr(StrUtil.toString(bo.getId()))
                 .setName(snowflakeIdStr)
                 .setMeta(
-                        VbenAdminMenuMetaBO.builder()
+                        VbenAdminMenuMetaVO.builder()
                                 .title(bo.getTitle())
                                 .affix(false)
                                 .icon(bo.getIcon())
@@ -325,7 +283,7 @@ public class SysMenuService extends HelioBaseServiceImpl<SysMenuMapper, SysMenuE
                 bo
                         .setComponent(SysConstant.VBEN_ADMIN_BLANK_VIEW)
                         .setExternalLink(null)
-                        .setPath("/" + snowflakeIdStr)
+                        .setPath(StrPool.SLASH + snowflakeIdStr)
                 ;
                 break;
             }
@@ -335,8 +293,8 @@ public class SysMenuService extends HelioBaseServiceImpl<SysMenuMapper, SysMenuE
                         .setPath(bo.getComponent())
                 ;
                 // 防止用户忘记加了, 主动补充/
-                if (StrUtil.isNotBlank(bo.getPath()) && !bo.getPath().startsWith("/")) {
-                    bo.setPath("/" + bo.getPath());
+                if (StrUtil.isNotBlank(bo.getPath()) && !bo.getPath().startsWith(StrPool.SLASH)) {
+                    bo.setPath(StrPool.SLASH + bo.getPath());
                 }
                 break;
             }
@@ -349,29 +307,14 @@ public class SysMenuService extends HelioBaseServiceImpl<SysMenuMapper, SysMenuE
             }
         }
 
-        // 遍历子级菜单
-        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) {
+    private List<SysMenuBO> entityList2BOs(List<SysMenuEntity> entityList) {
         // 深拷贝
         List<SysMenuBO> ret = new ArrayList<>(entityList.size());
         entityList.forEach(
-                entity -> ret.add(this.entity2BO(entity, traverseChildren, null))
+                entity -> ret.add(this.entity2BO(entity))
         );
 
         return ret;
@@ -379,43 +322,37 @@ public class SysMenuService extends HelioBaseServiceImpl<SysMenuMapper, SysMenuE
 
     /**
      * 取当前账号可见菜单Ids
+     *
      * @return 菜单Ids
      */
-    private List<Long> listCurrentUserVisibleMenuId() {
+    private Set<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);
-        }
+        Set<Long> roleIds = UserContextHolder.getUserContext().getRolesIds();
+        SysErrorEnum.NO_ROLE_AVAILABLE_FOR_CURRENT_USER.assertNotEmpty(roleIds);
 
-        // 2-1. 超级管理员直接允许所有菜单,即使是禁用状态
-        if (roleIds.contains(SysConstant.SUPER_ADMIN_ROLE_ID)) {
-            return this.list(
-                    new QueryWrapper<SysMenuEntity>()
-                            .select(HelioConstant.CRUD.SQL_COLUMN_ID)
-            ).stream().map(SysMenuEntity::getId).collect(Collectors.toList());
-        }
+        // 2. 得到所有可用的 菜单ID-上级菜单ID map,备用
+        Map<Long, Long> allMenuMap = this.list(
+                new QueryWrapper<SysMenuEntity>()
+                        .lambda()
+                        .select(SysMenuEntity::getId, SysMenuEntity::getParentId)
+                        .eq(SysMenuEntity::getStatus, GenericStatusEnum.ENABLED)
+        ).stream().collect(Collectors.toMap(SysMenuEntity::getId, SysMenuEntity::getParentId, ignoredThrowingMerger()));
 
-        // 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. 超级管理员直接返回所有菜单
+        if (roleIds.contains(SysConstant.SUPER_ADMIN_ROLE_ID)) {
+            return new HashSet<>(allMenuMap.keySet());
         }
 
-        /*
-        3. 补充上级菜单ID
-        这个Ant-Design的Tree组件有个吊诡的地方, 只选一部分子级的话, 父级菜单不算勾选
-        只能在代码里补上了
-         */
-        List<Long> missingParentIds = this.listMissingParentId(menuIds);
-        menuIds.addAll(missingParentIds);
+        // 4. 根据现有角色,获取直接关联的菜单ID
+        Set<Long> directlyRelatedMenuIds = sysRoleMenuRelationService.listMenuIdByRoleIds(roleIds);
+        SysErrorEnum.NO_MENU_AVAILABLE_FOR_CURRENT_ROLE.assertNotEmpty(directlyRelatedMenuIds);
 
-        return menuIds;
+        // 5. 因为直接关联的菜单ID,可能不包含父级菜单,使得级联关系缺失,这里得给他补上
+        return this.traceParentMenuIds(allMenuMap, directlyRelatedMenuIds);
     }
 
-    private List<SysMenuBO> listMenuByParentId(List<Long> visibleMenuIds, List<SysMenuTypeEnum> requiredMenuTypes, Long parentId) throws IllegalArgumentException {
+    private List<SysMenuBO> listByIds(Collection<Long> visibleMenuIds, List<SysMenuTypeEnum> requiredMenuTypes)
+            throws IllegalArgumentException {
         if (CollUtil.isEmpty(visibleMenuIds)) {
             throw new IllegalArgumentException("visibleMenuIds不能为空");
         }
@@ -429,39 +366,21 @@ public class SysMenuService extends HelioBaseServiceImpl<SysMenuMapper, SysMenuE
                         .lambda()
                         .in(SysMenuEntity::getId, visibleMenuIds)
                         .in(SysMenuEntity::getType, requiredMenuTypes)
-                        .eq(SysMenuEntity::getParentId, parentId)
                         .orderByAsc(SysMenuEntity::getSort)
         );
 
         if (CollUtil.isEmpty(entityList)) {
-            return CollUtil.newArrayList();
+            return Collections.emptyList();
         }
 
         List<SysMenuBO> ret = new ArrayList<>(entityList.size());
         entityList.forEach(
-                each -> ret.add(this.entity2BO(each, false, requiredMenuTypes))
+                each -> ret.add(this.entity2BO(each))
         );
 
         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());
-                    }
-                }
-        );
-    }
-
     /**
      * 检查是否已存在相同数据
      *
@@ -488,32 +407,50 @@ public class SysMenuService extends HelioBaseServiceImpl<SysMenuMapper, SysMenuE
     }
 
     /**
-     * 补充遗漏的上级菜单ID
+     * 追溯并补充可能缺失的级联上级菜单ID
+     *
+     * @param allMenuMap 完整的 菜单ID-上级菜单ID map
+     * @param directlyRelatedMenuIds 当前用户直接关联的菜单ID集合
+     * @return Set<Long> 已经补充好的、完整的菜单ID集合
      */
-    private List<Long> listMissingParentId(List<Long> menuIds) {
+    private Set<Long> traceParentMenuIds(Map<Long, Long> allMenuMap, Collection<Long> directlyRelatedMenuIds) {
         // 返回值
-        List<Long> ret = new ArrayList<>(32);
-        // 单次循环返回值
-        List<Long> loopRet = menuIds;
+        Set<Long> ret = new HashSet<>(directlyRelatedMenuIds.size() << 1);
+        ret.addAll(directlyRelatedMenuIds);
 
-        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());
+        // 本次循环要遍历的菜单ID集合
+        HashSet<Long> thisLoop;
+        // 下次循环要遍历的菜单ID集合
+        HashSet<Long> nextLoop = new HashSet<>(directlyRelatedMenuIds);
 
-            if (loopRet.isEmpty()) {
-                break;
-            } else {
-                ret.addAll(loopRet);
+        do {
+            // 深拷贝数据,用于本次循环
+            thisLoop = new HashSet<>(nextLoop);
+            nextLoop.clear();
+
+            for (Long menuId : thisLoop) {
+                Long parentId = allMenuMap.get(menuId);
+
+                if (Objects.nonNull(parentId)) {
+                    // 上级菜单可能还会有祖级菜单,需要继续上溯
+                    nextLoop.add(parentId);
+                }
             }
-        }
+
+            ret.addAll(nextLoop);
+
+        } while (!nextLoop.isEmpty());
 
         return ret;
     }
+
+    /**
+     * 原来键冲突会报错,使用自定义函数来无视
+     *
+     * @param <T> 键类型
+     * @return 键
+     */
+    private static <T> BinaryOperator<T> ignoredThrowingMerger() {
+        return (u, v) -> u;
+    }
 }

+ 51 - 21
src/main/java/cc/uncarbon/module/sys/service/SysParamService.java

@@ -13,11 +13,13 @@ 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.collection.CollUtil;
 import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -53,24 +55,24 @@ public class SysParamService extends HelioBaseServiceImpl<SysParamMapper, SysPar
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @deprecated 使用 getOneById(java.lang.Long, boolean) 替代
+     * @param id 主键ID
+     * @return null or BO
      */
-    @Deprecated
-    public SysParamBO getOneById(Long entityId) throws BusinessException {
-        return this.getOneById(entityId, true);
+    public SysParamBO getOneById(Long id) {
+        return this.getOneById(id, false);
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @param entityId 实体类主键ID
+     * @param id 主键ID
      * @param throwIfInvalidId 是否在 ID 无效时抛出异常
      * @return null or BO
      */
-    public SysParamBO getOneById(Long entityId, boolean throwIfInvalidId) throws BusinessException {
-        SysParamEntity entity = this.getById(entityId);
+    public SysParamBO getOneById(Long id, boolean throwIfInvalidId) throws BusinessException {
+        SysParamEntity entity = this.getById(id);
         if (throwIfInvalidId) {
             SysErrorEnum.INVALID_ID.assertNotNull(entity);
         }
@@ -84,7 +86,7 @@ public class SysParamService extends HelioBaseServiceImpl<SysParamMapper, SysPar
     @SysLog(value = "新增系统参数")
     @Transactional(rollbackFor = Exception.class)
     public Long adminInsert(AdminInsertOrUpdateSysParamDTO dto) {
-        log.info("[后台管理-新增系统参数] >> DTO={}", dto);
+        log.info("[后台管理-新增系统参数] >> 入参={}", dto);
         this.checkExistence(dto);
 
         dto.setId(null);
@@ -102,7 +104,7 @@ public class SysParamService extends HelioBaseServiceImpl<SysParamMapper, SysPar
     @SysLog(value = "编辑系统参数")
     @Transactional(rollbackFor = Exception.class)
     public void adminUpdate(AdminInsertOrUpdateSysParamDTO dto) {
-        log.info("[后台管理-编辑系统参数] >> DTO={}", dto);
+        log.info("[后台管理-编辑系统参数] >> 入参={}", dto);
         this.checkExistence(dto);
 
         SysParamEntity entity = new SysParamEntity();
@@ -117,7 +119,7 @@ public class SysParamService extends HelioBaseServiceImpl<SysParamMapper, SysPar
     @SysLog(value = "删除系统参数")
     @Transactional(rollbackFor = Exception.class)
     public void adminDelete(Collection<Long> ids) {
-        log.info("[后台管理-删除系统参数] >> ids={}", ids);
+        log.info("[后台管理-删除系统参数] >> 入参={}", ids);
         this.removeByIds(ids);
     }
 
@@ -125,7 +127,7 @@ public class SysParamService extends HelioBaseServiceImpl<SysParamMapper, SysPar
      * 根据键名取值
      *
      * @param name 键名
-     * @return 成功返回键值,失败返回null
+     * @return 成功返回键值,失败返回 null
      */
     public String getParamValueByName(String name) {
         SysParamEntity sysParamEntity = this.getOne(
@@ -147,7 +149,7 @@ public class SysParamService extends HelioBaseServiceImpl<SysParamMapper, SysPar
      *
      * @param name         键名
      * @param defaultValue 默认值
-     * @return 成功返回键值,失败返回defaultValue
+     * @return 成功返回键值,失败返回 defaultValue
      */
     public String getParamValueByName(String name, String defaultValue) {
         String value = this.getParamValueByName(name);
@@ -160,10 +162,17 @@ public class SysParamService extends HelioBaseServiceImpl<SysParamMapper, SysPar
 
 
     /*
-    私有方法
-    ------------------------------------------------------------------------------------------------
+    ----------------------------------------------------------------
+                        私有方法 private methods
+    ----------------------------------------------------------------
      */
 
+    /**
+     * 实体转 BO
+     *
+     * @param entity 实体
+     * @return BO
+     */
     private SysParamBO entity2BO(SysParamEntity entity) {
         if (entity == null) {
             return null;
@@ -177,16 +186,37 @@ public class SysParamService extends HelioBaseServiceImpl<SysParamMapper, SysPar
         return bo;
     }
 
-    private PageResult<SysParamBO> entityPage2BOPage(Page<SysParamEntity> entityPage) {
+    /**
+     * 实体 List 转 BO List
+     *
+     * @param entityList 实体 List
+     * @return BO List
+     */
+    private List<SysParamBO> entityList2BOs(List<SysParamEntity> entityList) {
+        if (CollUtil.isEmpty(entityList)) {
+            return Collections.emptyList();
+        }
+
         // 深拷贝
-        List<SysParamBO> boRecords = new ArrayList<>(entityPage.getRecords().size());
-        entityPage.getRecords().forEach(
-                entity -> boRecords.add(this.entity2BO(entity))
+        List<SysParamBO> ret = new ArrayList<>(entityList.size());
+        entityList.forEach(
+                entity -> ret.add(this.entity2BO(entity))
         );
 
+        return ret;
+    }
+
+    /**
+     * 实体分页转 BO 分页
+     *
+     * @param entityPage 实体分页
+     * @return BO 分页
+     */
+    private PageResult<SysParamBO> entityPage2BOPage(Page<SysParamEntity> entityPage) {
         PageResult<SysParamBO> ret = new PageResult<>();
         BeanUtil.copyProperties(entityPage, ret);
-        ret.setRecords(boRecords);
+        ret.setRecords(this.entityList2BOs(entityPage.getRecords()));
+
         return ret;
     }
 

+ 83 - 26
src/main/java/cc/uncarbon/module/sys/service/SysRoleMenuRelationService.java

@@ -4,15 +4,21 @@ 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.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
 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;
-
 
 /**
  * 后台角色-可见菜单关联
@@ -21,55 +27,106 @@ import java.util.stream.Collectors;
  */
 @Slf4j
 @Service
+@RequiredArgsConstructor
 public class SysRoleMenuRelationService extends HelioBaseServiceImpl<SysRoleMenuRelationMapper, SysRoleMenuRelationEntity> {
 
-    @Resource
-    private SysMenuService sysMenuService;
-
-
     /**
      * 根据角色Ids取菜单Ids
+     * 因为多种角色容易出现交集,所以干脆用 Set
      *
      * @param roleIds 角色Ids
      * @return 菜单Ids
      */
-    public List<Long> listMenuIdByRoleIds(List<Long> roleIds) throws IllegalArgumentException {
+    public Set<Long> listMenuIdByRoleIds(Collection<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());
+        Set<Long> ret = new HashSet<>(roleIds.size() * 16);
+        roleIds.forEach(
+                roleId -> ret.addAll(this.list(
+                            new QueryWrapper<SysRoleMenuRelationEntity>()
+                                    .lambda()
+                                    .select(SysRoleMenuRelationEntity::getMenuId)
+                                    .eq(SysRoleMenuRelationEntity::getRoleId, roleId)
+                    ).stream().map(SysRoleMenuRelationEntity::getMenuId).collect(Collectors.toSet()))
+        );
 
-        return sysMenuService.filterDisabledIds(menuIds);
+        return ret;
     }
 
     /**
-     * 先清理角色ID所有关联关系, 再绑定角色ID与菜单ID
+     * 绑定角色ID与菜单ID关联关系,增量更新
+     *
+     * @param roleId 角色ID
+     * @param menuIds 新菜单ID集合
      */
     @Transactional(rollbackFor = Exception.class)
-    public void cleanAndBind(Long roleId, List<Long> menuIds) {
+    public void cleanAndBind(Long roleId, Collection<Long> menuIds) {
+        LambdaQueryWrapper<SysRoleMenuRelationEntity> menuIdsQuery =
+                new QueryWrapper<SysRoleMenuRelationEntity>()
+                        .lambda()
+                        .select(SysRoleMenuRelationEntity::getMenuId)
+                        .eq(SysRoleMenuRelationEntity::getRoleId, roleId);
+
+        if (CollUtil.isEmpty(menuIds)) {
+            // 清除绑定,直接删除所有关联关系就行
+            this.remove(menuIdsQuery);
+            return;
+        }
+
+        /*
+        先删除不再需要的关联关系
+         */
         this.remove(
                 new QueryWrapper<SysRoleMenuRelationEntity>()
                         .lambda()
                         .eq(SysRoleMenuRelationEntity::getRoleId, roleId)
+                        .notIn(SysRoleMenuRelationEntity::getMenuId, menuIds)
         );
 
-        if (CollUtil.isNotEmpty(menuIds)) {
-            // 需要绑定菜单
+        /*
+        取出需要增量更新的部分
+         */
+        Set<Long> existingMenuIds = this.list(menuIdsQuery).stream().map(SysRoleMenuRelationEntity::getMenuId)
+                .collect(Collectors.toSet());
+
+        menuIds.removeAll(existingMenuIds);
 
+        /*
+        构造 && 批量插入
+         */
+        if (!menuIds.isEmpty()) {
+            List<SysRoleMenuRelationEntity> entityList = new ArrayList<>();
             menuIds.forEach(
-                    menuId -> this.save(
-                            SysRoleMenuRelationEntity.builder()
-                                    .roleId(roleId)
-                                    .menuId(menuId)
-                                    .build()
+                    menuId -> entityList.add(SysRoleMenuRelationEntity.builder()
+                            .roleId(roleId)
+                            .menuId(menuId)
+                            .build()
                     )
             );
+
+            this.saveBatch(entityList);
         }
     }
-}
+
+    /**
+     * 根据角色Ids取菜单Ids
+     * 因为多种角色容易出现交集,所以干脆用 Set
+     *
+     * @param roleId 角色Id
+     * @return 菜单Ids
+     */
+    public Set<Long> listMenuIdByRoleId(Long roleId) {
+        if (Objects.isNull(roleId)) {
+           return Collections.emptySet();
+        }
+
+        return this.list(
+                new QueryWrapper<SysRoleMenuRelationEntity>()
+                        .lambda()
+                        .select(SysRoleMenuRelationEntity::getMenuId)
+                        .eq(SysRoleMenuRelationEntity::getRoleId, roleId)
+        ).stream().map(SysRoleMenuRelationEntity::getMenuId).collect(Collectors.toSet());
+    }
+}

+ 84 - 39
src/main/java/cc/uncarbon/module/sys/service/SysRoleService.java

@@ -9,6 +9,7 @@ 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.AdminBindRoleMenuRelationDTO;
 import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysRoleDTO;
 import cc.uncarbon.module.sys.model.request.AdminListSysRoleDTO;
 import cc.uncarbon.module.sys.model.response.SysRoleBO;
@@ -20,8 +21,13 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
-import javax.annotation.Resource;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BinaryOperator;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -29,17 +35,19 @@ import org.springframework.transaction.annotation.Transactional;
 
 /**
  * 后台角色
+ *
  * @author Uncarbon
  */
 @Slf4j
 @Service
+@RequiredArgsConstructor
 public class SysRoleService extends HelioBaseServiceImpl<SysRoleMapper, SysRoleEntity> {
 
-    @Resource
-    private SysUserRoleRelationService sysUserRoleRelationService;
+    private final SysUserRoleRelationService sysUserRoleRelationService;
 
-    @Resource
-    private SysRoleMenuRelationService sysRoleMenuRelationService;
+    private final SysRoleMenuRelationService sysRoleMenuRelationService;
+
+    private final SysMenuService sysMenuService;
 
 
     /**
@@ -64,24 +72,24 @@ public class SysRoleService extends HelioBaseServiceImpl<SysRoleMapper, SysRoleE
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @deprecated 使用 getOneById(java.lang.Long, boolean) 替代
+     * @param id 主键ID
+     * @return null or BO
      */
-    @Deprecated
-    public SysRoleBO getOneById(Long entityId) throws BusinessException {
-        return this.getOneById(entityId, true);
+    public SysRoleBO getOneById(Long id) {
+        return this.getOneById(id, false);
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @param entityId 实体类主键ID
+     * @param id 主键ID
      * @param throwIfInvalidId 是否在 ID 无效时抛出异常
      * @return null or BO
      */
-    public SysRoleBO getOneById(Long entityId, boolean throwIfInvalidId) throws BusinessException {
-        SysRoleEntity entity = this.getById(entityId);
+    public SysRoleBO getOneById(Long id, boolean throwIfInvalidId) throws BusinessException {
+        SysRoleEntity entity = this.getById(id);
         if (throwIfInvalidId) {
             SysErrorEnum.INVALID_ID.assertNotNull(entity);
         }
@@ -91,12 +99,13 @@ public class SysRoleService extends HelioBaseServiceImpl<SysRoleMapper, SysRoleE
 
     /**
      * 后台管理-新增
+     *
      * @return 主键ID
      */
     @SysLog(value = "新增后台角色")
     @Transactional(rollbackFor = Exception.class)
     public Long adminInsert(AdminInsertOrUpdateSysRoleDTO dto) {
-        log.info("[后台管理-新增后台角色] >> DTO={}", dto);
+        log.info("[后台管理-新增后台角色] >> 入参={}", dto);
         this.checkExistence(dto);
 
         dto.setId(null);
@@ -105,8 +114,6 @@ public class SysRoleService extends HelioBaseServiceImpl<SysRoleMapper, SysRoleE
 
         this.save(entity);
 
-        sysRoleMenuRelationService.cleanAndBind(entity.getId(), dto.getMenuIds());
-
         return entity.getId();
     }
 
@@ -116,14 +123,12 @@ public class SysRoleService extends HelioBaseServiceImpl<SysRoleMapper, SysRoleE
     @SysLog(value = "编辑后台角色")
     @Transactional(rollbackFor = Exception.class)
     public void adminUpdate(AdminInsertOrUpdateSysRoleDTO dto) {
-        log.info("[后台管理-编辑后台角色] >> DTO={}", dto);
+        log.info("[后台管理-编辑后台角色] >> 入参={}", dto);
         this.checkExistence(dto);
 
         SysRoleEntity entity = new SysRoleEntity();
         BeanUtil.copyProperties(dto, entity);
 
-        sysRoleMenuRelationService.cleanAndBind(dto.getId(), dto.getMenuIds());
-
         this.updateById(entity);
     }
 
@@ -133,34 +138,35 @@ public class SysRoleService extends HelioBaseServiceImpl<SysRoleMapper, SysRoleE
     @SysLog(value = "删除后台角色")
     @Transactional(rollbackFor = Exception.class)
     public void adminDelete(Collection<Long> ids) {
-        log.info("[后台管理-删除后台角色] >> ids={}", ids);
+        log.info("[后台管理-删除后台角色] >> 入参={}", ids);
         this.removeByIds(ids);
     }
 
     /**
-     * 取拥有角色列表
-     * @param userId 用户ID
-     * @return 失败返回空列表
+     * 后台管理-绑定角色与菜单关联关系
+     *
+     * @return 新菜单ID集合对应的权限名
      */
-    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);
+    public Set<String> adminBindMenus(AdminBindRoleMenuRelationDTO dto) {
+        Set<String> newPermissions = sysMenuService.listPermissionByMenuIds(dto.getMenuIds());
+        sysRoleMenuRelationService.cleanAndBind(dto.getRoleId(), dto.getMenuIds());
 
-        return this.entityList2BOs(entityList);
+        return newPermissions;
     }
 
 
     /*
-    私有方法
-    ------------------------------------------------------------------------------------------------
+    ----------------------------------------------------------------
+                        私有方法 private methods
+    ----------------------------------------------------------------
      */
 
+    /**
+     * 实体转 BO
+     *
+     * @param entity 实体
+     * @return BO
+     */
     private SysRoleBO entity2BO(SysRoleEntity entity) {
         if (entity == null) {
             return null;
@@ -170,12 +176,16 @@ public class SysRoleService extends HelioBaseServiceImpl<SysRoleMapper, SysRoleE
         BeanUtil.copyProperties(entity, bo);
 
         // 可以在此处为BO填充字段
-        bo.setMenuIds(sysRoleMenuRelationService.listMenuIdByRoleIds(
-           CollUtil.newArrayList(bo.getId())
-        ));
+        bo.setMenuIds(sysRoleMenuRelationService.listMenuIdByRoleIds(CollUtil.newHashSet(bo.getId())));
         return bo;
     }
 
+    /**
+     * 实体 List 转 BO List
+     *
+     * @param entityList 实体 List
+     * @return BO List
+     */
     private List<SysRoleBO> entityList2BOs(List<SysRoleEntity> entityList) {
         // 深拷贝
         List<SysRoleBO> ret = new ArrayList<>(entityList.size());
@@ -186,6 +196,12 @@ public class SysRoleService extends HelioBaseServiceImpl<SysRoleMapper, SysRoleE
         return ret;
     }
 
+    /**
+     * 实体分页转 BO 分页
+     *
+     * @param entityPage 实体分页
+     * @return BO 分页
+     */
     private PageResult<SysRoleBO> entityPage2BOPage(Page<SysRoleEntity> entityPage) {
         PageResult<SysRoleBO> ret = new PageResult<>();
         BeanUtil.copyProperties(entityPage, ret);
@@ -214,4 +230,33 @@ public class SysRoleService extends HelioBaseServiceImpl<SysRoleMapper, SysRoleE
             throw new BusinessException(400, "已存在相同后台角色,请重新输入");
         }
     }
+
+    /**
+     * 取用户ID拥有角色对应的 角色ID-角色名 map
+     *
+     * @param userId 用户ID
+     * @return 失败返回空 map
+     */
+    public Map<Long, String> getRoleMapByUserId(Long userId) {
+        Set<Long> roleIds = sysUserRoleRelationService.listRoleIdByUserId(userId);
+
+        if (CollUtil.isEmpty(roleIds)) {
+            return Collections.emptyMap();
+        }
+
+        // 根据角色Ids取 map
+        return this.list(
+                new QueryWrapper<SysRoleEntity>()
+                        .lambda()
+                        .select(SysRoleEntity::getId, SysRoleEntity::getValue)
+                        .in(SysRoleEntity::getId, roleIds)
+        ).stream().collect(Collectors.toMap(SysRoleEntity::getId, SysRoleEntity::getValue, this.ignoredThrowingMerger()));
+    }
+
+    /**
+     * 主动忽略 map key 重复错误,不然 key 重复的话,转换成 map 的过程会抛异常
+     */
+    private <T> BinaryOperator<T> ignoredThrowingMerger() {
+        return (u, v) -> u;
+    }
 }

+ 62 - 129
src/main/java/cc/uncarbon/module/sys/service/SysTenantService.java

@@ -1,19 +1,15 @@
 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.AdminInsertOrUpdateSysRoleDTO;
-import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysUserDTO;
-import cc.uncarbon.module.sys.model.request.AdminInsertSysTenantDTO;
+import cc.uncarbon.module.sys.mapper.SysUserMapper;
 import cc.uncarbon.module.sys.model.request.AdminListSysTenantDTO;
 import cc.uncarbon.module.sys.model.request.AdminUpdateSysTenantDTO;
 import cc.uncarbon.module.sys.model.response.SysTenantBO;
@@ -22,11 +18,10 @@ 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 java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
-import javax.annotation.Resource;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -38,16 +33,10 @@ import org.springframework.transaction.annotation.Transactional;
  */
 @Slf4j
 @Service
+@RequiredArgsConstructor
 public class SysTenantService extends HelioBaseServiceImpl<SysTenantMapper, SysTenantEntity> {
 
-    @Resource
-    private SysRoleService sysRoleService;
-
-    @Resource
-    private SysUserService sysUserService;
-
-    @Resource
-    private SysUserRoleRelationService sysUserRoleRelationService;
+    private final SysUserMapper sysUserMapper;
 
 
     /**
@@ -74,24 +63,24 @@ public class SysTenantService extends HelioBaseServiceImpl<SysTenantMapper, SysT
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @deprecated 使用 getOneById(java.lang.Long, boolean) 替代
+     * @param id 主键ID
+     * @return null or BO
      */
-    @Deprecated
-    public SysTenantBO getOneById(Long entityId) throws BusinessException {
-        return this.getOneById(entityId, true);
+    public SysTenantBO getOneById(Long id) {
+        return this.getOneById(id, false);
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @param entityId 实体类主键ID
+     * @param id 主键ID
      * @param throwIfInvalidId 是否在 ID 无效时抛出异常
      * @return null or BO
      */
-    public SysTenantBO getOneById(Long entityId, boolean throwIfInvalidId) throws BusinessException {
-        SysTenantEntity entity = this.getById(entityId);
+    public SysTenantBO getOneById(Long id, boolean throwIfInvalidId) throws BusinessException {
+        SysTenantEntity entity = this.getById(id);
         if (throwIfInvalidId) {
             SysErrorEnum.INVALID_ID.assertNotNull(entity);
         }
@@ -99,81 +88,13 @@ public class SysTenantService extends HelioBaseServiceImpl<SysTenantMapper, SysT
         return this.entity2BO(entity);
     }
 
-    /**
-     * 后台管理-新增
-     */
-    @SysLog(value = "新增系统租户")
-    @Transactional(rollbackFor = Exception.class)
-    public Long adminInsert(AdminInsertSysTenantDTO dto) {
-        log.info("[后台管理-新增系统租户] >> DTO={}", dto);
-        this.checkExistence(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) {
-        log.info("[后台管理-编辑系统租户] >> DTO={}", dto);
+        log.info("[后台管理-编辑系统租户] >> 入参={}", dto);
         this.checkExistence(dto);
 
         SysTenantEntity entity = new SysTenantEntity();
@@ -188,34 +109,58 @@ public class SysTenantService extends HelioBaseServiceImpl<SysTenantMapper, SysT
     @SysLog(value = "删除系统租户")
     @Transactional(rollbackFor = Exception.class)
     public void adminDelete(Collection<Long> ids) {
-        log.info("[后台管理-删除系统租户] >> ids={}", ids);
+        log.info("[后台管理-删除系统租户] >> 入参={}", ids);
         this.removeByIds(ids);
     }
 
     /**
-     * 通用-根据租户ID(非主键ID)查询
+     * 根据租户ID(非主键ID),得到租户实体
      */
-    public SysTenantBO getTenantByTenantId(Long tenantId) {
-        SysTenantEntity sysTenantEntity = this.getOne(
+    public SysTenantEntity getTenantEntityByTenantId(Long tenantId) {
+        return this.getOne(
                 new QueryWrapper<SysTenantEntity>()
                         .lambda()
                         .eq(SysTenantEntity::getTenantId, tenantId)
                         .last(HelioConstant.CRUD.SQL_LIMIT_1)
         );
+    }
 
-        if (sysTenantEntity == null) {
-            return null;
-        }
+    /**
+     * 检查是否已存在相同数据
+     *
+     * @param dto DTO
+     */
+    public void checkExistence(AdminUpdateSysTenantDTO dto) {
+        SysTenantEntity existingEntity = this.getOne(
+                new QueryWrapper<SysTenantEntity>()
+                        .lambda()
+                        // 仅取主键ID
+                        .select(SysTenantEntity::getId)
+                        // 租户ID相同
+                        .eq(SysTenantEntity::getTenantId, dto.getTenantId())
+                        .or()
+                        // 或租户名相同
+                        .eq(SysTenantEntity::getTenantName, dto.getTenantName())
+                        .last(HelioConstant.CRUD.SQL_LIMIT_1)
+        );
 
-        return this.entity2BO(sysTenantEntity);
+        if (existingEntity != null && !existingEntity.getId().equals(dto.getId())) {
+            throw new BusinessException(400, "已存在相同系统租户,请重新输入");
+        }
     }
 
-
     /*
-    私有方法
-    ------------------------------------------------------------------------------------------------
+    ----------------------------------------------------------------
+                        私有方法 private methods
+    ----------------------------------------------------------------
      */
 
+    /**
+     * 实体转 BO
+     *
+     * @param entity 实体
+     * @return BO
+     */
     private SysTenantBO entity2BO(SysTenantEntity entity) {
         if (entity == null) {
             return null;
@@ -227,13 +172,19 @@ public class SysTenantService extends HelioBaseServiceImpl<SysTenantMapper, SysT
         // 可以在此处为BO填充字段
         if (ObjectUtil.isNotNull(entity.getTenantAdminUserId())) {
             bo
-                    .setTenantAdminUser(sysUserService.getBaseInfoById(entity.getTenantAdminUserId()))
+                    .setTenantAdminUser(sysUserMapper.getBaseInfoByUserId(entity.getTenantAdminUserId()))
             ;
         }
 
         return bo;
     }
 
+    /**
+     * 实体 List 转 BO List
+     *
+     * @param entityList 实体 List
+     * @return BO List
+     */
     private List<SysTenantBO> entityList2BOs(List<SysTenantEntity> entityList) {
         // 深拷贝
         List<SysTenantBO> ret = new ArrayList<>(entityList.size());
@@ -244,6 +195,12 @@ public class SysTenantService extends HelioBaseServiceImpl<SysTenantMapper, SysT
         return ret;
     }
 
+    /**
+     * 实体分页转 BO 分页
+     *
+     * @param entityPage 实体分页
+     * @return BO 分页
+     */
     private PageResult<SysTenantBO> entityPage2BOPage(Page<SysTenantEntity> entityPage) {
         PageResult<SysTenantBO> ret = new PageResult<>();
         BeanUtil.copyProperties(entityPage, ret);
@@ -252,28 +209,4 @@ public class SysTenantService extends HelioBaseServiceImpl<SysTenantMapper, SysT
         return ret;
     }
 
-    /**
-     * 检查是否已存在相同数据
-     *
-     * @param dto DTO
-     */
-    private void checkExistence(AdminUpdateSysTenantDTO dto) {
-        SysTenantEntity existingEntity = this.getOne(
-                new QueryWrapper<SysTenantEntity>()
-                        .lambda()
-                        // 仅取主键ID
-                        .select(SysTenantEntity::getId)
-                        // 租户ID相同
-                        .eq(SysTenantEntity::getTenantId, dto.getTenantId())
-                        .or()
-                        // 或租户名相同
-                        .eq(SysTenantEntity::getTenantName, dto.getTenantName())
-                        .last(HelioConstant.CRUD.SQL_LIMIT_1)
-        );
-
-        if (existingEntity != null && !existingEntity.getId().equals(dto.getId())) {
-            throw new BusinessException(400, "已存在相同系统租户,请重新输入");
-        }
-    }
-
 }

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

@@ -39,4 +39,4 @@ public class SysUserDeptRelationService extends HelioBaseServiceImpl<SysUserDept
 
     }
 
-}
+}

+ 6 - 22
src/main/java/cc/uncarbon/module/sys/service/SysUserRoleRelationService.java

@@ -5,13 +5,13 @@ 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 java.util.Collection;
+import java.util.Set;
+import java.util.stream.Collectors;
 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;
-
 
 /**
  * 后台用户-角色关联
@@ -25,7 +25,7 @@ public class SysUserRoleRelationService extends HelioBaseServiceImpl<SysUserRole
      * 先清理用户ID所有关联关系, 再绑定用户ID与角色ID
      */
     @Transactional(rollbackFor = Exception.class)
-    public void cleanAndBind(Long userId, List<Long> roleIds) {
+    public void cleanAndBind(Long userId, Collection<Long> roleIds) {
         this.remove(
                 new QueryWrapper<SysUserRoleRelationEntity>()
                         .lambda()
@@ -45,28 +45,12 @@ public class SysUserRoleRelationService extends HelioBaseServiceImpl<SysUserRole
         }
     }
 
-    /**
-     * 根据角色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>()
-                        .lambda()
-                        .select(SysUserRoleRelationEntity::getUserId)
-                        .in(SysUserRoleRelationEntity::getRoleId, roleIds)
-        ).stream().map(SysUserRoleRelationEntity::getUserId).collect(Collectors.toList());
-    }
-
     /**
      * 取拥有角色Ids
      * @param userId 用户ID
      * @return 失败返回空列表
      */
-    public List<Long> listRoleIdByUserId(Long userId) throws IllegalArgumentException {
+    public Set<Long> listRoleIdByUserId(Long userId) throws IllegalArgumentException {
         if (userId == null) {
             throw new IllegalArgumentException("userId不能为空");
         }
@@ -76,6 +60,6 @@ public class SysUserRoleRelationService extends HelioBaseServiceImpl<SysUserRole
                         .lambda()
                         .select(SysUserRoleRelationEntity::getRoleId)
                         .eq(SysUserRoleRelationEntity::getUserId, userId)
-        ).stream().map(SysUserRoleRelationEntity::getRoleId).collect(Collectors.toList());
+        ).stream().map(SysUserRoleRelationEntity::getRoleId).collect(Collectors.toSet());
     }
 }

+ 156 - 111
src/main/java/cc/uncarbon/module/sys/service/SysUserService.java

@@ -1,75 +1,71 @@
 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.TenantContextHolder;
 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.core.props.HelioProperties;
 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.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.AdminBindUserRoleRelationDTO;
-import cc.uncarbon.module.sys.model.request.AdminInsertOrUpdateSysUserDTO;
-import cc.uncarbon.module.sys.model.request.AdminListSysUserDTO;
-import cc.uncarbon.module.sys.model.request.AdminResetSysUserPasswordDTO;
-import cc.uncarbon.module.sys.model.request.AdminUpdateCurrentSysUserPasswordDTO;
-import cc.uncarbon.module.sys.model.request.SysUserLoginDTO;
-import cc.uncarbon.module.sys.model.response.SysDeptBO;
-import cc.uncarbon.module.sys.model.response.SysRoleBO;
-import cc.uncarbon.module.sys.model.response.SysTenantBO;
-import cc.uncarbon.module.sys.model.response.SysUserBO;
-import cc.uncarbon.module.sys.model.response.SysUserBaseInfoBO;
-import cc.uncarbon.module.sys.model.response.SysUserLoginBO;
-import cc.uncarbon.module.sys.model.response.VbenAdminUserInfoBO;
+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.collection.CollUtil;
 import cn.hutool.core.date.LocalDateTimeUtil;
 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 com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.stream.Collectors;
-import javax.annotation.Resource;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import javax.annotation.PostConstruct;
+import java.time.LocalDateTime;
+import java.util.*;
+
 
 /**
  * 后台用户
+ *
  * @author Uncarbon
  */
 @Slf4j
 @Service
+@RequiredArgsConstructor
 public class SysUserService extends HelioBaseServiceImpl<SysUserMapper, SysUserEntity> {
 
-    @Resource
-    private SysRoleService sysRoleService;
+    private final SysRoleService sysRoleService;
+
+    private final SysDeptService sysDeptService;
 
-    @Resource
-    private SysDeptService sysDeptService;
+    private final SysMenuService sysMenuService;
 
-    @Resource
-    private SysMenuService sysMenuService;
+    private final SysTenantService sysTenantService;
 
-    @Resource
-    private SysTenantService sysTenantService;
+    private final SysUserDeptRelationService sysUserDeptRelationService;
 
-    @Resource
-    private SysUserDeptRelationService sysUserDeptRelationService;
+    private final SysUserRoleRelationService sysUserRoleRelationService;
 
-    @Resource
-    private SysUserRoleRelationService sysUserRoleRelationService;
+    private final HelioProperties helioProperties;
 
+    private boolean isTenantEnabled;
+
+    @PostConstruct
+    public void postConstruct() {
+        this.isTenantEnabled = helioProperties.getTenant().getEnabled();
+    }
 
     /**
      * 后台管理-分页列表
@@ -89,40 +85,40 @@ public class SysUserService extends HelioBaseServiceImpl<SysUserMapper, SysUserE
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @deprecated 使用 getOneById(java.lang.Long, boolean, boolean) 替代
+     * @param id 主键ID
+     * @return null or BO
      */
-    @Deprecated
-    public SysUserBO getOneById(Long entityId) throws BusinessException {
-        return this.getOneById(entityId, true, false);
+    public SysUserBO getOneById(Long id) {
+        return this.getOneById(id, false);
     }
 
     /**
-     * 通用-详情
+     * 根据 ID 取详情
      *
-     * @param entityId 实体类主键ID
+     * @param id 主键ID
      * @param throwIfInvalidId 是否在 ID 无效时抛出异常
-     * @param joinFullRolesAndPermissions 是否显示完整角色和权限信息
      * @return null or BO
      */
-    public SysUserBO getOneById(Long entityId, boolean throwIfInvalidId, boolean joinFullRolesAndPermissions) throws BusinessException {
-        SysUserEntity entity = this.getById(entityId);
+    public SysUserBO getOneById(Long id, boolean throwIfInvalidId) throws BusinessException {
+        SysUserEntity entity = this.getById(id);
         if (throwIfInvalidId) {
             SysErrorEnum.INVALID_ID.assertNotNull(entity);
         }
 
-        return this.entity2BO(entity, joinFullRolesAndPermissions);
+        return this.entity2BO(entity);
     }
 
     /**
      * 后台管理-新增
+     *
      * @return 主键ID
      */
     @SysLog(value = "新增后台用户")
     @Transactional(rollbackFor = Exception.class)
     public Long adminInsert(AdminInsertOrUpdateSysUserDTO dto) {
-        log.info("[后台管理-新增后台用户] >> DTO={}", dto);
+        log.info("[后台管理-新增后台用户] >> 入参={}", dto);
         this.checkExistence(dto);
 
         dto.setId(null);
@@ -149,7 +145,7 @@ public class SysUserService extends HelioBaseServiceImpl<SysUserMapper, SysUserE
     @SysLog(value = "编辑后台用户")
     @Transactional(rollbackFor = Exception.class)
     public void adminUpdate(AdminInsertOrUpdateSysUserDTO dto) {
-        log.info("[后台管理-编辑后台用户] >> DTO={}", dto);
+        log.info("[后台管理-编辑后台用户] >> 入参={}", dto);
         this.checkExistence(dto);
 
         SysUserEntity updateEntity = new SysUserEntity();
@@ -166,7 +162,7 @@ public class SysUserService extends HelioBaseServiceImpl<SysUserMapper, SysUserE
     @SysLog(value = "删除后台用户")
     @Transactional(rollbackFor = Exception.class)
     public void adminDelete(Collection<Long> ids) {
-        log.info("[后台管理-删除后台用户] >> ids={}", ids);
+        log.info("[后台管理-删除后台用户] >> 入参={}", ids);
         this.removeByIds(ids);
     }
 
@@ -175,8 +171,23 @@ public class SysUserService extends HelioBaseServiceImpl<SysUserMapper, SysUserE
      */
     @SysLog(value = "登录后台用户")
     public SysUserLoginBO adminLogin(SysUserLoginDTO dto) {
-        // 主动清空用户上下文,避免残留租户ID导致的尴尬
-        UserContextHolder.setUserContext(null);
+        /*
+        这里是实际启用了多租户功能,并主动指定租户ID
+        实际生产应用时,推荐前端传值加密,后端在此解密
+
+        表级、数据源级多租户,登录前【必须】主动指定租户ID
+        e.g. dto.setTenantId(101L)
+         */
+
+        dto.setTenantId(101L);
+
+        // ConcurrentHashMap 的 value 不能为 null,还是 new 一个吧
+        TenantContext tenantContext = new TenantContext();
+        if (isTenantEnabled && ObjectUtil.isNotNull(dto.getTenantId())) {
+            tenantContext = this.checkAndGetTenantContext(dto.getTenantId());
+            // 验证通过,将所属租户写入租户上下文,使得 SQL 拦截器可以正确执行
+            TenantContextHolder.setTenantContext(tenantContext);
+        }
 
         SysUserEntity sysUserEntity = this.getUserByPin(dto.getUsername());
         if (sysUserEntity == null) {
@@ -191,47 +202,43 @@ public class SysUserService extends HelioBaseServiceImpl<SysUserMapper, SysUserE
             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);
+        if (isTenantEnabled && ObjectUtil.isNotNull(sysUserEntity.getTenantId())) {
+            // 二次赋值,以防万一(也许前端没有传租户ID,上方的 if 块并没有执行)
+            tenantContext = this.checkAndGetTenantContext(sysUserEntity.getTenantId());
+            TenantContextHolder.setTenantContext(tenantContext);
+        }
 
         try {
-            this.getBaseMapper().updateLastLoginAt(sysUserEntity.getId(), LocalDateTimeUtil.now());
+            this.updateLastLoginAt(sysUserEntity.getId(), LocalDateTimeUtil.now());
         } catch (Exception ignored) {
             // 实际开发环境请删除本try-catch块
         }
 
+        // 取账号完整信息
+        SysUserBO sysUserBO = this.entity2BO(sysUserEntity);
+        Map<Long, String> roleMap = sysRoleService.getRoleMapByUserId(sysUserBO.getId());
 
-        // 取账号完整BO
-        SysUserBO sysUserBO = this.getOneById(sysUserEntity.getId(), true, true);
+        Map<Long, Set<String>> roleIdPermissionMap = sysMenuService.getRoleIdPermissionMap(roleMap.keySet());
 
+        // 包装返回体;有的字段类型不一致, 单独转换
         SysUserLoginBO ret = new SysUserLoginBO();
         BeanUtil.copyProperties(sysUserBO, ret);
 
-        // 因字段类型不一致, 单独转换
+        // aka * 64
+        HashSet<String> permissions = new HashSet<>(roleIdPermissionMap.size() << 6);
+        roleIdPermissionMap.values().forEach(permissions::addAll);
+
         ret
-                .setRoleIds(sysUserBO.getRoles().stream().map(SysRoleBO::getId).filter(ObjectUtil::isNotNull).sorted().collect(Collectors.toList()))
-                .setRoles(sysUserBO.getRoles().stream().map(SysRoleBO::getValue).filter(StrUtil::isNotBlank).collect(Collectors.toList()))
-                .setPermissions(sysUserBO.getPermissions())
-                .setRelationalTenant(currentTenantContext)
+                .setRoleIds(new HashSet<>(roleMap.keySet()))
+                .setRoles(new ArrayList<>(roleMap.values()))
+                .setPermissions(permissions)
+                .setRoleIdPermissionMap(roleIdPermissionMap)
+                .setTenantContext(tenantContext)
         ;
 
         return ret;
@@ -240,9 +247,9 @@ public class SysUserService extends HelioBaseServiceImpl<SysUserMapper, SysUserE
     /**
      * 后台管理-取当前用户信息
      */
-    public VbenAdminUserInfoBO adminGetCurrentUserInfo() {
-        SysUserBO sysUserBO = this.getOneById(UserContextHolder.getUserId(), true, false);
-        return VbenAdminUserInfoBO.builder()
+    public VbenAdminUserInfoVO adminGetCurrentUserInfo() {
+        SysUserBO sysUserBO = this.getOneById(UserContextHolder.getUserId(), true);
+        return VbenAdminUserInfoVO.builder()
                 .username(sysUserBO.getUsername())
                 .nickname(sysUserBO.getNickname())
                 .lastLoginAt(sysUserBO.getLastLoginAt())
@@ -260,7 +267,7 @@ public class SysUserService extends HelioBaseServiceImpl<SysUserMapper, SysUserE
         templateEntity
                 .setPwd(PwdUtil.encrypt(dto.getRandomPassword(), sysUserEntity.getSalt()))
                 .setId(dto.getUserId())
-                ;
+        ;
 
         this.updateById(templateEntity);
     }
@@ -284,37 +291,32 @@ public class SysUserService extends HelioBaseServiceImpl<SysUserMapper, SysUserE
     }
 
     /**
-     * 通用-根据用户账号查询
-     */
-    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());
     }
 
     /**
-     * 后台管理-绑定用户与角色关联关系
+     * 根据用户账号查询
      */
-    public void adminBindRoles(AdminBindUserRoleRelationDTO dto) {
-        sysUserRoleRelationService.cleanAndBind(dto.getUserId(), dto.getRoleIds());
+    public SysUserEntity getUserByPin(String pin) {
+        return this.getBaseMapper().getUserByPin(pin);
     }
 
     /*
-    私  有  方  法
-    ------------------------------------------------------------------------------------------------
+    ----------------------------------------------------------------
+                        私有方法 private methods
+    ----------------------------------------------------------------
      */
 
-    private SysUserBO entity2BO(SysUserEntity entity, boolean joinFullRolesAndPermissions) {
+    /**
+     * 实体转 BO
+     *
+     * @param entity 实体
+     * @return BO
+     */
+    private SysUserBO entity2BO(SysUserEntity entity) {
         if (entity == null) {
             return null;
         }
@@ -323,8 +325,6 @@ public class SysUserService extends HelioBaseServiceImpl<SysUserMapper, SysUserE
         BeanUtil.copyProperties(entity, bo);
 
         // 可以在此处为BO填充字段
-        bo.setRoleIds(sysUserRoleRelationService.listRoleIdByUserId(bo.getId()));
-
         SysDeptBO dept = sysDeptService.getPlainDeptByUserId(bo.getId());
         if (dept != null) {
             bo
@@ -333,27 +333,40 @@ public class SysUserService extends HelioBaseServiceImpl<SysUserMapper, SysUserE
             ;
         }
 
-        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 转 BO List
+     *
+     * @param entityList 实体 List
+     * @return BO List
+     */
+    private List<SysUserBO> entityList2BOs(List<SysUserEntity> entityList) {
+        if (CollUtil.isEmpty(entityList)) {
+            return Collections.emptyList();
+        }
+
         // 深拷贝
-        List<SysUserBO> boRecords = new ArrayList<>(entityPage.getRecords().size());
-        entityPage.getRecords().forEach(
-                entity -> boRecords.add(this.entity2BO(entity, false))
+        List<SysUserBO> ret = new ArrayList<>(entityList.size());
+        entityList.forEach(
+                entity -> ret.add(this.entity2BO(entity))
         );
 
+        return ret;
+    }
+
+    /**
+     * 实体分页转 BO 分页
+     *
+     * @param entityPage 实体分页
+     * @return BO 分页
+     */
+    private PageResult<SysUserBO> entityPage2BOPage(Page<SysUserEntity> entityPage) {
         PageResult<SysUserBO> ret = new PageResult<>();
         BeanUtil.copyProperties(entityPage, ret);
-        ret.setRecords(boRecords);
+        ret.setRecords(this.entityList2BOs(entityPage.getRecords()));
+
         return ret;
     }
 
@@ -369,4 +382,36 @@ public class SysUserService extends HelioBaseServiceImpl<SysUserMapper, SysUserE
             throw new BusinessException(400, "已存在相同账号,请重新输入");
         }
     }
+
+    /**
+     * 检查并获取租户上下文 bean,无效或被禁用则直接抛出异常
+     * @param tenantId 租户ID
+     * @return TenantContext
+     */
+    private TenantContext checkAndGetTenantContext(Long tenantId) throws BusinessException {
+        // 查询租户是否仍有效
+        SysTenantEntity tenantEntity = sysTenantService.getTenantEntityByTenantId(tenantId);
+        if (tenantEntity == null) {
+            throw new BusinessException(SysErrorEnum.INVALID_TENANT);
+        }
+
+        if (GenericStatusEnum.DISABLED.equals(tenantEntity.getStatus())) {
+            throw new BusinessException(SysErrorEnum.DISABLED_TENANT);
+        }
+
+        return TenantContext.builder()
+                .tenantId(tenantEntity.getTenantId())
+                .tenantName(tenantEntity.getTenantName())
+                .build()
+        ;
+    }
+
+    private void updateLastLoginAt(Long userId, LocalDateTime lastLoginAt) {
+        SysUserEntity entity = new SysUserEntity();
+        entity
+                .setLastLoginAt(lastLoginAt)
+                .setId(userId)
+                ;
+        this.updateById(entity);
+    }
 }

+ 48 - 22
src/main/java/cc/uncarbon/module/sys/controller/AdminAuthController.java → src/main/java/cc/uncarbon/module/sys/web/auth/AdminAuthController.java

@@ -1,52 +1,62 @@
-package cc.uncarbon.module.sys.controller;
+package cc.uncarbon.module.sys.web.auth;
 
 
 import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.context.TenantContext;
+import cc.uncarbon.framework.core.context.TenantContextHolder;
 import cc.uncarbon.framework.core.context.UserContext;
 import cc.uncarbon.framework.core.context.UserContextHolder;
+import cc.uncarbon.framework.core.exception.BusinessException;
 import cc.uncarbon.framework.web.model.response.ApiResult;
+import cc.uncarbon.helper.CaptchaHelper;
+import cc.uncarbon.helper.RolePermissionCacheHelper;
 import cc.uncarbon.module.sys.constant.SysConstant;
+import cc.uncarbon.module.sys.enums.SysErrorEnum;
 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.model.response.SysUserLoginVO;
 import cc.uncarbon.module.sys.service.SysUserService;
 import cc.uncarbon.module.sys.util.AdminStpUtil;
 import cn.dev33.satoken.annotation.SaCheckLogin;
+import cn.hutool.captcha.AbstractCaptcha;
+import cn.hutool.core.lang.Assert;
 import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
 import io.swagger.annotations.ApiOperation;
+import java.io.IOException;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
 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
  */
+@RequiredArgsConstructor
 @Slf4j
 @Api(value = "SaaS后台管理鉴权接口", tags = {"SaaS后台管理鉴权接口"})
 @RequestMapping(SysConstant.SYS_MODULE_CONTEXT_PATH + HelioConstant.Version.HTTP_API_VERSION_V1 + "/auth")
 @RestController
 public class AdminAuthController {
 
-    @Resource
-    private SysUserService sysUserService;
+    private final SysUserService sysUserService;
+
+    private final RolePermissionCacheHelper rolePermissionCacheHelper;
 
-    @Resource
-    private SysMenuService sysMenuService;
+    private final CaptchaHelper captchaHelper;
 
 
     @ApiOperation(value = "登录", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
     @PostMapping(value = "/login")
-    public ApiResult<?> login(@RequestBody @Valid SysUserLoginDTO dto) {
+    public ApiResult<SysUserLoginVO> login(@RequestBody @Valid SysUserLoginDTO dto) {
         // RPC调用, 失败抛异常, 成功返回用户信息
         SysUserLoginBO userInfo = sysUserService.adminLogin(dto);
 
@@ -59,21 +69,23 @@ public class AdminAuthController {
                 .extraData(null)
                 .rolesIds(userInfo.getRoleIds())
                 .roles(userInfo.getRoles())
-                .permissions(userInfo.getPermissions())
-                .relationalTenant(userInfo.getRelationalTenant())
                 .build();
 
-        // 注册到SA-Token
+        // 将用户ID注册到 SA-Token ,并附加一些业务字段
         AdminStpUtil.login(userInfo.getId(), dto.getRememberMe());
         AdminStpUtil.getSession().set(UserContext.CAMEL_NAME, userContext);
+        AdminStpUtil.getSession().set(TenantContext.CAMEL_NAME, userInfo.getTenantContext());
 
+        // 更新角色-权限缓存
+        rolePermissionCacheHelper.putCache(userInfo.getRoleIdPermissionMap());
 
         // 返回登录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());
+        SysUserLoginVO tokenInfo = SysUserLoginVO.builder()
+                .tokenName(AdminStpUtil.getTokenName())
+                .tokenValue(AdminStpUtil.getTokenValue())
+                .roles(userInfo.getRoles())
+                .permissions(userInfo.getPermissions())
+                .build();
 
         return ApiResult.data("登录成功", tokenInfo);
     }
@@ -82,12 +94,26 @@ public class AdminAuthController {
     @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);
+        TenantContextHolder.setTenantContext(null);
 
         return ApiResult.success();
     }
 
+    @ApiOperation(value = "验证码图片")
+    @ApiImplicitParam(name = "uuid", value = "验证码图片UUID", required = true)
+    @GetMapping(value = "/captcha")
+    public void captcha(HttpServletResponse response, String uuid) throws IOException {
+        Assert.notBlank(uuid, () -> new BusinessException(SysErrorEnum.UUID_CANNOT_BE_BLANK));
+
+        // 核验方法:captchaHelper.validate(uuid, true);
+        AbstractCaptcha captcha = captchaHelper.generate(uuid);
+
+        // 写入响应流
+        response.setHeader("Cache-Control", "no-store, no-cache");
+        response.setContentType("image/png");
+        captcha.write(response.getOutputStream());
+    }
+
 }

+ 4 - 4
src/main/java/cc/uncarbon/module/sys/controller/AdminSysDataDictController.java → src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysDataDictController.java

@@ -1,4 +1,4 @@
-package cc.uncarbon.module.sys.controller;
+package cc.uncarbon.module.sys.web.sys;
 
 import cc.uncarbon.framework.core.constant.HelioConstant;
 import cc.uncarbon.framework.core.page.PageParam;
@@ -15,17 +15,18 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
 import cn.dev33.satoken.annotation.SaCheckPermission;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
 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
  */
+@RequiredArgsConstructor
 @SaCheckLogin(type = AdminStpUtil.TYPE)
 @Slf4j
 @Api(value = "数据字典管理接口", tags = {"数据字典管理接口"})
@@ -35,8 +36,7 @@ public class AdminSysDataDictController {
 
     private static final String PERMISSION_PREFIX = "SysDataDict:" ;
 
-    @Resource
-    private SysDataDictService sysDataDictService;
+    private final SysDataDictService sysDataDictService;
 
 
     @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)

+ 4 - 4
src/main/java/cc/uncarbon/module/sys/controller/AdminSysDeptController.java → src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysDeptController.java

@@ -1,4 +1,4 @@
-package cc.uncarbon.module.sys.controller;
+package cc.uncarbon.module.sys.web.sys;
 
 
 import cc.uncarbon.framework.core.constant.HelioConstant;
@@ -14,17 +14,18 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
 import cn.dev33.satoken.annotation.SaCheckPermission;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
 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
  */
+@RequiredArgsConstructor
 @SaCheckLogin(type = AdminStpUtil.TYPE)
 @Slf4j
 @Api(value = "部门管理接口", tags = {"部门管理接口"})
@@ -34,8 +35,7 @@ public class AdminSysDeptController {
 
     private static final String PERMISSION_PREFIX = "SysDept:";
 
-    @Resource
-    private SysDeptService sysDeptService;
+    private final SysDeptService sysDeptService;
 
 
     @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)

+ 4 - 5
src/main/java/cc/uncarbon/module/sys/controller/AdminSysLogController.java → src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysLogController.java

@@ -1,4 +1,4 @@
-package cc.uncarbon.module.sys.controller;
+package cc.uncarbon.module.sys.web.sys;
 
 import cc.uncarbon.framework.core.constant.HelioConstant;
 import cc.uncarbon.framework.core.page.PageParam;
@@ -13,6 +13,7 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
 import cn.dev33.satoken.annotation.SaCheckPermission;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -20,12 +21,11 @@ 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
  */
+@RequiredArgsConstructor
 @SaCheckLogin(type = AdminStpUtil.TYPE)
 @Slf4j
 @Api(value = "后台操作日志管理接口", tags = {"后台操作日志管理接口"})
@@ -35,8 +35,7 @@ public class AdminSysLogController {
 
     private static final String PERMISSION_PREFIX = "SysLog:";
 
-    @Resource
-    private SysLogService sysLogService;
+    private final SysLogService sysLogService;
 
 
     @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)

+ 18 - 51
src/main/java/cc/uncarbon/module/sys/controller/AdminSysMenuController.java → src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysMenuController.java

@@ -1,7 +1,6 @@
-package cc.uncarbon.module.sys.controller;
+package cc.uncarbon.module.sys.web.sys;
 
 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;
@@ -9,28 +8,30 @@ 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 java.util.List;
+import javax.validation.Valid;
+import lombok.RequiredArgsConstructor;
 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;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
 
 
 /**
  * @author Uncarbon
  */
+@RequiredArgsConstructor
 @SaCheckLogin(type = AdminStpUtil.TYPE)
 @Slf4j
 @Api(value = "后台菜单管理接口", tags = {"后台菜单管理接口"})
@@ -40,14 +41,7 @@ public class AdminSysMenuController {
 
     private static final String PERMISSION_PREFIX = "SysMenu:";
 
-    @Resource
-    private SysMenuService sysMenuService;
-
-    @Resource
-    private SysParamService sysParamService;
-
-    @Resource
-    private StringRedisTemplate stringRedisTemplate;
+    private final SysMenuService sysMenuService;
 
 
     @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
@@ -69,7 +63,6 @@ public class AdminSysMenuController {
     @PostMapping
     public ApiResult<?> insert(@RequestBody @Valid AdminInsertOrUpdateSysMenuDTO dto) {
         sysMenuService.adminInsert(dto);
-        sysMenuService.cleanMenuCacheInRedis();
 
         return ApiResult.success();
     }
@@ -80,7 +73,6 @@ public class AdminSysMenuController {
     public ApiResult<?> update(@PathVariable Long id, @RequestBody @Valid AdminInsertOrUpdateSysMenuDTO dto) {
         dto.setId(id);
         sysMenuService.adminUpdate(dto);
-        sysMenuService.cleanMenuCacheInRedis();
 
         return ApiResult.success();
     }
@@ -90,45 +82,20 @@ public class AdminSysMenuController {
     @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)
+    @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.getParamValueByName(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);
+        return ApiResult.data(sysMenuService.adminListSideMenu());
     }
 
-    @ApiOperation(value = "取当前账号所有可见菜单", produces = MediaType.APPLICATION_JSON_VALUE)
+    @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.getParamValueByName(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);
+        return ApiResult.data(sysMenuService.adminListVisibleMenu());
     }
 
 }

+ 4 - 4
src/main/java/cc/uncarbon/module/sys/controller/AdminSysParamController.java → src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysParamController.java

@@ -1,4 +1,4 @@
-package cc.uncarbon.module.sys.controller;
+package cc.uncarbon.module.sys.web.sys;
 
 import cc.uncarbon.framework.core.constant.HelioConstant;
 import cc.uncarbon.framework.core.page.PageParam;
@@ -15,16 +15,17 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
 import cn.dev33.satoken.annotation.SaCheckPermission;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
 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
  */
+@RequiredArgsConstructor
 @SaCheckLogin(type = AdminStpUtil.TYPE)
 @Slf4j
 @Api(value = "系统参数管理接口", tags = {"系统参数管理接口"})
@@ -34,8 +35,7 @@ public class AdminSysParamController {
 
     private static final String PERMISSION_PREFIX = "SysParam:";
 
-    @Resource
-    private SysParamService sysParamService;
+    private final SysParamService sysParamService;
 
 
     @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)

+ 32 - 31
src/main/java/cc/uncarbon/module/sys/controller/AdminSysRoleController.java → src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysRoleController.java

@@ -1,35 +1,42 @@
-package cc.uncarbon.module.sys.controller;
+package cc.uncarbon.module.sys.web.sys;
 
 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.helper.RolePermissionCacheHelper;
 import cc.uncarbon.module.sys.constant.SysConstant;
+import cc.uncarbon.module.sys.model.request.AdminBindRoleMenuRelationDTO;
 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 javax.validation.Valid;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+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.List;
+import java.util.Set;
 
 
 /**
  * @author Uncarbon
  */
+@RequiredArgsConstructor
 @SaCheckLogin(type = AdminStpUtil.TYPE)
 @Slf4j
 @Api(value = "后台角色管理接口", tags = {"后台角色管理接口"})
@@ -39,14 +46,9 @@ public class AdminSysRoleController {
 
     private static final String PERMISSION_PREFIX = "SysRole:";
 
-    @Resource
-    private SysRoleService sysRoleService;
-
-    @Resource
-    private SysUserRoleRelationService sysUserRoleRelationService;
+    private final SysRoleService sysRoleService;
 
-    @Resource
-    private StringRedisTemplate stringRedisTemplate;
+    private final RolePermissionCacheHelper rolePermissionCacheHelper;
 
 
     @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
@@ -79,22 +81,6 @@ public class AdminSysRoleController {
         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();
     }
 
@@ -104,6 +90,21 @@ public class AdminSysRoleController {
     public ApiResult<?> delete(@RequestBody @Valid IdsDTO<Long> dto) {
         sysRoleService.adminDelete(dto.getIds());
 
+        // 角色删除时,删除对应缓存键
+        rolePermissionCacheHelper.deleteCache(dto.getIds());
+
+        return ApiResult.success();
+    }
+
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + "bindMenus")
+    @ApiOperation(value = "绑定角色与菜单关联关系", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @PostMapping(value = "/bindMenus")
+    public ApiResult<?> bindMenus(@RequestBody @Valid AdminBindRoleMenuRelationDTO dto) {
+        Set<String> newPermissions = sysRoleService.adminBindMenus(dto);
+
+        // 覆盖更新缓存
+        rolePermissionCacheHelper.putCache(dto.getRoleId(), newPermissions);
+
         return ApiResult.success();
     }
 

+ 8 - 5
src/main/java/cc/uncarbon/module/sys/controller/AdminSysTenantController.java → src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysTenantController.java

@@ -1,10 +1,11 @@
-package cc.uncarbon.module.sys.controller;
+package cc.uncarbon.module.sys.web.sys;
 
 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.facade.SysTenantFacade;
 import cc.uncarbon.module.sys.constant.SysConstant;
 import cc.uncarbon.module.sys.model.request.AdminInsertSysTenantDTO;
 import cc.uncarbon.module.sys.model.request.AdminListSysTenantDTO;
@@ -16,17 +17,18 @@ import cn.dev33.satoken.annotation.SaCheckLogin;
 import cn.dev33.satoken.annotation.SaCheckPermission;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
 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
  */
+@RequiredArgsConstructor
 @SaCheckLogin(type = AdminStpUtil.TYPE)
 @Slf4j
 @Api(value = "系统租户管理接口", tags = {"系统租户管理接口"})
@@ -36,8 +38,9 @@ public class AdminSysTenantController {
 
     private static final String PERMISSION_PREFIX = "SysTenant:";
 
-    @Resource
-    private SysTenantService sysTenantService;
+    private final SysTenantService sysTenantService;
+
+    private final SysTenantFacade sysTenantFacade;
 
 
     @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
@@ -58,7 +61,7 @@ public class AdminSysTenantController {
     @ApiOperation(value = "新增", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
     @PostMapping
     public ApiResult<?> insert(@RequestBody @Valid AdminInsertSysTenantDTO dto) {
-        sysTenantService.adminInsert(dto);
+        sysTenantFacade.adminInsert(dto);
 
         return ApiResult.success();
     }

+ 10 - 10
src/main/java/cc/uncarbon/module/sys/controller/AdminSysUserController.java → src/main/java/cc/uncarbon/module/sys/web/sys/AdminSysUserController.java

@@ -1,4 +1,4 @@
-package cc.uncarbon.module.sys.controller;
+package cc.uncarbon.module.sys.web.sys;
 
 import cc.uncarbon.framework.core.constant.HelioConstant;
 import cc.uncarbon.framework.core.exception.BusinessException;
@@ -9,7 +9,7 @@ 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.model.response.VbenAdminUserInfoBO;
+import cc.uncarbon.module.sys.model.response.VbenAdminUserInfoVO;
 import cc.uncarbon.module.sys.service.SysUserService;
 import cc.uncarbon.module.sys.util.AdminStpUtil;
 import cn.dev33.satoken.annotation.SaCheckLogin;
@@ -17,17 +17,18 @@ import cn.dev33.satoken.annotation.SaCheckPermission;
 import cn.hutool.core.util.StrUtil;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
 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
  */
+@RequiredArgsConstructor
 @SaCheckLogin(type = AdminStpUtil.TYPE)
 @Slf4j
 @Api(value = "后台用户管理接口", tags = {"后台用户管理接口"})
@@ -37,8 +38,7 @@ public class AdminSysUserController {
 
     private static final String PERMISSION_PREFIX = "SysUser:";
 
-    @Resource
-    private SysUserService sysUserService;
+    private final SysUserService sysUserService;
 
 
     @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.RETRIEVE)
@@ -52,7 +52,7 @@ public class AdminSysUserController {
     @ApiOperation(value = "详情", produces = MediaType.APPLICATION_JSON_VALUE)
     @GetMapping(value = "/{id}")
     public ApiResult<SysUserBO> getById(@PathVariable Long id) {
-        return ApiResult.data(sysUserService.getOneById(id, true, false));
+        return ApiResult.data(sysUserService.getOneById(id, true));
     }
 
     @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + HelioConstant.Permission.CREATE)
@@ -85,11 +85,11 @@ public class AdminSysUserController {
 
     @ApiOperation(value = "取当前用户信息", produces = MediaType.APPLICATION_JSON_VALUE)
     @GetMapping(value = "/info")
-    public ApiResult<VbenAdminUserInfoBO> getCurrentUserInfo() {
+    public ApiResult<VbenAdminUserInfoVO> getCurrentUserInfo() {
         return ApiResult.data(sysUserService.adminGetCurrentUserInfo());
     }
 
-    @SaCheckPermission(type = AdminStpUtil.TYPE, value = "SysUser:resetPassword")
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + "resetPassword")
     @ApiOperation(value = "重置某用户密码", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
     @PostMapping(value = "/resetPassword")
     public ApiResult<?> resetPassword(@RequestBody @Valid AdminResetSysUserPasswordDTO dto) {
@@ -119,7 +119,7 @@ public class AdminSysUserController {
         return ApiResult.success();
     }
 
-    @SaCheckPermission(type = AdminStpUtil.TYPE, value = "SysUser:bindRoles")
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + "bindRoles")
     @ApiOperation(value = "绑定用户与角色关联关系", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
     @PostMapping(value = "/bindRoles")
     public ApiResult<?> bindRoles(@RequestBody @Valid AdminBindUserRoleRelationDTO dto) {
@@ -128,7 +128,7 @@ public class AdminSysUserController {
         return ApiResult.success();
     }
 
-    @SaCheckPermission(type = AdminStpUtil.TYPE, value = "SysUser:kickOut")
+    @SaCheckPermission(type = AdminStpUtil.TYPE, value = PERMISSION_PREFIX + "kickOut")
     @ApiOperation(value = "踢某用户下线", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
     @PostMapping(value = "/kickOut")
     public ApiResult<?> kickOut(@RequestBody @Valid AdminKickOutSysUserDTO dto) {

+ 4 - 0
src/main/resources/application-dev.yml

@@ -14,6 +14,10 @@ helio:
     db-password: root
     # 数据库名
     db-name: helio_boot
+  web:
+    logging:
+      # 是否启用 Web 访问日志切面;开发、测试环境可以开启,生产环境不开启
+      enabled: false
 
 spring:
   redis:

+ 6 - 3
src/main/resources/application-prod.yml

@@ -14,6 +14,10 @@ helio:
     db-password: root
     # 数据库名
     db-name: helio_boot
+  web:
+    logging:
+      # 是否启用 Web 访问日志切面;开发、测试环境可以开启,生产环境不开启
+      enabled: false
 
 spring:
   redis:
@@ -32,6 +36,5 @@ knife4j:
 
 logging:
   level:
-    # 分别指定脚手架、业务模块的日志级别
-    cc.uncarbon.framework: INFO
-    cc.uncarbon.module: INFO
+    # 指定日志级别,开发、测试环境建议为 DEBUG,生产环境建议为 INFO
+    cc.uncarbon: INFO

+ 5 - 1
src/main/resources/application-test.yml

@@ -14,6 +14,10 @@ helio:
     db-password: root
     # 数据库名
     db-name: helio_boot
+  web:
+    logging:
+      # 是否启用 Web 访问日志切面;开发、测试环境可以开启,生产环境不开启
+      enabled: true
 
 spring:
   redis:
@@ -32,4 +36,4 @@ knife4j:
 
 mybatis-plus:
   configuration:
-    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

+ 18 - 17
src/main/resources/application.yml

@@ -13,6 +13,10 @@ spring:
     type: redis
     redis:
       cache-null-values: true
+  mvc:
+    pathmatch:
+      # Boot 2.6+ 默认的路由匹配策略,Swagger 暂不兼容,使用老策略
+      matching-strategy: ant_path_matcher
 
 helio:
   crud:
@@ -23,16 +27,6 @@ helio:
       datacenter-id: ${DATACENTER_ID:0}
       # 雪花ID-起始日期-可以选用项目开工的日期-这个一旦确定就不要改动了
       epoch-date: '2021-07-17'
-    tenant:
-      # 是否开启租户
-      enabled: true
-      ignored-tables:
-        # 以下表不参与租户隔离
-        - sys_menu
-        - sys_tenant
-        - sys_role_menu_relation
-        - sys_user_dept_relation
-        - sys_user_role_relation
     optimistic-lock:
       # 是否开启乐观锁
       enabled: true
@@ -46,7 +40,18 @@ helio:
       - /v3/api-docs
       # C端登录接口
       - /app/api/v1/auth/login
-
+  tenant:
+    # 是否开启多租户,详情请见文档【进阶使用-多租户】
+    enabled: false
+    # 多租户隔离级别
+    isolate-level: LINE
+    ignored-tables:
+      # 以下表不参与【行级】多租户 SQL 拦截器条件拼接
+      - sys_menu
+      - sys_tenant
+      - sys_role_menu_relation
+      - sys_user_dept_relation
+      - sys_user_role_relation
 
 knife4j:
   # 保持true
@@ -58,8 +63,6 @@ knife4j:
 
 server:
   port: 7003
-  http2:
-    enabled: true
 
 mybatis-plus:
   # 枚举和XML扫描,支持统配符 * 或者 ; 分割
@@ -68,10 +71,8 @@ mybatis-plus:
 
 logging:
   level:
-    # 分别指定脚手架、业务模块、单元测试的日志级别
-    cc.uncarbon.framework: DEBUG
-    cc.uncarbon.module: DEBUG
-    cc.uncarbon.test: DEBUG
+    # 指定日志级别,开发、测试环境建议为 DEBUG,生产环境建议为 INFO
+    cc.uncarbon: DEBUG
 
 sa-token:
   # token名称

+ 1 - 1
src/main/resources/banner.txt

@@ -10,4 +10,4 @@ ${AnsiColor.BLUE}
 ${AnsiColor.GREEN}
 Spring Boot Version: ${spring-boot.version}
 
-${AnsiColor.BRIGHT_WHITE}
+${AnsiColor.BLACK}

+ 31 - 13
src/main/resources/logback-spring.xml

@@ -41,14 +41,19 @@
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
             <!-- 历史日志输出文件名 -->
             <FileNamePattern>${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}-debug.log</FileNamePattern>
+            <!-- 历史日志保留天数 -->
             <MaxHistory>${LOG_MAX_HISTORY}</MaxHistory>
+            <!-- 除按日期记录之外,还配置了日志文件不能超过 ${LOG_MAX_SIZE} ,超过后日志文件会以索引 0 开始,
+                命名日志文件,例如 debug-2013-12-21.0.log
+                需要 FileNamePattern 中添加 %i 占位符
+                 -->
+            <!--<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <MaxFileSize>${LOG_MAX_SIZE}</MaxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>-->
         </rollingPolicy>
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
             <pattern>${LOG_FILE_OUTPUT_PATTERN}</pattern>
         </encoder>
-        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
-            <MaxFileSize>${LOG_MAX_SIZE}</MaxFileSize>
-        </triggeringPolicy>
     </appender>
 
     <!-- 异步处理DEBUG日志 -->
@@ -76,14 +81,18 @@
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
             <!-- 历史日志输出文件名 -->
             <FileNamePattern>${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}-warn.log</FileNamePattern>
+            <!-- 历史日志保留天数 -->
             <MaxHistory>${LOG_MAX_HISTORY}</MaxHistory>
+            <!-- 除按日期记录之外,还配置了日志文件不能超过 ${LOG_MAX_SIZE} ,超过后日志文件会以索引 0 开始,
+            命名日志文件,例如 warn-2013-12-21.0.log
+            需要 FileNamePattern 中添加 %i 占位符 -->
+            <!--<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <MaxFileSize>${LOG_MAX_SIZE}</MaxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>-->
         </rollingPolicy>
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
             <pattern>${LOG_FILE_OUTPUT_PATTERN}</pattern>
         </encoder>
-        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
-            <MaxFileSize>${LOG_MAX_SIZE}</MaxFileSize>
-        </triggeringPolicy>
     </appender>
 
     <!-- 异步处理WARN日志 -->
@@ -111,14 +120,18 @@
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
             <!-- 历史日志输出文件名 -->
             <FileNamePattern>${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}-info.log</FileNamePattern>
+            <!-- 历史日志保留天数 -->
             <MaxHistory>${LOG_MAX_HISTORY}</MaxHistory>
+            <!-- 除按日期记录之外,还配置了日志文件不能超过 ${LOG_MAX_SIZE} ,超过后日志文件会以索引 0 开始,
+            命名日志文件,例如 info-2013-12-21.0.log
+            需要 FileNamePattern 中添加 %i 占位符 -->
+            <!--<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <MaxFileSize>${LOG_MAX_SIZE}</MaxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>-->
         </rollingPolicy>
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
             <pattern>${LOG_FILE_OUTPUT_PATTERN}</pattern>
         </encoder>
-        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
-            <MaxFileSize>${LOG_MAX_SIZE}</MaxFileSize>
-        </triggeringPolicy>
     </appender>
 
     <!-- 异步处理INFO日志 -->
@@ -146,14 +159,18 @@
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
             <!-- 历史日志输出文件名 -->
             <FileNamePattern>${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}-error.log</FileNamePattern>
+            <!-- 历史日志保留天数 -->
             <MaxHistory>${LOG_MAX_HISTORY}</MaxHistory>
+            <!-- 除按日期记录之外,还配置了日志文件不能超过 ${LOG_MAX_SIZE} ,超过后日志文件会以索引 0 开始,
+            命名日志文件,例如 error-2013-12-21.0.log
+            需要 FileNamePattern 中添加 %i 占位符 -->
+            <!--<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <MaxFileSize>${LOG_MAX_SIZE}</MaxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>-->
         </rollingPolicy>
         <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
             <pattern>${LOG_FILE_OUTPUT_PATTERN}</pattern>
         </encoder>
-        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
-            <MaxFileSize>${LOG_MAX_SIZE}</MaxFileSize>
-        </triggeringPolicy>
     </appender>
 
     <!-- 异步处理ERROR日志 -->
@@ -170,8 +187,9 @@
     <shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>
 
     <logger name="io.undertow" level="WARN"/>
-    <logger name="com.alibaba.nacos" level="INFO"/>
+    <logger name="com.alibaba.nacos.client" level="ERROR"/>
     <logger name="org.apache.dubbo.common.Version" level="off"/>
+    <logger name="org.apache.dubbo.registry.nacos.NacosRegistry" level="ERROR"/>
 
     <!-- 默认日志输出级别 -->
     <root level="WARN">

+ 15 - 13
src/main/resources/mapper/SysUserMapper.xml

@@ -2,21 +2,23 @@
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="cc.uncarbon.module.sys.mapper.SysUserMapper">
 
-    <update id="updateLastLoginAt">
-        UPDATE sys_user
-        SET last_login_at = #{lastLoginAt}
-        WHERE id = #{userId}
-    </update>
+    <select id="getUserByPin" resultType="cc.uncarbon.module.sys.entity.SysUserEntity">
+        SELECT *
+        FROM sys_user
+        WHERE pin = #{pin}
+        LIMIT 1
+    </select>
 
     <select id="getBaseInfoByUserId" resultType="cc.uncarbon.module.sys.model.response.SysUserBaseInfoBO">
-        SELECT pin AS username,
-               nickname,
-               status,
-               gender,
-               email,
-               phone_no
+        SELECT pin AS username
+               , nickname
+               , status
+               , gender
+               , email
+               , phone_no
         FROM sys_user
-        WHERE id = #{userId} LIMIT 1
+        WHERE id = #{userId}
+        LIMIT 1
     </select>
 
-</mapper>
+</mapper>

+ 38 - 0
src/test/java/cc/uncarbon/test/InjectUnitTest.java

@@ -0,0 +1,38 @@
+package cc.uncarbon.test;
+
+import cc.uncarbon.HelioBootApplication;
+import cc.uncarbon.framework.core.exception.BusinessException;
+import cc.uncarbon.framework.core.props.HelioProperties;
+import cc.uncarbon.framework.crud.support.TenantSupport;
+import cc.uncarbon.framework.crud.support.impl.DefaultTenantSupport;
+import javax.annotation.Resource;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
+
+/**
+ * @author Uncarbon
+ */
+@SpringBootTest(classes = HelioBootApplication.class)
+class InjectUnitTest {
+
+    @Resource
+    private TenantSupport tenantSupport;
+
+    @Resource
+    private HelioProperties helioProperties;
+
+    @Value("${helio.tenant.enabled}")
+    private Boolean tenantEnabled;
+
+    @Test
+    void init() {
+        throw new BusinessException(500, "template test {} {} {} {} {}",
+                tenantEnabled,
+                helioProperties,
+                helioProperties.getTenant().getEnabled(),
+                helioProperties.getTenant().getIgnoredTables(),
+                tenantSupport.getClass().isAssignableFrom(DefaultTenantSupport.class)
+                );
+    }
+}