模块接口更改

This commit is contained in:
chenxing
2026-08-11 18:37:08 +08:00
parent 97959757d0
commit cb2a4d7ea4
@@ -2,8 +2,10 @@ package com.yunhai.userbase.service.impl;
import com.yunhai.platform.utils.exception.DException; import com.yunhai.platform.utils.exception.DException;
import com.yunhai.userbase.entity.*; import com.yunhai.userbase.entity.*;
import com.yunhai.userbase.enums.GroupTypeEnum;
import com.yunhai.userbase.enums.IEnum; import com.yunhai.userbase.enums.IEnum;
import com.yunhai.userbase.enums.SysModeEnum; 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.GroupAppAssignRequest;
import com.yunhai.userbase.model.groupauthz.GroupAppVo; import com.yunhai.userbase.model.groupauthz.GroupAppVo;
import com.yunhai.userbase.model.groupauthz.GroupAuthzAppVo; import com.yunhai.userbase.model.groupauthz.GroupAuthzAppVo;
@@ -54,7 +56,8 @@ import java.util.Set;
* @date 2026/7/23 * @date 2026/7/23
* @description 组应用权限服务实现。<br> * @description 组应用权限服务实现。<br>
* 功能1「应用开通」:sys_group_app_rel 全量覆盖式保存,开通不继承(子组须自己开通)。<br> * 功能1「应用开通」:sys_group_app_rel 全量覆盖式保存,开通不继承(子组须自己开通)。<br>
* 功能2「组授权」:sys_group_module_perm 全量覆盖式保存,inheritFlag=true 时子孙组自动继承。<br> * 功能2「组授权」:sys_group_module_perm 全量覆盖式保存,inheritFlag=true
* 时子孙组自动继承。<br>
* 权限判定:沿 sys_group 祖先链内存回溯(树深度有限,MySQL 5.7 无递归 CTE,批量 IN 查询)。<br> * 权限判定:沿 sys_group 祖先链内存回溯(树深度有限,MySQL 5.7 无递归 CTE,批量 IN 查询)。<br>
* 角色授权并集:最终模块权限 = 组授权模块 ∪ 启用角色授权模块;角色模块不要求组先开通应用(见 AI-FIX 标记)。<br> * 角色授权并集:最终模块权限 = 组授权模块 ∪ 启用角色授权模块;角色模块不要求组先开通应用(见 AI-FIX 标记)。<br>
*/ */
@@ -97,12 +100,14 @@ public class GroupAuthzService implements IGroupAuthzService {
@Autowired @Autowired
private SysUserAppRoleRelRepository sysUserAppRoleRelRepository; private SysUserAppRoleRelRepository sysUserAppRoleRelRepository;
// ===================== 新增:沿祖先链收集「本组+所有祖先组开通的应用ID并集」(应用池联动GetSchoolAppId) ===================== // ===================== 新增:沿祖先链收集「本组+所有祖先组开通的应用ID并集」(应用池联动GetSchoolAppId)
// =====================
/** /**
* 按用户需求:父组(学校组)若开通了应用(GetSchoolAppId返回非空),则其所有子孙组的模块权限分配面板也能看到这些应用(无需子组重复开通)。<br> * 按用户需求:父组(学校组)若开通了应用(GetSchoolAppId返回非空),则其所有子孙组的模块权限分配面板也能看到这些应用(无需子组重复开通)。<br>
* 取应用范围=sys_group_app_rel里group_id属于「本组+全部祖先组」的app_id并集;开通不继承,模块权限池继承。<br> * 取应用范围=sys_group_app_rel里group_id属于「本组+全部祖先组」的app_id并集;开通不继承,模块权限池继承。<br>
* 对 hasModulePermission / UserPermissionTree 不做修改,避免越权(最终用户拿到权限仍需要自己的归属组开通应用+继承到模块权限,双保险)。 * 对 hasModulePermission / UserPermissionTree
* 不做修改,避免越权(最终用户拿到权限仍需要自己的归属组开通应用+继承到模块权限,双保险)。
*/ */
private Set<Integer> collectAppIdsForAuthzScope(Integer groupId) { private Set<Integer> collectAppIdsForAuthzScope(Integer groupId) {
Map<Integer, SysGroup> allGroupMap = buildAllGroupMap(); Map<Integer, SysGroup> allGroupMap = buildAllGroupMap();
@@ -303,8 +308,8 @@ public class GroupAuthzService implements IGroupAuthzService {
if (!sysGroupRepository.existsById(groupId)) { if (!sysGroupRepository.existsById(groupId)) {
throw new DException("系统组不存在"); throw new DException("系统组不存在");
} }
List<GroupModulePermRequest.ModulePermItem> perms = List<GroupModulePermRequest.ModulePermItem> perms = request.getPerms() == null ? Collections.emptyList()
request.getPerms() == null ? Collections.emptyList() : request.getPerms(); : request.getPerms();
Set<Integer> appIdSet = collectAppIdsForAuthzScope(groupId); Set<Integer> appIdSet = collectAppIdsForAuthzScope(groupId);
Set<Integer> validModuleIds = new HashSet<>(); Set<Integer> validModuleIds = new HashSet<>();
if (!appIdSet.isEmpty()) { if (!appIdSet.isEmpty()) {
@@ -558,7 +563,8 @@ public class GroupAuthzService implements IGroupAuthzService {
} }
SysUserGroupRel existing = mainRelMap.get(rel.getUserId()); SysUserGroupRel existing = mainRelMap.get(rel.getUserId());
// 优先取主归属(isMain=1),否则保留第一个 // 优先取主归属(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); mainRelMap.put(rel.getUserId(), rel);
} }
} }
@@ -588,6 +594,12 @@ public class GroupAuthzService implements IGroupAuthzService {
if (user == null) { if (user == null) {
throw new DException("用户不存在"); throw new DException("用户不存在");
} }
// ============================================================
// 超级管理员(UserTypeEnum.SUPER_ADMIN,type=1):直接返回所有已开通应用下的全部模块,不做任何组/继承判定
// ============================================================
if (isSuperAdmin(user.getType())) {
return buildSuperAdminAllPermissions();
}
// AI-FIX(角色授权): 先取用户经「启用」角色获得的模块ID集合,与组授权做并集; // AI-FIX(角色授权): 先取用户经「启用」角色获得的模块ID集合,与组授权做并集;
// 角色授权不依赖组归属与组开通应用,故下方不再因无组归属而提前返回 // 角色授权不依赖组归属与组开通应用,故下方不再因无组归属而提前返回
Set<Integer> roleModuleIds = collectEnabledRoleModuleIds(userId); Set<Integer> roleModuleIds = collectEnabledRoleModuleIds(userId);
@@ -612,7 +624,8 @@ public class GroupAuthzService implements IGroupAuthzService {
} }
// 3. 收集「用户归属组 + 其祖先链」所有组已开通的应用并集。 // 3. 收集「用户归属组 + 其祖先链」所有组已开通的应用并集。
// 安全:hasUserModulePermission 仍会判定 groupId(当前归属组)必须在 openedApps 链上(即归属组或其祖先开通应用), // 安全:hasUserModulePermission 仍会判定 groupId(当前归属组)必须在 openedApps
// 链上(即归属组或其祖先开通应用),
// 不会出现“无组归属、父级未开”的越权;此处只是把过滤和写面板(GroupModuleTreeByCategory)联动对齐, // 不会出现“无组归属、父级未开”的越权;此处只是把过滤和写面板(GroupModuleTreeByCategory)联动对齐,
// 解决之前“管理员在组8面板勾了checked=true,但UserPermissionTree返回空”的对称bug。 // 解决之前“管理员在组8面板勾了checked=true,但UserPermissionTree返回空”的对称bug。
Map<Integer, Set<Integer>> groupToApps = new HashMap<>(); Map<Integer, Set<Integer>> groupToApps = new HashMap<>();
@@ -672,7 +685,8 @@ public class GroupAuthzService implements IGroupAuthzService {
Set<Integer> permittedModuleIds = new LinkedHashSet<>(); Set<Integer> permittedModuleIds = new LinkedHashSet<>();
for (AppModule m : allModules) { for (AppModule m : allModules) {
if (roleModuleIds.contains(m.getId()) 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()); permittedModuleIds.add(m.getId());
} }
} }
@@ -839,14 +853,25 @@ public class GroupAuthzService implements IGroupAuthzService {
} }
/** /**
* 用户维度权限判定:用户对指定模块是否有权限。<br> * 用户维度权限判定:用户对指定模块是否有权限(两段式 + 学校截止 + 超管兜底(超管在入口短路,这里只处理普通用户))。
* 复用 hasModulePermission 的祖先链继承逻辑,但基于已预热的批量查询结果。 *
* <p>
* 两段式(用户拍板:边界1选B):
* ① 先查「本组」(归属组 selfGroupId)有没有 sys_group_module_perm
* 记录(仅记录存在即=显式设置,无论inherit_flag值)→ 有 = 本组说了算,直接true,不再查祖先;
* ② 本组无记录 = 没有显式设置 = 进入继承查找:沿祖先链向上找,直到第一个学校组(包含学校组本身,不跨学校到教育局/根),第一个「记录存在且
* inherit_flag=true」的组即继承成功;
* ③ 学校组之上的节点(区域/教育局/根)都不查找,避免跨学校继承到未开通的应用(用户新增边界3)。
*
* <p>
* 其余不变:归属组或其祖先需已开通该模块所属应用(双保险,防越权)。
*/ */
private boolean hasUserModulePermission(Integer moduleId, Integer appId, private boolean hasUserModulePermission(Integer moduleId, Integer appId,
Set<Integer> selfGroupIds, Set<Integer> selfGroupIds,
Map<Integer, List<Integer>> chainMap, Map<Integer, List<Integer>> chainMap,
Map<Integer, Set<Integer>> groupToApps, Map<Integer, Set<Integer>> groupToApps,
Map<Integer, Map<Integer, SysGroupModulePerm>> modulePermMap) { Map<Integer, Map<Integer, SysGroupModulePerm>> modulePermMap,
Map<Integer, SysGroup> allGroupMap) {
Map<Integer, SysGroupModulePerm> permMap = modulePermMap.get(moduleId); Map<Integer, SysGroupModulePerm> permMap = modulePermMap.get(moduleId);
if (permMap == null || permMap.isEmpty()) { if (permMap == null || permMap.isEmpty()) {
return false; return false;
@@ -860,22 +885,119 @@ public class GroupAuthzService implements IGroupAuthzService {
if (chain == null || chain.isEmpty()) { if (chain == null || chain.isEmpty()) {
continue; continue;
} }
// 第一段:本组显式设置优先(记录存在=true,完全按本组来,不继承不向上)
SysGroupModulePerm selfPerm = permMap.get(selfGroupId);
if (selfPerm != null) {
return true;
}
// 第二段:本组无记录 → 向上继承查找;遇到第一个学校组(SCHOOL_GROUP)就包含它自己,处理完停止(不跨学校)
for (Integer ancestorId : chain) { for (Integer ancestorId : chain) {
SysGroupModulePerm perm = permMap.get(ancestorId); if (Objects.equals(ancestorId, selfGroupId)) {
if (perm == null) {
continue; 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; return true;
} }
if (Boolean.TRUE.equals(perm.getInheritFlag())) { // 用户要求:向上查找只查到学校组这一层,包含学校组本身(上面已判断学校组的perm),之后不再跨学校
return true; if (isSchool) {
break;
} }
} }
} }
return false; 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<UserPermissionCategoryVo> buildSuperAdminAllPermissions() {
List<AppCategory> categories = appCategoryRepository.findAllByOrderBySortNoAsc();
List<ApplicationInfo> allApps = applicationInfoRepository.findAll();
allApps.sort(appOrder());
Map<Integer, ApplicationInfo> appMap = new HashMap<>();
Set<Integer> allAppIds = new LinkedHashSet<>();
for (ApplicationInfo app : allApps) {
appMap.put(app.getId(), app);
allAppIds.add(app.getId());
}
if (allAppIds.isEmpty()) {
return Collections.emptyList();
}
List<AppModule> allModules = appModuleRepository.findByAppIdIn(new ArrayList<>(allAppIds));
if (allModules.isEmpty()) {
return Collections.emptyList();
}
Map<Integer, List<AppModule>> appModulesMap = new HashMap<>();
Set<Integer> allModuleIds = new LinkedHashSet<>();
for (AppModule m : allModules) {
allModuleIds.add(m.getId());
appModulesMap.computeIfAbsent(m.getAppId(), k -> new ArrayList<>()).add(m);
}
Map<Integer, List<UserPermissionAppVo>> categoryAppMap = new HashMap<>();
for (Map.Entry<Integer, List<AppModule>> entry : appModulesMap.entrySet()) {
Integer appId = entry.getKey();
ApplicationInfo app = appMap.get(appId);
if (app == null) {
continue;
}
List<UserPermissionModuleVo> 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<UserPermissionCategoryVo> result = new ArrayList<>();
for (AppCategory category : categories) {
List<UserPermissionAppVo> 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<UserPermissionAppVo> 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;
}
/** /**
* 构建用户权限一览的模块树:仅保留有权限的模块,父节点无权限时子节点挂到根级。 * 构建用户权限一览的模块树:仅保留有权限的模块,父节点无权限时子节点挂到根级。
*/ */
@@ -1029,8 +1151,8 @@ public class GroupAuthzService implements IGroupAuthzService {
} }
// 4. 一次批量查询目标模块在所有祖先链节点上的权限记录 // 4. 一次批量查询目标模块在所有祖先链节点上的权限记录
Map<Integer, SysGroupModulePerm> permMap = new HashMap<>(); Map<Integer, SysGroupModulePerm> permMap = new HashMap<>();
List<SysGroupModulePerm> perms = List<SysGroupModulePerm> perms = sysGroupModulePermRepository.findByModuleIdAndGroupIdIn(moduleId,
sysGroupModulePermRepository.findByModuleIdAndGroupIdIn(moduleId, new ArrayList<>(allChainIds)); new ArrayList<>(allChainIds));
for (SysGroupModulePerm perm : perms) { for (SysGroupModulePerm perm : perms) {
permMap.put(perm.getGroupId(), perm); permMap.put(perm.getGroupId(), perm);
} }