修改modules的bug
ci-userbase-service / build-and-push (push) Successful in 1m31s

This commit is contained in:
chenxing
2026-08-14 15:20:21 +08:00
parent 236c9545f9
commit 87163ba944
3 changed files with 303 additions and 58 deletions
@@ -1,22 +1,24 @@
package com.yunhai.userbase.controller.oapi;
import com.yunhai.platform.appauth.AppAuthFilter;
import com.yunhai.platform.utils.exception.DException;
import com.yunhai.platform.utils.model.BoaGroup;
import com.yunhai.platform.utils.model.BoaModule;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.userbase.config.Config;
import com.yunhai.userbase.model.groupauthz.UserPermissionAppVo;
import com.yunhai.userbase.model.groupauthz.UserPermissionCategoryVo;
import com.yunhai.userbase.model.groupauthz.UserPermissionModuleVo;
import com.yunhai.userbase.entity.ApplicationInfo;
import com.yunhai.userbase.repository.ApplicationInfoRepository;
import com.yunhai.userbase.service.IGroupAuthzService;
import com.yunhai.userbase.service.impl.UserCenterService;
import org.springframework.beans.BeanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.List;
@@ -34,12 +36,17 @@ import java.util.List;
@RequestMapping(Config.OPEN_API_PATH + "user/")
public class UserController {
private static final Logger log = LoggerFactory.getLogger(UserController.class);
@Autowired
private UserCenterService userCenterService;
@Autowired
private IGroupAuthzService groupAuthzService;
@Autowired
private ApplicationInfoRepository applicationInfoRepository;
@GetMapping("byId")
public BoaUser byId(@RequestParam("id") Integer id) {
return userCenterService.getUserInfoWithDeptById(id);
@@ -62,60 +69,72 @@ public class UserController {
}
/**
* 查询用户在指定学校下的已授权模块列表。<br>
* 若 {@code schId} 为 null 时,自动取用户的主学校;若仍无主学校信息,退化为取用户主归属组(groups 第 0 个)。
* 查询用户在<b>调用方应用</b>下、指定学校(上下文)里的已授权模块列表。<br>
* <br>
* 与管理端「权限一览」返回<b>全应用聚合</b>不同,本方法严格按调用方应用维度过滤:<br>
* ① 通过 {@link AppAuthFilter#APP_AUTH_ATTR} 拿到验签时的 appKey(RequireAppSignFilter
* 已保证非空);<br>
* ② 查 application_info 映射到具体 appId;<br>
* ③ 调 Service 专用方法:所有 SQL/集合构建均限定在该 appId 范围内(数据库层面过滤)。<br>
* <br>
* 边界:调用方 appKey 查不到对应应用 → 直接返回空列表(安全优先,禁止意外返回其他应用的模块)。<br>
*
* @param userId 用户ID(必填)
* @param schId 学校ID/组ID(可选;为 null 时按上述规则推断)
* @return 扁平化的已授权模块列表(含基础平台统一 BoaModule 模型);无权限时返回空列表
* @param request 用于取 AppAuthFilter 设置的 appKey
* @param userId 用户ID(必填)
* @param schId 学校ID/组ID(可选;为 null 时按"主学校→主归属组"规则推断)
* @return 该应用下用户已授权模块(扁平化;无权限时返回空列表)
*/
@GetMapping("modules")
public List<BoaModule> modules(@RequestParam("userId") Integer userId,
public List<BoaModule> modules(HttpServletRequest request,
@RequestParam("userId") Integer userId,
@RequestParam(value = "schId", required = false) Integer schId) {
if (userId == null) {
return Collections.emptyList();
}
Integer effectiveGroupId = resolveEffectiveGroupId(userId, schId);
// 注意:超管即使没有学校/归属组,service.findUserPermissionTree 内部也会走超管短路返回全部权限,
// 因此这里不能因 effectiveGroupId 为 null 直接返回空,否则超管在 oapi/user/modules 永远拿不到权限;
// 普通用户无组/无学校时,findUserPermissionTree 同样会返回空列表,行为一致。
List<UserPermissionCategoryVo> tree = groupAuthzService.findUserPermissionTree(effectiveGroupId, userId);
if (tree == null || tree.isEmpty()) {
Integer appId = resolveCallerAppId(request);
if (appId == null) {
return Collections.emptyList();
}
List<BoaModule> result = new ArrayList<>();
for (UserPermissionCategoryVo category : tree) {
if (category.getApps() == null)
continue;
for (UserPermissionAppVo appVo : category.getApps()) {
String icon = appVo.getPcIcon();
collectModules(appVo.getModules(), icon, result);
}
}
return result;
return groupAuthzService.findUserModulesForApp(appId, schId, userId);
}
/**
* 递归收集已授权模块树到扁平列表,并把应用 PC 图标作为模块的 icon 兜底(BoaModule.icon)。
* 从请求 attribute 中解析调用方 appKey,并映射为 application_info 的主键 id。<br>
* 映射失败返回 null(Controller 层统一返回空列表;异常只打 warn 日志,不给前端 500)。
*/
private void collectModules(List<UserPermissionModuleVo> modules,
String appIcon,
List<BoaModule> collector) {
if (modules == null || modules.isEmpty()) {
return;
private Integer resolveCallerAppId(HttpServletRequest request) {
Object attr = request.getAttribute(AppAuthFilter.APP_AUTH_ATTR);
if (attr == null) {
log.warn("[oapi:user:modules] no AppAuth attr (should be blocked by RequireAppSignFilter), uri={}",
request.getRequestURI());
return null;
}
for (UserPermissionModuleVo m : modules) {
BoaModule bm = new BoaModule();
BeanUtils.copyProperties(m, bm);
bm.setIcon(appIcon);
collector.add(bm);
collectModules(m.getChildren(), appIcon, collector);
String appKeyStr = (attr instanceof String) ? (String) attr : String.valueOf(attr);
if (appKeyStr.trim().isEmpty()) {
log.warn("[oapi:user:modules] AppAuth attr is blank, uri={}", request.getRequestURI());
return null;
}
Integer appKey;
try {
appKey = Integer.parseInt(appKeyStr.trim());
} catch (NumberFormatException e) {
log.warn("[oapi:user:modules] invalid appKey format={}, uri={}", appKeyStr, request.getRequestURI());
return null;
}
ApplicationInfo app = applicationInfoRepository.findByAppKey(appKey);
if (app == null) {
log.warn("[oapi:user:modules] appKey={} not found in application_info, uri={}", appKey,
request.getRequestURI());
return null;
}
return app.getId();
}
/**
* 按「schId 为空时」解析出用户主学校/主组作为模块查询的上下文组ID。
* 按「schId 为空时」解析出用户主学校/主组作为模块查询的上下文组ID。<br>
* 保留方法(逻辑已下沉到 Service.resolveEffectiveGroupId 复用),不再被 modules() 使用。
*/
@Deprecated
private Integer resolveEffectiveGroupId(Integer userId, Integer schId) {
if (schId != null) {
return schId;
@@ -115,6 +115,23 @@ public interface IGroupAuthzService {
*/
List<UserGroupNodeVo> findUserGroupTree(Integer userId);
/**
* 开放接口专用:查询用户在<b>指定调用方应用</b>下的已授权模块(扁平化 BoaModule 列表)。<br>
* 与 {@link #findUserPermissionTree(Integer, Integer)} 的核心差别:<br>
* - 后者返回用户在全部已开通应用下的权限聚合(供管理端权限一览使用);<br>
* - 本方法只返回 {@code appId} 对应这一个应用下的模块,所有 SQL/集合构建均限定该应用(数据库层面过滤)。<br>
* <br>
* 上下文组(schId)语义与开放接口一致:schId 非空=只认该组链路;schId 为空=取用户主学校组(若仍空=用户主归属组 id)。<br>
* 超级管理员:不做组/继承判定,直接返回该应用下全部模块(但仍只返回该应用,不会跨应用返回)。<br>
* 角色授权并集:最终权限 = 组授权模块 ∪ 启用角色下属于该应用的模块(逻辑与权限一览保持一致)。
*
* @param appId 调用方应用ID(必填;查不到或该应用无开通=返回空列表)
* @param schId 学校/组上下文(可选;为 null 时按"主学校→主归属组"规则推断)
* @param userId 用户ID(必填)
* @return 该应用下用户有权限的模块(扁平化;无权限时返回空列表)
*/
List<com.yunhai.platform.utils.model.BoaModule> findUserModulesForApp(Integer appId, Integer schId, Integer userId);
// ===================== 新增:树节点式增量授权(供左侧树 + 右侧 appAuthzList 的勾选交互) =====================
/**
@@ -1,6 +1,9 @@
package com.yunhai.userbase.service.impl;
import com.yunhai.platform.utils.exception.DException;
import com.yunhai.platform.utils.model.BoaGroup;
import com.yunhai.platform.utils.model.BoaModule;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.userbase.entity.*;
import com.yunhai.userbase.enums.GroupTypeEnum;
import com.yunhai.userbase.enums.IEnum;
@@ -32,6 +35,7 @@ import com.yunhai.userbase.repository.SysUserAppRoleRelRepository;
import com.yunhai.userbase.repository.SysUserGroupRelRepository;
import com.yunhai.userbase.repository.SysUserRepository;
import com.yunhai.userbase.service.IGroupAuthzService;
import com.yunhai.userbase.service.impl.UserCenterService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.jpa.domain.Specification;
@@ -100,6 +104,9 @@ public class GroupAuthzService implements IGroupAuthzService {
@Autowired
private SysUserAppRoleRelRepository sysUserAppRoleRelRepository;
@Autowired
private UserCenterService userCenterService;
// ===================== 新增:沿祖先链收集「本组+所有祖先组开通的应用ID并集」(应用池联动GetSchoolAppId)
// =====================
@@ -493,6 +500,200 @@ public class GroupAuthzService implements IGroupAuthzService {
return Collections.singletonList(moduleId);
}
// ===================== 开放接口专用:单应用维度模块过滤 =====================
@Override
public List<BoaModule> findUserModulesForApp(Integer appId, Integer schId, Integer userId) {
Objects.requireNonNull(appId, "应用ID不能为空");
Objects.requireNonNull(userId, "用户ID不能为空");
// 1. 调用方应用本身存在性校验(数据库过滤第一层:appId 必须真实存在,否则直接空)
ApplicationInfo app = applicationInfoRepository.findById(appId).orElse(null);
if (app == null) {
return Collections.emptyList();
}
String appIcon = app.getPcIcon();
SysUser user = sysUserRepository.findById(userId).orElse(null);
if (user == null) {
return Collections.emptyList();
}
// 2. 超级管理员短路:不再返回全应用模块=仅返回该应用下全部模块(数据库过滤第二层:appId 限定)
if (isSuperAdmin(user.getType(), userId)) {
List<AppModule> allModules = appModuleRepository.findByAppIdIn(Collections.singletonList(appId));
return flattenToBoaModules(allModules, appIcon);
}
// 3. 收集该用户启用角色下=属于目标应用的模块ID(角色授权并集,不要求组先开通应用)
Set<Integer> roleModuleIdsInApp = collectEnabledRoleModuleIdsForApp(userId, appId);
// 4. 上下文组解析(schId 非空=只认这一条链路;schId 空=按"主学校→主归属组"规则推断)
Integer effectiveGroupId = resolveEffectiveGroupId(userId, schId);
// 5. 若上下文组有效=构建其祖先链,并判定:该祖先链开通池里是否包含目标 appId(应用开通闸门)
// 上下文组无效(用户无学校/无组)时,仍可能因角色授权而返回模块,不能直接返回空
Set<Integer> selfGroupIds = new LinkedHashSet<>();
Map<Integer, SysGroup> allGroupMap = buildAllGroupMap();
Map<Integer, List<Integer>> chainMap = new HashMap<>();
Set<Integer> allChainIds = new LinkedHashSet<>();
Map<Integer, Set<Integer>> groupToApps = new HashMap<>();
boolean appOpened = false;
if (effectiveGroupId != null) {
selfGroupIds.add(effectiveGroupId);
List<Integer> chain = ancestorChain(effectiveGroupId, allGroupMap);
chainMap.put(effectiveGroupId, chain);
allChainIds.addAll(chain);
Set<Integer> openedApps = collectAppIdsForAuthzScope(effectiveGroupId);
groupToApps.put(effectiveGroupId, openedApps);
if (openedApps.contains(appId)) {
appOpened = true;
}
}
// 6. 数据库过滤第三层:只加载目标应用下的模块
List<AppModule> appModules = appModuleRepository.findByAppIdIn(Collections.singletonList(appId));
if (appModules.isEmpty()) {
return (roleModuleIdsInApp.isEmpty()) ? Collections.emptyList()
: flattenToBoaModulesById(roleModuleIdsInApp, appIcon, buildAppModuleMap(appModules));
}
Set<Integer> appModuleIds = new LinkedHashSet<>();
for (AppModule m : appModules) {
appModuleIds.add(m.getId());
}
// 7. 数据库过滤第四层:只查「目标应用模块 × 祖先链」上的组授权记录
Map<Integer, Map<Integer, SysGroupModulePerm>> modulePermMap = new HashMap<>();
Map<Integer, Set<Integer>> groupConfiguredApps = new HashMap<>();
if (appOpened && !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;
}
modulePermMap.computeIfAbsent(perm.getModuleId(), k -> new HashMap<>())
.put(perm.getGroupId(), perm);
groupConfiguredApps.computeIfAbsent(perm.getGroupId(), k -> new HashSet<>()).add(appId);
}
}
// 8. 权限判定合并:组授权 ∪ 角色授权(均属于该应用)
Set<Integer> permittedModuleIds = new LinkedHashSet<>();
for (Integer mid : roleModuleIdsInApp) {
if (appModuleIds.contains(mid)) {
permittedModuleIds.add(mid);
}
}
if (appOpened) {
for (AppModule m : appModules) {
if (permittedModuleIds.contains(m.getId())) {
continue;
}
if (hasUserModulePermission(m.getId(), m.getAppId(), selfGroupIds, chainMap, groupToApps,
modulePermMap, allGroupMap, groupConfiguredApps)) {
permittedModuleIds.add(m.getId());
}
}
}
if (permittedModuleIds.isEmpty()) {
return Collections.emptyList();
}
return flattenToBoaModulesById(permittedModuleIds, appIcon, buildAppModuleMap(appModules));
}
/**
* 收集用户启用角色下=且属于目标应用 appId 的模块ID集合(角色授权并集专用的"应用过滤版")。<br>
* 与 {@link #collectEnabledRoleModuleIds(Integer)} 的区别:后者返回全部角色模块;
* 本方法在其之上再按模块.appId == appId 过滤一次(不额外发 SQL,遍历两次即可)。
*/
private Set<Integer> collectEnabledRoleModuleIdsForApp(Integer userId, Integer appId) {
Set<Integer> allRoleModuleIds = collectEnabledRoleModuleIds(userId);
if (allRoleModuleIds.isEmpty() || appId == null) {
return Collections.emptySet();
}
Set<Integer> result = new LinkedHashSet<>();
for (AppModule m : appModuleRepository.findAllById(allRoleModuleIds)) {
if (Objects.equals(m.getAppId(), appId) && m.getId() != null) {
result.add(m.getId());
}
}
return result;
}
/**
* 上下文组解析(与开放接口 modules 语义严格一致):
* - schId 非空:直接用其作为唯一 selfGroupId;
* - schId 为空:取用户主学校组(BoaUser.school.id);
* - 仍空:取用户主归属组 id(groups 第 0 个);
* - 全空:返回 null(此时只能靠角色授权拿模块)。
*/
private Integer resolveEffectiveGroupId(Integer userId, Integer schId) {
if (schId != null) {
return schId;
}
BoaUser boa = userCenterService.getUserInfoWithDeptById(userId);
if (boa == null) {
return null;
}
BoaGroup school = boa.getSchool();
if (school != null && school.getId() != null) {
return school.getId();
}
List<BoaGroup> groups = boa.getGroups();
if (groups != null && !groups.isEmpty()) {
BoaGroup main = groups.get(0);
return main == null ? null : main.getId();
}
return null;
}
private static Map<Integer, AppModule> buildAppModuleMap(List<AppModule> appModules) {
Map<Integer, AppModule> map = new HashMap<>();
for (AppModule m : appModules) {
if (m.getId() != null) {
map.put(m.getId(), m);
}
}
return map;
}
private static List<BoaModule> flattenToBoaModules(List<AppModule> modules, String icon) {
if (modules == null || modules.isEmpty()) {
return Collections.emptyList();
}
modules.sort(moduleOrder());
List<BoaModule> result = new ArrayList<>(modules.size());
for (AppModule m : modules) {
result.add(toBoaModule(m, icon));
}
return result;
}
private static List<BoaModule> flattenToBoaModulesById(Set<Integer> moduleIds, String icon,
Map<Integer, AppModule> moduleMap) {
if (moduleIds == null || moduleIds.isEmpty()) {
return Collections.emptyList();
}
List<AppModule> list = new ArrayList<>(moduleIds.size());
for (Integer mid : moduleIds) {
AppModule m = moduleMap.get(mid);
if (m != null) {
list.add(m);
}
}
return flattenToBoaModules(list, icon);
}
private static BoaModule toBoaModule(AppModule m, String icon) {
BoaModule bm = new BoaModule();
BeanUtils.copyProperties(m, bm);
bm.setIcon(icon);
return bm;
}
private static Comparator<AppModule> moduleOrder() {
return (a, b) -> {
int sa = a.getSortNo() == null ? 999 : a.getSortNo();
int sb = b.getSortNo() == null ? 999 : b.getSortNo();
if (sa != sb) {
return Integer.compare(sa, sb);
}
return Integer.compare(a.getId() == null ? 0 : a.getId(), b.getId() == null ? 0 : b.getId());
};
}
// ===================== 权限一览 =====================
@Override
@@ -605,14 +806,14 @@ public class GroupAuthzService implements IGroupAuthzService {
Set<Integer> roleModuleIds = collectEnabledRoleModuleIds(userId);
// 1. 归属组集合:区分「组上下文模式」和「用户全局模式」
// A) 接口显式传了 groupId(= 管理员在"某个组的授权面板"点用户权限一览,是典型的组视角查询):
// 以传入的 groupId 作为唯一 selfGroupId,仅沿「该组 -> 祖先 -> 第一个学校组(含)」链路判定;
// 【关键】不再把用户真实加入的其他组(例如用户还属于学校组)并入判断。
// 原因:如果允许用户归属的"学校组"作为独立 selfGroupId 参与本组显式判定,
// 则子组未设置的模块会直接命中学校组自己的显式记录(permMap.get(学校组)!=null),
// 导致"子组设置的覆盖限制"完全失效=父级所有模块都拿到,即你观察到的"本组继承没生效/还是父级全量"。
// B) 未传 groupId(全局查用户权限/开放接口 modules 未传schId以外的其他上下文):
// 正常取用户所有真实归属组并集。
// 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) {
@@ -884,12 +1085,15 @@ public class GroupAuthzService implements IGroupAuthzService {
/**
* 就近原则:在 groupId 的祖先链上,找到离它最近的、对目标应用有显式模块配置的组(含自身)。
*
* <p>链首即自身({@link #ancestorChain} 由自身向根排列),天然「就近优先」;遍历时第一个
* <p>
* 链首即自身({@link #ancestorChain} 由自身向根排列),天然「就近优先」;遍历时第一个
* 「groupConfiguredApps 中包含该应用」的组即作为该应用在本组范围内的<b>唯一权限来源</b>返回。
*
* <p>学校截止:遇到学校组(无论是否已配置)即作为继承上限,不再向教育局/根越级,避免跨学校继承到未开通的应用。
* <p>
* 学校截止:遇到学校组(无论是否已配置)即作为继承上限,不再向教育局/根越级,避免跨学校继承到未开通的应用。
*
* <p>找不到返回 null(该应用在此范围内没有任何组的显式配置,对应模块不许可)。
* <p>
* 找不到返回 null(该应用在此范围内没有任何组的显式配置,对应模块不许可)。
*/
private Integer findNearestAuthorityGroup(Integer groupId, List<Integer> chain,
Integer appId, Map<Integer, Set<Integer>> groupConfiguredApps,
@@ -913,7 +1117,8 @@ public class GroupAuthzService implements IGroupAuthzService {
/**
* 子组继承闸门:判定「来源组 authority 对 moduleId 的记录」是否对<b>本组</b> selfGroupId 生效。
*
* <p>规则(修复「父级未勾子组继承却仍被继承」的 bug):
* <p>
* 规则(修复「父级未勾子组继承却仍被继承」的 bug):
* ① 来源组 == 本组:本组自身勾选的模块直接生效——{@code inheritFlag} 只约束是否下放到<b>子组</b>,不影响本组自己;
* ② 来源组为祖先(往上回溯得来):必须勾选了「子组继承」({@code inheritFlag=true}) 才会下放到本组,
* 否则该模块不继承。即「只过滤勾选了子组继承的模块;若来源组对该应用所有模块都没勾子组继承,则本组对该应用返回空」。
@@ -933,7 +1138,8 @@ public class GroupAuthzService implements IGroupAuthzService {
/**
* 用户维度权限判定:用户对指定模块是否有权限(就近原则 + 学校截止 + 角色授权并集 + 超管兜底)。
*
* <p><b>就近原则</b>(统一了「本组覆盖」与「最近祖先继承」两种情形,修复两类合并 bug):
* <p>
* <b>就近原则</b>(统一了「本组覆盖」与「最近祖先继承」两种情形,修复两类合并 bug):
* 沿 selfGroupId 的祖先链(含自身)向上,找到「离它最近、且对 appId 有任意显式模块配置」的组作为
* 该应用的<b>唯一权限来源</b>;该模块被许可,当且仅当这个来源组对它自身有显式 {@code sys_group_module_perm} 记录。
* 这样:
@@ -941,13 +1147,16 @@ public class GroupAuthzService implements IGroupAuthzService {
* ② 本组无配置(如英语组为空)→ 取<b>最近</b>的有配置祖先(如教师组)为来源,不再越级到更远的学校组;
* ③ 来源组的模块集合即最终集合,更上层同应用模块(如学校组多勾的模块8)一律不合并。
*
* <p><b>子组继承闸门</b>(修复「父级未勾子组继承仍被继承」的 bug):来源组若是本组,本组勾选的模块直接生效;
* <p>
* <b>子组继承闸门</b>(修复「父级未勾子组继承仍被继承」的 bug):来源组若是本组,本组勾选的模块直接生效;
* 来源组若是祖先,必须该模块记录 {@code inheritFlag=true}(勾选子组继承)才下放到本组,否则不继承——
* 即「只过滤勾选了子组继承的模块;若来源组对该应用所有模块都没勾子组继承,则本组对该应用返回空」。
*
* <p><b>学校截止</b>:祖先链只查到第一个学校组(含)为止,不跨学校到教育局/根。
* <p>
* <b>学校截止</b>:祖先链只查到第一个学校组(含)为止,不跨学校到教育局/根。
*
* <p><b>其余</b>:归属组需已开通该模块所属应用(双保险,防越权)。
* <p>
* <b>其余</b>:归属组需已开通该模块所属应用(双保险,防越权)。
*/
private boolean hasUserModulePermission(Integer moduleId, Integer appId,
Set<Integer> selfGroupIds,
@@ -1235,7 +1444,7 @@ public class GroupAuthzService implements IGroupAuthzService {
return false;
}
// 4. 一次批量查询「该应用下所有模块」在全部祖先链节点上的权限记录
// (要判定就近来源,需要知道链上哪些组对该应用有显式配置,不能只看单模块)
// (要判定就近来源,需要知道链上哪些组对该应用有显式配置,不能只看单模块)
List<AppModule> appModules = appModuleRepository.findByAppIdIn(Collections.singletonList(appId));
Set<Integer> appModuleIds = new LinkedHashSet<>();
for (AppModule m : appModules) {
@@ -1256,7 +1465,7 @@ public class GroupAuthzService implements IGroupAuthzService {
}
}
// 5. 就近原则判定:对每个归属组,取离它最近的、对该应用有显式配置的组为唯一权限来源;
// 该来源组对目标模块有记录即通过(多个归属组取并集)。
// 该来源组对目标模块有记录即通过(多个归属组取并集)。
for (Integer selfGroupId : selfGroupIds) {
List<Integer> chain = chainMap.get(selfGroupId);
Integer authority = findNearestAuthorityGroup(selfGroupId, chain, appId, groupConfiguredApps, allGroupMap);