模块权限修订
ci-userbase-service / build-and-push (push) Successful in 1m50s

This commit is contained in:
chenxing
2026-08-11 21:38:14 +08:00
parent 54f9fb7e3f
commit 32d03e3eb4
@@ -604,18 +604,26 @@ public class GroupAuthzService implements IGroupAuthzService {
// 角色授权不依赖组归属与组开通应用,故下方不再因无组归属而提前返回
Set<Integer> roleModuleIds = collectEnabledRoleModuleIds(userId);
// 1. 用户所属的全部组 + 【强制包含接口传入的 groupId 上下文】
// 场景修复:管理员在「组8的授权面板」给模块M打勾(写 sys_group_module_perm 组8的记录),
// 然后同面板点「用户权限一览」UserPermissionTree?userId=X&groupId=8,必须读到组8的设置,
// 即便用户X还没加入组8(预览模式、或界面只做"组视角下的权限查询");否则"本组设置"不生效。
// 1. 归属组集合:区分「组上下文模式」和「用户全局模式」
// A) 接口显式传了 groupId(= 管理员在"某个组的授权面板"点用户权限一览,是典型的组视角查询):
// 以传入的 groupId 作为唯一 selfGroupId,仅沿「该组 -> 祖先 -> 第一个学校组(含)」链路判定;
// 【关键】不再把用户真实加入的其他组(例如用户还属于学校组)并入判断。
// 原因:如果允许用户归属的"学校组"作为独立 selfGroupId 参与本组显式判定,
// 则子组未设置的模块会直接命中学校组自己的显式记录(permMap.get(学校组)!=null),
// 导致"子组设置的覆盖限制"完全失效=父级所有模块都拿到,即你观察到的"本组继承没生效/还是父级全量"。
// B) 未传 groupId(全局查用户权限/开放接口 modules 未传schId以外的其他上下文):
// 正常取用户所有真实归属组并集。
List<SysUserGroupRel> userRels = sysUserGroupRelRepository.findByUserId(userId);
Set<Integer> selfGroupIds = new LinkedHashSet<>();
if (groupId != null) {
// 组上下文模式:只认传入的 groupId,不认其他真实归属组(避免越级命中显式记录)
selfGroupIds.add(groupId);
}
for (SysUserGroupRel rel : userRels) {
if (rel.getGroupId() != null) {
selfGroupIds.add(rel.getGroupId());
} else {
// 用户全局模式:按真实归属组并集
for (SysUserGroupRel rel : userRels) {
if (rel.getGroupId() != null) {
selfGroupIds.add(rel.getGroupId());
}
}
}
@@ -667,9 +675,11 @@ public class GroupAuthzService implements IGroupAuthzService {
}
Set<Integer> allModuleIds = new LinkedHashSet<>();
Map<Integer, List<AppModule>> appModulesMap = new HashMap<>();
Map<Integer, Integer> moduleToAppMap = new HashMap<>();
for (AppModule m : allModules) {
allModuleIds.add(m.getId());
appModulesMap.computeIfAbsent(m.getAppId(), k -> new ArrayList<>()).add(m);
moduleToAppMap.put(m.getId(), m.getAppId());
}
// 6. 批量查询所有相关模块在所有相关组/祖先上的权限记录
@@ -686,13 +696,26 @@ public class GroupAuthzService implements IGroupAuthzService {
}
}
// AI-FIX(应用级覆盖): 某组只要对该应用有任意显式模块配置,子孙/本组就只按该组配置判定,
// 不再继承更上层祖先的同应用模块权限(避免「子组只勾了1个模块,却仍继承学校全部模块」)。
Map<Integer, Set<Integer>> groupConfiguredApps = new HashMap<>();
for (Map.Entry<Integer, Map<Integer, SysGroupModulePerm>> entry : modulePermMap.entrySet()) {
Integer appId = moduleToAppMap.get(entry.getKey());
if (appId == null) {
continue;
}
for (Integer gid : entry.getValue().keySet()) {
groupConfiguredApps.computeIfAbsent(gid, k -> new HashSet<>()).add(appId);
}
}
// 7. 判定每个模块是否有权限
// AI-FIX(角色授权): 最终模块权限 = 组授权模块 ∪ 角色授权模块
Set<Integer> permittedModuleIds = new LinkedHashSet<>();
for (AppModule m : allModules) {
if (roleModuleIds.contains(m.getId())
|| hasUserModulePermission(m.getId(), m.getAppId(), selfGroupIds, chainMap, groupToApps,
modulePermMap, allGroupMap)) {
modulePermMap, allGroupMap, groupConfiguredApps)) {
permittedModuleIds.add(m.getId());
}
}
@@ -859,29 +882,57 @@ public class GroupAuthzService implements IGroupAuthzService {
}
/**
* 用户维度权限判定:用户对指定模块是否有权限(两段式 + 学校截止 + 超管兜底(超管在入口短路,这里只处理普通用户))。
* 就近原则:在 groupId 的祖先链上,找到离它最近的、对目标应用有显式模块配置的组(含自身)。
*
* <p>
* 两段式(用户拍板:边界1选B):
* ① 先查「本组」(归属组 selfGroupId)有没有 sys_group_module_perm
* 记录(仅记录存在即=显式设置,无论inherit_flag值)→ 有 = 本组说了算,直接true,不再查祖先;
* ② 本组无记录 = 没有显式设置 = 进入继承查找:沿祖先链向上找,直到第一个学校组(包含学校组本身,不跨学校到教育局/根),第一个「记录存在且
* inherit_flag=true」的组即继承成功;
* ③ 学校组之上的节点(区域/教育局/根)都不查找,避免跨学校继承到未开通的应用(用户新增边界3)。
* <p>链首即自身({@link #ancestorChain} 由自身向根排列),天然「就近优先」;遍历时第一个
* 「groupConfiguredApps 中包含该应用」的组即作为该应用在本组范围内的<b>唯一权限来源</b>返回。
*
* <p>
* 其余不变:归属组或其祖先需已开通该模块所属应用(双保险,防越权)。
* <p>学校截止:遇到学校组(无论是否已配置)即作为继承上限,不再向教育局/根越级,避免跨学校继承到未开通的应用。
*
* <p>找不到返回 null(该应用在此范围内没有任何组的显式配置,对应模块不许可)。
*/
private Integer findNearestAuthorityGroup(Integer groupId, List<Integer> chain,
Integer appId, Map<Integer, Set<Integer>> groupConfiguredApps,
Map<Integer, SysGroup> allGroupMap) {
if (chain == null || chain.isEmpty()) {
return null;
}
for (Integer gid : chain) {
Set<Integer> apps = groupConfiguredApps.get(gid);
if (apps != null && apps.contains(appId)) {
return gid;
}
SysGroup g = allGroupMap.get(gid);
if (g != null && g.getType() == GroupTypeEnum.SCHOOL_GROUP) {
break;
}
}
return null;
}
/**
* 用户维度权限判定:用户对指定模块是否有权限(就近原则 + 学校截止 + 角色授权并集 + 超管兜底)。
*
* <p><b>就近原则</b>(统一了「本组覆盖」与「最近祖先继承」两种情形,修复两类合并 bug):
* 沿 selfGroupId 的祖先链(含自身)向上,找到「离它最近、且对 appId 有任意显式模块配置」的组作为
* 该应用的<b>唯一权限来源</b>;该模块被许可,当且仅当这个来源组对它自身有显式 {@code sys_group_module_perm} 记录。
* 这样:
* ① 本组有配置(如英语组只勾模块8)→ 本组即来源,只返回本组勾选的模块,不再合并学校全部模块;
* ② 本组无配置(如英语组为空)→ 取<b>最近</b>的有配置祖先(如教师组)为来源,不再越级到更远的学校组;
* ③ 来源组的模块集合即最终集合,更上层同应用模块(如学校组多勾的模块8)一律不合并。
*
* <p><b>学校截止</b>:祖先链只查到第一个学校组(含)为止,不跨学校到教育局/根。
*
* <p><b>其余</b>:归属组需已开通该模块所属应用(双保险,防越权)。
*/
private boolean hasUserModulePermission(Integer moduleId, Integer appId,
Set<Integer> selfGroupIds,
Map<Integer, List<Integer>> chainMap,
Map<Integer, Set<Integer>> groupToApps,
Map<Integer, Map<Integer, SysGroupModulePerm>> modulePermMap,
Map<Integer, SysGroup> allGroupMap) {
Map<Integer, SysGroup> allGroupMap,
Map<Integer, Set<Integer>> groupConfiguredApps) {
Map<Integer, SysGroupModulePerm> permMap = modulePermMap.get(moduleId);
if (permMap == null || permMap.isEmpty()) {
return false;
}
for (Integer selfGroupId : selfGroupIds) {
Set<Integer> openedApps = groupToApps.get(selfGroupId);
if (openedApps == null || !openedApps.contains(appId)) {
@@ -891,33 +942,14 @@ public class GroupAuthzService implements IGroupAuthzService {
if (chain == null || chain.isEmpty()) {
continue;
}
// 第一段:本组显式设置优先(记录存在=true,完全按本组来,不继承不向上)
SysGroupModulePerm selfPerm = permMap.get(selfGroupId);
if (selfPerm != null) {
return true;
}
// 本组无记录 → 第二段:继承查找。
// 如果「本组本身就是学校组」:学校组已经是继承上限,学校组之上(区域/教育局/根)不再允许查找,直接跳过本段。
// 否则(本组是班级/年级/教师/学生等学校内子组):沿祖先向上,直到包含第一个学校组为止,出了学校组立刻break,不跨学校。
SysGroup selfG = allGroupMap.get(selfGroupId);
boolean selfIsSchool = selfG != null && selfG.getType() == GroupTypeEnum.SCHOOL_GROUP;
if (selfIsSchool) {
// 就近原则:取离本组最近、对该应用有显式配置的组作为唯一权限来源
Integer authority = findNearestAuthorityGroup(selfGroupId, chain, appId, groupConfiguredApps, allGroupMap);
if (authority == null) {
continue;
}
for (Integer ancestorId : chain) {
if (Objects.equals(ancestorId, selfGroupId)) {
continue;
}
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;
}
// 用户要求:向上查找只查到学校组这一层,包含学校组本身(上面已判断学校组的perm),之后不再跨学校
if (isSchool) {
break;
}
// 权限来源组对该模块有显式记录 → 许可;否则(来源组未勾此模块)不许可
if (permMap != null && permMap.get(authority) != null) {
return true;
}
}
return false;
@@ -1135,6 +1167,7 @@ public class GroupAuthzService implements IGroupAuthzService {
if (module == null) {
return false;
}
Integer appId = module.getAppId();
// AI-FIX(角色授权): 用户经「启用」角色获得该模块授权即通过,不要求组开通模块所属应用;禁用角色已在收集时过滤
if (collectEnabledRoleModuleIds(userId).contains(moduleId)) {
return true;
@@ -1149,18 +1182,20 @@ public class GroupAuthzService implements IGroupAuthzService {
for (SysGroup g : sysGroupRepository.findAll()) {
allGroupMap.put(g.getId(), g);
}
// 3. 前置校验:所属组必须已开通模块所属应用(最终权限 = 继承的模块权限 ∩ 已开通应用);
// 3. 前置校验:所属组必须已开通模块所属应用(最终权限 = 就近来源模块权限 ∩ 已开通应用);
// 通过校验的组构建祖先链(含自身),并收集祖先链并集用于一次批量 IN 查询
Map<Integer, List<Integer>> chainMap = new HashMap<>();
Set<Integer> allChainIds = new LinkedHashSet<>();
Set<Integer> selfGroupIds = new LinkedHashSet<>();
for (SysUserGroupRel rel : userRels) {
Integer groupId = rel.getGroupId();
if (groupId == null || chainMap.containsKey(groupId)) {
continue;
}
if (!sysGroupAppRelRepository.existsByGroupIdAndAppId(groupId, module.getAppId())) {
if (!sysGroupAppRelRepository.existsByGroupIdAndAppId(groupId, appId)) {
continue;
}
selfGroupIds.add(groupId);
List<Integer> chain = ancestorChain(groupId, allGroupMap);
chainMap.put(groupId, chain);
allChainIds.addAll(chain);
@@ -1168,30 +1203,38 @@ public class GroupAuthzService implements IGroupAuthzService {
if (chainMap.isEmpty()) {
return false;
}
// 4. 一次批量查询目标模块在所有祖先链节点上的权限记录
Map<Integer, SysGroupModulePerm> permMap = new HashMap<>();
List<SysGroupModulePerm> perms = sysGroupModulePermRepository.findByModuleIdAndGroupIdIn(moduleId,
new ArrayList<>(allChainIds));
for (SysGroupModulePerm perm : perms) {
permMap.put(perm.getGroupId(), perm);
// 4. 一次批量查询「该应用下所有模块」在全部祖先链节点上的权限记录
// (要判定就近来源,需要知道链上哪些组对该应用有显式配置,不能只看单模块)
List<AppModule> appModules = appModuleRepository.findByAppIdIn(Collections.singletonList(appId));
Set<Integer> appModuleIds = new LinkedHashSet<>();
for (AppModule m : appModules) {
appModuleIds.add(m.getId());
}
if (permMap.isEmpty()) {
return false;
}
// 5. 判定:自身显式勾选直接通过;祖先勾选且开启继承则子孙生效
for (Map.Entry<Integer, List<Integer>> entry : chainMap.entrySet()) {
Integer selfGroupId = entry.getKey();
for (Integer ancestorId : entry.getValue()) {
SysGroupModulePerm perm = permMap.get(ancestorId);
if (perm == null) {
Map<Integer, Map<Integer, SysGroupModulePerm>> modulePermMap = new HashMap<>();
Map<Integer, Set<Integer>> groupConfiguredApps = new HashMap<>();
if (!appModuleIds.isEmpty() && !allChainIds.isEmpty()) {
List<SysGroupModulePerm> perms = sysGroupModulePermRepository.findByModuleIdInAndGroupIdIn(
new ArrayList<>(appModuleIds), new ArrayList<>(allChainIds));
for (SysGroupModulePerm perm : perms) {
if (perm.getModuleId() == null || perm.getGroupId() == null) {
continue;
}
if (Objects.equals(ancestorId, selfGroupId)) {
return true;
}
if (Boolean.TRUE.equals(perm.getInheritFlag())) {
return true;
}
modulePermMap.computeIfAbsent(perm.getModuleId(), k -> new HashMap<>())
.put(perm.getGroupId(), perm);
groupConfiguredApps.computeIfAbsent(perm.getGroupId(), k -> new HashSet<>()).add(appId);
}
}
// 5. 就近原则判定:对每个归属组,取离它最近的、对该应用有显式配置的组为唯一权限来源;
// 该来源组对目标模块有记录即通过(多个归属组取并集)。
for (Integer selfGroupId : selfGroupIds) {
List<Integer> chain = chainMap.get(selfGroupId);
Integer authority = findNearestAuthorityGroup(selfGroupId, chain, appId, groupConfiguredApps, allGroupMap);
if (authority == null) {
continue;
}
Map<Integer, SysGroupModulePerm> pm = modulePermMap.get(moduleId);
if (pm != null && pm.get(authority) != null) {
return true;
}
}
return false;