From cb2a4d7ea4749b79d4353e45126df55a0b469e6a Mon Sep 17 00:00:00 2001 From: chenxing Date: Tue, 11 Aug 2026 18:37:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=8E=A5=E5=8F=A3=E6=9B=B4?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/GroupAuthzService.java | 186 +++++++++++++++--- 1 file changed, 154 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java b/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java index f78fe7e..ddb82da 100644 --- a/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java +++ b/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java @@ -2,8 +2,10 @@ package com.yunhai.userbase.service.impl; import com.yunhai.platform.utils.exception.DException; import com.yunhai.userbase.entity.*; +import com.yunhai.userbase.enums.GroupTypeEnum; import com.yunhai.userbase.enums.IEnum; import com.yunhai.userbase.enums.SysModeEnum; +import com.yunhai.userbase.enums.UserTypeEnum; import com.yunhai.userbase.model.groupauthz.GroupAppAssignRequest; import com.yunhai.userbase.model.groupauthz.GroupAppVo; import com.yunhai.userbase.model.groupauthz.GroupAuthzAppVo; @@ -53,10 +55,11 @@ import java.util.Set; * @author chenx * @date 2026/7/23 * @description 组应用权限服务实现。
- * 功能1「应用开通」:sys_group_app_rel 全量覆盖式保存,开通不继承(子组须自己开通)。
- * 功能2「组授权」:sys_group_module_perm 全量覆盖式保存,inheritFlag=true 时子孙组自动继承。
- * 权限判定:沿 sys_group 祖先链内存回溯(树深度有限,MySQL 5.7 无递归 CTE,批量 IN 查询)。
- * 角色授权并集:最终模块权限 = 组授权模块 ∪ 启用角色授权模块;角色模块不要求组先开通应用(见 AI-FIX 标记)。
+ * 功能1「应用开通」:sys_group_app_rel 全量覆盖式保存,开通不继承(子组须自己开通)。
+ * 功能2「组授权」:sys_group_module_perm 全量覆盖式保存,inheritFlag=true + * 时子孙组自动继承。
+ * 权限判定:沿 sys_group 祖先链内存回溯(树深度有限,MySQL 5.7 无递归 CTE,批量 IN 查询)。
+ * 角色授权并集:最终模块权限 = 组授权模块 ∪ 启用角色授权模块;角色模块不要求组先开通应用(见 AI-FIX 标记)。
*/ @Service public class GroupAuthzService implements IGroupAuthzService { @@ -97,12 +100,14 @@ public class GroupAuthzService implements IGroupAuthzService { @Autowired private SysUserAppRoleRelRepository sysUserAppRoleRelRepository; - // ===================== 新增:沿祖先链收集「本组+所有祖先组开通的应用ID并集」(应用池联动GetSchoolAppId) ===================== + // ===================== 新增:沿祖先链收集「本组+所有祖先组开通的应用ID并集」(应用池联动GetSchoolAppId) + // ===================== /** * 按用户需求:父组(学校组)若开通了应用(GetSchoolAppId返回非空),则其所有子孙组的模块权限分配面板也能看到这些应用(无需子组重复开通)。
* 取应用范围=sys_group_app_rel里group_id属于「本组+全部祖先组」的app_id并集;开通不继承,模块权限池继承。
- * 对 hasModulePermission / UserPermissionTree 不做修改,避免越权(最终用户拿到权限仍需要自己的归属组开通应用+继承到模块权限,双保险)。 + * 对 hasModulePermission / UserPermissionTree + * 不做修改,避免越权(最终用户拿到权限仍需要自己的归属组开通应用+继承到模块权限,双保险)。 */ private Set collectAppIdsForAuthzScope(Integer groupId) { Map allGroupMap = buildAllGroupMap(); @@ -303,8 +308,8 @@ public class GroupAuthzService implements IGroupAuthzService { if (!sysGroupRepository.existsById(groupId)) { throw new DException("系统组不存在"); } - List perms = - request.getPerms() == null ? Collections.emptyList() : request.getPerms(); + List perms = request.getPerms() == null ? Collections.emptyList() + : request.getPerms(); Set appIdSet = collectAppIdsForAuthzScope(groupId); Set validModuleIds = new HashSet<>(); if (!appIdSet.isEmpty()) { @@ -558,7 +563,8 @@ public class GroupAuthzService implements IGroupAuthzService { } SysUserGroupRel existing = mainRelMap.get(rel.getUserId()); // 优先取主归属(isMain=1),否则保留第一个 - if (existing == null || (existing.getIsMain() == null || existing.getIsMain() != 1) && rel.getIsMain() != null && rel.getIsMain() == 1) { + if (existing == null || (existing.getIsMain() == null || existing.getIsMain() != 1) + && rel.getIsMain() != null && rel.getIsMain() == 1) { mainRelMap.put(rel.getUserId(), rel); } } @@ -588,6 +594,12 @@ public class GroupAuthzService implements IGroupAuthzService { if (user == null) { throw new DException("用户不存在"); } + // ============================================================ + // 超级管理员(UserTypeEnum.SUPER_ADMIN,type=1):直接返回所有已开通应用下的全部模块,不做任何组/继承判定 + // ============================================================ + if (isSuperAdmin(user.getType())) { + return buildSuperAdminAllPermissions(); + } // AI-FIX(角色授权): 先取用户经「启用」角色获得的模块ID集合,与组授权做并集; // 角色授权不依赖组归属与组开通应用,故下方不再因无组归属而提前返回 Set roleModuleIds = collectEnabledRoleModuleIds(userId); @@ -612,9 +624,10 @@ public class GroupAuthzService implements IGroupAuthzService { } // 3. 收集「用户归属组 + 其祖先链」所有组已开通的应用并集。 - // 安全:hasUserModulePermission 仍会判定 groupId(当前归属组)必须在 openedApps 链上(即归属组或其祖先开通应用), - // 不会出现“无组归属、父级未开”的越权;此处只是把过滤和写面板(GroupModuleTreeByCategory)联动对齐, - // 解决之前“管理员在组8面板勾了checked=true,但UserPermissionTree返回空”的对称bug。 + // 安全:hasUserModulePermission 仍会判定 groupId(当前归属组)必须在 openedApps + // 链上(即归属组或其祖先开通应用), + // 不会出现“无组归属、父级未开”的越权;此处只是把过滤和写面板(GroupModuleTreeByCategory)联动对齐, + // 解决之前“管理员在组8面板勾了checked=true,但UserPermissionTree返回空”的对称bug。 Map> groupToApps = new HashMap<>(); Set allAppIds = new LinkedHashSet<>(); for (Integer groupIdItem : selfGroupIds) { @@ -672,7 +685,8 @@ public class GroupAuthzService implements IGroupAuthzService { Set permittedModuleIds = new LinkedHashSet<>(); for (AppModule m : allModules) { if (roleModuleIds.contains(m.getId()) - || hasUserModulePermission(m.getId(), m.getAppId(), selfGroupIds, chainMap, groupToApps, modulePermMap)) { + || hasUserModulePermission(m.getId(), m.getAppId(), selfGroupIds, chainMap, groupToApps, + modulePermMap, allGroupMap)) { permittedModuleIds.add(m.getId()); } } @@ -839,14 +853,25 @@ public class GroupAuthzService implements IGroupAuthzService { } /** - * 用户维度权限判定:用户对指定模块是否有权限。
- * 复用 hasModulePermission 的祖先链继承逻辑,但基于已预热的批量查询结果。 + * 用户维度权限判定:用户对指定模块是否有权限(两段式 + 学校截止 + 超管兜底(超管在入口短路,这里只处理普通用户))。 + * + *

+ * 两段式(用户拍板:边界1选B): + * ① 先查「本组」(归属组 selfGroupId)有没有 sys_group_module_perm + * 记录(仅记录存在即=显式设置,无论inherit_flag值)→ 有 = 本组说了算,直接true,不再查祖先; + * ② 本组无记录 = 没有显式设置 = 进入继承查找:沿祖先链向上找,直到第一个学校组(包含学校组本身,不跨学校到教育局/根),第一个「记录存在且 + * inherit_flag=true」的组即继承成功; + * ③ 学校组之上的节点(区域/教育局/根)都不查找,避免跨学校继承到未开通的应用(用户新增边界3)。 + * + *

+ * 其余不变:归属组或其祖先需已开通该模块所属应用(双保险,防越权)。 */ private boolean hasUserModulePermission(Integer moduleId, Integer appId, - Set selfGroupIds, - Map> chainMap, - Map> groupToApps, - Map> modulePermMap) { + Set selfGroupIds, + Map> chainMap, + Map> groupToApps, + Map> modulePermMap, + Map allGroupMap) { Map permMap = modulePermMap.get(moduleId); if (permMap == null || permMap.isEmpty()) { return false; @@ -860,27 +885,124 @@ public class GroupAuthzService implements IGroupAuthzService { if (chain == null || chain.isEmpty()) { continue; } + // 第一段:本组显式设置优先(记录存在=true,完全按本组来,不继承不向上) + SysGroupModulePerm selfPerm = permMap.get(selfGroupId); + if (selfPerm != null) { + return true; + } + // 第二段:本组无记录 → 向上继承查找;遇到第一个学校组(SCHOOL_GROUP)就包含它自己,处理完停止(不跨学校) for (Integer ancestorId : chain) { - SysGroupModulePerm perm = permMap.get(ancestorId); - if (perm == null) { + if (Objects.equals(ancestorId, selfGroupId)) { continue; } - if (Objects.equals(ancestorId, selfGroupId)) { + SysGroup g = allGroupMap.get(ancestorId); + SysGroupModulePerm perm = permMap.get(ancestorId); + boolean isSchool = g != null && g.getType() == GroupTypeEnum.SCHOOL_GROUP; + if (perm != null && Boolean.TRUE.equals(perm.getInheritFlag())) { return true; } - if (Boolean.TRUE.equals(perm.getInheritFlag())) { - return true; + // 用户要求:向上查找只查到学校组这一层,包含学校组本身(上面已判断学校组的perm),之后不再跨学校 + if (isSchool) { + break; } } } return false; } + // ===================== 超级管理员 & 工具方法 ===================== + + /** + * 超级管理员 type=1(UserTypeEnum.SUPER_ADMIN / DicUtil.USER_TYPE_SUPER_MANAGER=1) + */ + private static boolean isSuperAdmin(UserTypeEnum userType) { + return userType != null && UserTypeEnum.SUPER_ADMIN.getKey().equals(userType.getKey()); + } + + /** + * 超级管理员全模块权限:不依赖组/继承,直接「所有已分类应用下的模块」全量返回。 + * 应用范围 = 所有已分类(category 存在)且有模块的应用,按应用过滤构建分类→应用→模块树。 + */ + private List buildSuperAdminAllPermissions() { + List categories = appCategoryRepository.findAllByOrderBySortNoAsc(); + List allApps = applicationInfoRepository.findAll(); + allApps.sort(appOrder()); + Map appMap = new HashMap<>(); + Set allAppIds = new LinkedHashSet<>(); + for (ApplicationInfo app : allApps) { + appMap.put(app.getId(), app); + allAppIds.add(app.getId()); + } + if (allAppIds.isEmpty()) { + return Collections.emptyList(); + } + List allModules = appModuleRepository.findByAppIdIn(new ArrayList<>(allAppIds)); + if (allModules.isEmpty()) { + return Collections.emptyList(); + } + Map> appModulesMap = new HashMap<>(); + Set allModuleIds = new LinkedHashSet<>(); + for (AppModule m : allModules) { + allModuleIds.add(m.getId()); + appModulesMap.computeIfAbsent(m.getAppId(), k -> new ArrayList<>()).add(m); + } + Map> categoryAppMap = new HashMap<>(); + for (Map.Entry> entry : appModulesMap.entrySet()) { + Integer appId = entry.getKey(); + ApplicationInfo app = appMap.get(appId); + if (app == null) { + continue; + } + List moduleTree = buildUserPermissionModuleTree(entry.getValue(), allModuleIds); + if (moduleTree.isEmpty()) { + continue; + } + UserPermissionAppVo appVo = new UserPermissionAppVo(); + appVo.setId(app.getId()); + appVo.setName(app.getName()); + appVo.setAppKey(app.getAppKey()); + appVo.setPcIcon(app.getPcIcon()); + appVo.setSortNo(app.getSortNo()); + appVo.setModules(moduleTree); + Integer categoryId = app.getCategoryId() == null ? 0 : app.getCategoryId(); + categoryAppMap.computeIfAbsent(categoryId, k -> new ArrayList<>()).add(appVo); + } + if (categoryAppMap.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (AppCategory category : categories) { + List appVos = categoryAppMap.remove(category.getId()); + if (appVos == null || appVos.isEmpty()) { + continue; + } + appVos.sort(userPermissionAppOrder()); + UserPermissionCategoryVo c = new UserPermissionCategoryVo(); + c.setId(category.getId()); + c.setName(category.getName()); + c.setSortNo(category.getSortNo()); + c.setApps(appVos); + result.add(c); + } + List remain = categoryAppMap.remove(0); + if (remain != null && !remain.isEmpty()) { + remain.sort(userPermissionAppOrder()); + UserPermissionCategoryVo uncategorized = new UserPermissionCategoryVo(); + uncategorized.setId(0); + uncategorized.setName("未分类"); + uncategorized.setSortNo(Integer.MAX_VALUE); + uncategorized.setApps(remain); + result.add(uncategorized); + } + result.sort(Comparator.comparingInt(c -> c.getSortNo() == null ? Integer.MAX_VALUE : c.getSortNo())); + return result; + } + /** * 构建用户权限一览的模块树:仅保留有权限的模块,父节点无权限时子节点挂到根级。 */ private List buildUserPermissionModuleTree(List modules, - Set permittedModuleIds) { + Set permittedModuleIds) { // 先过滤 List permitted = new ArrayList<>(); for (AppModule m : modules) { @@ -918,7 +1040,7 @@ public class GroupAuthzService implements IGroupAuthzService { } private UserPermissionModuleVo buildUserPermissionModuleNode(AppModule node, - Map> childrenMap) { + Map> childrenMap) { UserPermissionModuleVo vo = new UserPermissionModuleVo(); BeanUtils.copyProperties(node, vo); List children = childrenMap.get(node.getId()); @@ -1009,7 +1131,7 @@ public class GroupAuthzService implements IGroupAuthzService { allGroupMap.put(g.getId(), g); } // 3. 前置校验:所属组必须已开通模块所属应用(最终权限 = 继承的模块权限 ∩ 已开通应用); - // 通过校验的组构建祖先链(含自身),并收集祖先链并集用于一次批量 IN 查询 + // 通过校验的组构建祖先链(含自身),并收集祖先链并集用于一次批量 IN 查询 Map> chainMap = new HashMap<>(); Set allChainIds = new LinkedHashSet<>(); for (SysUserGroupRel rel : userRels) { @@ -1029,8 +1151,8 @@ public class GroupAuthzService implements IGroupAuthzService { } // 4. 一次批量查询目标模块在所有祖先链节点上的权限记录 Map permMap = new HashMap<>(); - List perms = - sysGroupModulePermRepository.findByModuleIdAndGroupIdIn(moduleId, new ArrayList<>(allChainIds)); + List perms = sysGroupModulePermRepository.findByModuleIdAndGroupIdIn(moduleId, + new ArrayList<>(allChainIds)); for (SysGroupModulePerm perm : perms) { permMap.put(perm.getGroupId(), perm); } @@ -1081,7 +1203,7 @@ public class GroupAuthzService implements IGroupAuthzService { * 组装单个应用下的模块权限树(沿用 AppModuleService 的组装方式:childrenMap + sortNo 排序)。
*/ private List buildModuleTree(List modules, - Map permMap) { + Map permMap) { // parentId -> 子节点列表(null 视为 0) Map> childrenMap = new HashMap<>(); for (AppModule m : modules) { @@ -1112,8 +1234,8 @@ public class GroupAuthzService implements IGroupAuthzService { * 递归组装模块节点,并按权限记录回显 checked / inheritFlag。
*/ private GroupAuthzModuleVo buildModuleNode(AppModule node, - Map> childrenMap, - Map permMap) { + Map> childrenMap, + Map permMap) { GroupAuthzModuleVo vo = new GroupAuthzModuleVo(); BeanUtils.copyProperties(node, vo); SysGroupModulePerm perm = permMap.get(node.getId());