模块接口更改
This commit is contained in:
@@ -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 组应用权限服务实现。<br>
|
||||
* 功能1「应用开通」:sys_group_app_rel 全量覆盖式保存,开通不继承(子组须自己开通)。<br>
|
||||
* 功能2「组授权」:sys_group_module_perm 全量覆盖式保存,inheritFlag=true 时子孙组自动继承。<br>
|
||||
* 权限判定:沿 sys_group 祖先链内存回溯(树深度有限,MySQL 5.7 无递归 CTE,批量 IN 查询)。<br>
|
||||
* 角色授权并集:最终模块权限 = 组授权模块 ∪ 启用角色授权模块;角色模块不要求组先开通应用(见 AI-FIX 标记)。<br>
|
||||
* 功能1「应用开通」:sys_group_app_rel 全量覆盖式保存,开通不继承(子组须自己开通)。<br>
|
||||
* 功能2「组授权」:sys_group_module_perm 全量覆盖式保存,inheritFlag=true
|
||||
* 时子孙组自动继承。<br>
|
||||
* 权限判定:沿 sys_group 祖先链内存回溯(树深度有限,MySQL 5.7 无递归 CTE,批量 IN 查询)。<br>
|
||||
* 角色授权并集:最终模块权限 = 组授权模块 ∪ 启用角色授权模块;角色模块不要求组先开通应用(见 AI-FIX 标记)。<br>
|
||||
*/
|
||||
@Service
|
||||
public class GroupAuthzService implements IGroupAuthzService {
|
||||
@@ -97,12 +100,14 @@ public class GroupAuthzService implements IGroupAuthzService {
|
||||
@Autowired
|
||||
private SysUserAppRoleRelRepository sysUserAppRoleRelRepository;
|
||||
|
||||
// ===================== 新增:沿祖先链收集「本组+所有祖先组开通的应用ID并集」(应用池联动GetSchoolAppId) =====================
|
||||
// ===================== 新增:沿祖先链收集「本组+所有祖先组开通的应用ID并集」(应用池联动GetSchoolAppId)
|
||||
// =====================
|
||||
|
||||
/**
|
||||
* 按用户需求:父组(学校组)若开通了应用(GetSchoolAppId返回非空),则其所有子孙组的模块权限分配面板也能看到这些应用(无需子组重复开通)。<br>
|
||||
* 取应用范围=sys_group_app_rel里group_id属于「本组+全部祖先组」的app_id并集;开通不继承,模块权限池继承。<br>
|
||||
* 对 hasModulePermission / UserPermissionTree 不做修改,避免越权(最终用户拿到权限仍需要自己的归属组开通应用+继承到模块权限,双保险)。
|
||||
* 对 hasModulePermission / UserPermissionTree
|
||||
* 不做修改,避免越权(最终用户拿到权限仍需要自己的归属组开通应用+继承到模块权限,双保险)。
|
||||
*/
|
||||
private Set<Integer> collectAppIdsForAuthzScope(Integer groupId) {
|
||||
Map<Integer, SysGroup> allGroupMap = buildAllGroupMap();
|
||||
@@ -303,8 +308,8 @@ public class GroupAuthzService implements IGroupAuthzService {
|
||||
if (!sysGroupRepository.existsById(groupId)) {
|
||||
throw new DException("系统组不存在");
|
||||
}
|
||||
List<GroupModulePermRequest.ModulePermItem> perms =
|
||||
request.getPerms() == null ? Collections.emptyList() : request.getPerms();
|
||||
List<GroupModulePermRequest.ModulePermItem> perms = request.getPerms() == null ? Collections.emptyList()
|
||||
: request.getPerms();
|
||||
Set<Integer> appIdSet = collectAppIdsForAuthzScope(groupId);
|
||||
Set<Integer> 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<Integer> 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<Integer, Set<Integer>> groupToApps = new HashMap<>();
|
||||
Set<Integer> allAppIds = new LinkedHashSet<>();
|
||||
for (Integer groupIdItem : selfGroupIds) {
|
||||
@@ -672,7 +685,8 @@ public class GroupAuthzService implements IGroupAuthzService {
|
||||
Set<Integer> 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 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户维度权限判定:用户对指定模块是否有权限。<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,
|
||||
Set<Integer> selfGroupIds,
|
||||
Map<Integer, List<Integer>> chainMap,
|
||||
Map<Integer, Set<Integer>> groupToApps,
|
||||
Map<Integer, Map<Integer, SysGroupModulePerm>> modulePermMap) {
|
||||
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, SysGroupModulePerm> 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<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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建用户权限一览的模块树:仅保留有权限的模块,父节点无权限时子节点挂到根级。
|
||||
*/
|
||||
private List<UserPermissionModuleVo> buildUserPermissionModuleTree(List<AppModule> modules,
|
||||
Set<Integer> permittedModuleIds) {
|
||||
Set<Integer> permittedModuleIds) {
|
||||
// 先过滤
|
||||
List<AppModule> permitted = new ArrayList<>();
|
||||
for (AppModule m : modules) {
|
||||
@@ -918,7 +1040,7 @@ public class GroupAuthzService implements IGroupAuthzService {
|
||||
}
|
||||
|
||||
private UserPermissionModuleVo buildUserPermissionModuleNode(AppModule node,
|
||||
Map<Integer, List<AppModule>> childrenMap) {
|
||||
Map<Integer, List<AppModule>> childrenMap) {
|
||||
UserPermissionModuleVo vo = new UserPermissionModuleVo();
|
||||
BeanUtils.copyProperties(node, vo);
|
||||
List<AppModule> children = childrenMap.get(node.getId());
|
||||
@@ -1009,7 +1131,7 @@ public class GroupAuthzService implements IGroupAuthzService {
|
||||
allGroupMap.put(g.getId(), g);
|
||||
}
|
||||
// 3. 前置校验:所属组必须已开通模块所属应用(最终权限 = 继承的模块权限 ∩ 已开通应用);
|
||||
// 通过校验的组构建祖先链(含自身),并收集祖先链并集用于一次批量 IN 查询
|
||||
// 通过校验的组构建祖先链(含自身),并收集祖先链并集用于一次批量 IN 查询
|
||||
Map<Integer, List<Integer>> chainMap = new HashMap<>();
|
||||
Set<Integer> allChainIds = new LinkedHashSet<>();
|
||||
for (SysUserGroupRel rel : userRels) {
|
||||
@@ -1029,8 +1151,8 @@ public class GroupAuthzService implements IGroupAuthzService {
|
||||
}
|
||||
// 4. 一次批量查询目标模块在所有祖先链节点上的权限记录
|
||||
Map<Integer, SysGroupModulePerm> permMap = new HashMap<>();
|
||||
List<SysGroupModulePerm> perms =
|
||||
sysGroupModulePermRepository.findByModuleIdAndGroupIdIn(moduleId, new ArrayList<>(allChainIds));
|
||||
List<SysGroupModulePerm> 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 排序)。<br>
|
||||
*/
|
||||
private List<GroupAuthzModuleVo> buildModuleTree(List<AppModule> modules,
|
||||
Map<Integer, SysGroupModulePerm> permMap) {
|
||||
Map<Integer, SysGroupModulePerm> permMap) {
|
||||
// parentId -> 子节点列表(null 视为 0)
|
||||
Map<Integer, List<AppModule>> childrenMap = new HashMap<>();
|
||||
for (AppModule m : modules) {
|
||||
@@ -1112,8 +1234,8 @@ public class GroupAuthzService implements IGroupAuthzService {
|
||||
* 递归组装模块节点,并按权限记录回显 checked / inheritFlag。<br>
|
||||
*/
|
||||
private GroupAuthzModuleVo buildModuleNode(AppModule node,
|
||||
Map<Integer, List<AppModule>> childrenMap,
|
||||
Map<Integer, SysGroupModulePerm> permMap) {
|
||||
Map<Integer, List<AppModule>> childrenMap,
|
||||
Map<Integer, SysGroupModulePerm> permMap) {
|
||||
GroupAuthzModuleVo vo = new GroupAuthzModuleVo();
|
||||
BeanUtils.copyProperties(node, vo);
|
||||
SysGroupModulePerm perm = permMap.get(node.getId());
|
||||
|
||||
Reference in New Issue
Block a user