1. 父级选中态(组授权 / 角色授权口径统一)
两页 isAppChecked 由「全部子模块都勾选才 true」改为「任一子孙被勾选即 true」,
配合前端样式把 zTree checkbox_true_part 图标指向 checkbox_true_full,
折叠态也能一眼看出「部分授权」,不再依赖视觉很弱的半选。
2. 无子模块应用(如「一周安排」)此前勾选不落库、刷新即失效
- 新增 constant/AppLevelModuleId:用 -appId 作「整应用授权」的 module_id 哨兵。
两套授权表都是 (owner_id, module_id) 唯一键,用 0 会让同组/同角色的第二个
无模块应用撞唯一键;真实模块 id 恒正,负数段天然空闲且权限判定永不命中。
- GroupAuthzService#resolveModuleIds:应用无任何模块时返回 -appId,使「整应用」
授权能落库;findGroupAppsWithModules 按 -appId 回填应用级 checked / inheritFlag。
- AppRoleService:findRoleModuleTree 同口径回填 checked;saveRoleModules 对负数项
跳过模块校验,改为校验应用存在性与角色授权范围(角色授权是全量覆盖,哨兵必须原样落库)。
- GroupAuthzService#buildModuleTree:脏数据兜底——parent_id 指向已删模块导致根节点
全丢时,把全部模块提升为根,避免「有模块的应用」被误判成无模块应用。
涉及前端配套改动见 UserBaseFront 同名提交(GroupAuth.vue / AppModuleCheckTree.vue)。
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
package com.yunhai.userbase.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「整应用授权」的 module_id 哨兵编码。<br>
|
||||||
|
* <p>
|
||||||
|
* 授权关系表 {@code sys_group_module_perm} / {@code sys_app_role_module_rel} 都只有
|
||||||
|
* (owner_id, module_id) 两列,<b>没有 app_id</b>。当某个应用下没有任何 {@code app_module} 记录时
|
||||||
|
* (如「一周安排」这类单页应用),模块级授权无处可挂,于是用 <b>-appId</b> 作为该应用
|
||||||
|
* 「整体已授权」的哨兵值:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>不是 workaround</b>:module_id 命名空间里真实模块 id 恒为正整数,负数段天然是空闲的
|
||||||
|
* 应用级通道,与真实模块不会冲突;</li>
|
||||||
|
* <li>不同应用编码不同,满足 {@code uk_group_module(group_id, module_id)} /
|
||||||
|
* {@code uk_app_role_module(role_id, module_id)} 唯一键,同组/同角色可同时授权多个无模块应用;</li>
|
||||||
|
* <li>权限判定链路(hasModulePermission / collectEnabledRoleModuleIds 等)按真实 moduleId 匹配,
|
||||||
|
* 哨兵值永远匹配不上,不会被误判成某个模块的权限。</li>
|
||||||
|
* </ul>
|
||||||
|
* 写入侧由 Service 内部把「入参 moduleId=0(整应用语义)」翻译成本编码,前端无需感知。
|
||||||
|
*/
|
||||||
|
public final class AppLevelModuleId {
|
||||||
|
|
||||||
|
private AppLevelModuleId() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用ID → 整应用授权的哨兵 module_id(-appId)。appId 为空时返回 null。
|
||||||
|
*/
|
||||||
|
public static Integer of(Integer appId) {
|
||||||
|
return appId == null ? null : -appId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 哨兵 module_id → 应用ID;传入真实模块 id(正数)或 null 时返回 null。
|
||||||
|
*/
|
||||||
|
public static Integer resolveAppId(Integer moduleId) {
|
||||||
|
return (moduleId != null && moduleId < 0) ? -moduleId : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为整应用授权的哨兵值。
|
||||||
|
*/
|
||||||
|
public static boolean isAppLevel(Integer moduleId) {
|
||||||
|
return moduleId != null && moduleId < 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,13 @@ public class AppRoleAuthzAppVo {
|
|||||||
/** 排序号。 */
|
/** 排序号。 */
|
||||||
private Integer sortNo;
|
private Integer sortNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用级勾选态:<b>仅当该应用下没有任何模块时</b>才有效。<br>
|
||||||
|
* 此时授权落在 sys_app_role_module_rel 的 -appId 哨兵记录上(见 {@code AppLevelModuleId});
|
||||||
|
* 有模块的应用恒为 false,勾选态由 modules 树逐节点表达。
|
||||||
|
*/
|
||||||
|
private Boolean checked = Boolean.FALSE;
|
||||||
|
|
||||||
/** 该应用下的模块权限树(含勾选回显)。 */
|
/** 该应用下的模块权限树(含勾选回显)。 */
|
||||||
private List<AppRoleAuthzModuleVo> modules = new ArrayList<>();
|
private List<AppRoleAuthzModuleVo> modules = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ public class GroupAuthzAppVo {
|
|||||||
|
|
||||||
private Integer sortNo;
|
private Integer sortNo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用级勾选态:<b>仅当该应用下没有任何模块时</b>才有效。<br>
|
||||||
|
* 此时授权落在 sys_group_module_perm 的 module_id=0 记录上(整应用授权);
|
||||||
|
* 有模块的应用恒为 false,勾选态由 modules 树逐节点表达。
|
||||||
|
*/
|
||||||
|
private Boolean checked = Boolean.FALSE;
|
||||||
|
|
||||||
|
/** 应用级子组继承:仅当该应用下没有模块时有意义,对应 module_id=0 记录的 inherit_flag。 */
|
||||||
|
private Boolean inheritFlag = Boolean.FALSE;
|
||||||
|
|
||||||
/** 该应用下的模块权限树;默认空列表,避免 JSON 序列化出现 null。 */
|
/** 该应用下的模块权限树;默认空列表,避免 JSON 序列化出现 null。 */
|
||||||
private List<GroupAuthzModuleVo> modules = new ArrayList<>();
|
private List<GroupAuthzModuleVo> modules = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ public class GroupModuleAuthzItem {
|
|||||||
/**
|
/**
|
||||||
* 模块ID;传 0 表示「该应用下全部模块」。<br>
|
* 模块ID;传 0 表示「该应用下全部模块」。<br>
|
||||||
* 注意:后端不会向 sys_group_module_perm 写入 module_id=0 的伪记录,<br>
|
* 注意:后端不会向 sys_group_module_perm 写入 module_id=0 的伪记录,<br>
|
||||||
* 而是把 0 展开为应用下每个真实模块各写一条权限记录。
|
* 而是把 0 展开为应用下每个真实模块各写一条权限记录。<br>
|
||||||
|
* 特殊情形:应用下<b>一个模块都没有</b>时,改写成 -appId 的整应用哨兵记录
|
||||||
|
* (见 {@code AppLevelModuleId}),保证这类应用也能被授权与回显。
|
||||||
*/
|
*/
|
||||||
@NotNull(message = "模块ID不能为空(整应用传 0)")
|
@NotNull(message = "模块ID不能为空(整应用传 0)")
|
||||||
private Integer moduleId;
|
private Integer moduleId;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.yunhai.userbase.entity.SysGroup;
|
|||||||
import com.yunhai.userbase.entity.SysUser;
|
import com.yunhai.userbase.entity.SysUser;
|
||||||
import com.yunhai.userbase.entity.SysUserAppRoleRel;
|
import com.yunhai.userbase.entity.SysUserAppRoleRel;
|
||||||
import com.yunhai.userbase.entity.SysUserGroupRel;
|
import com.yunhai.userbase.entity.SysUserGroupRel;
|
||||||
|
import com.yunhai.userbase.constant.AppLevelModuleId;
|
||||||
import com.yunhai.userbase.constant.AppNature;
|
import com.yunhai.userbase.constant.AppNature;
|
||||||
import com.yunhai.userbase.event.AppAuthzChangedEvent;
|
import com.yunhai.userbase.event.AppAuthzChangedEvent;
|
||||||
import com.yunhai.userbase.model.approle.AppRoleAuthzAppVo;
|
import com.yunhai.userbase.model.approle.AppRoleAuthzAppVo;
|
||||||
@@ -242,8 +243,12 @@ public class AppRoleService implements IAppRoleService {
|
|||||||
AppRoleAuthzAppVo appVo = new AppRoleAuthzAppVo();
|
AppRoleAuthzAppVo appVo = new AppRoleAuthzAppVo();
|
||||||
BeanUtils.copyProperties(app, appVo);
|
BeanUtils.copyProperties(app, appVo);
|
||||||
List<AppModule> modules = appModuleMap.get(app.getId());
|
List<AppModule> modules = appModuleMap.get(app.getId());
|
||||||
if (modules != null) {
|
if (modules != null && !modules.isEmpty()) {
|
||||||
appVo.setModules(buildModuleTree(modules, checkedModuleIds));
|
appVo.setModules(buildModuleTree(modules, checkedModuleIds));
|
||||||
|
} else {
|
||||||
|
// 无子模块应用:授权挂在 -appId 的整应用哨兵记录上(见 AppLevelModuleId),
|
||||||
|
// 否则这类应用勾选态永远无回显(勾了刷新就丢)。
|
||||||
|
appVo.setChecked(checkedModuleIds.contains(AppLevelModuleId.of(app.getId())));
|
||||||
}
|
}
|
||||||
result.add(appVo);
|
result.add(appVo);
|
||||||
}
|
}
|
||||||
@@ -266,6 +271,18 @@ public class AppRoleService implements IAppRoleService {
|
|||||||
moduleMap.put(m.getId(), m);
|
moduleMap.put(m.getId(), m);
|
||||||
}
|
}
|
||||||
for (Integer moduleId : moduleIds) {
|
for (Integer moduleId : moduleIds) {
|
||||||
|
// 整应用授权哨兵(-appId):不走模块校验,改为校验应用存在性与角色授权范围
|
||||||
|
Integer appLevelAppId = AppLevelModuleId.resolveAppId(moduleId);
|
||||||
|
if (appLevelAppId != null) {
|
||||||
|
if (!applicationInfoRepository.existsById(appLevelAppId)) {
|
||||||
|
throw new DException("应用不存在: " + appLevelAppId);
|
||||||
|
}
|
||||||
|
if (!Boolean.TRUE.equals(role.getAllAppFlag())
|
||||||
|
&& !Objects.equals(appLevelAppId, role.getAppId())) {
|
||||||
|
throw new DException("应用 " + appLevelAppId + " 不属于该角色所属应用,普通角色不能跨应用授权");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
AppModule m = moduleMap.get(moduleId);
|
AppModule m = moduleMap.get(moduleId);
|
||||||
if (m == null) {
|
if (m == null) {
|
||||||
throw new DException("模块不存在: " + moduleId);
|
throw new DException("模块不存在: " + moduleId);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.yunhai.userbase.service.impl;
|
|||||||
import com.yunhai.platform.utils.exception.DException;
|
import com.yunhai.platform.utils.exception.DException;
|
||||||
import com.yunhai.platform.utils.model.BoaModule;
|
import com.yunhai.platform.utils.model.BoaModule;
|
||||||
import com.yunhai.platform.utils.model.BoaRoleInfo;
|
import com.yunhai.platform.utils.model.BoaRoleInfo;
|
||||||
|
import com.yunhai.userbase.constant.AppLevelModuleId;
|
||||||
import com.yunhai.userbase.entity.*;
|
import com.yunhai.userbase.entity.*;
|
||||||
import com.yunhai.userbase.enums.GroupTypeEnum;
|
import com.yunhai.userbase.enums.GroupTypeEnum;
|
||||||
import com.yunhai.platform.utils.enums.IEnum;
|
import com.yunhai.platform.utils.enums.IEnum;
|
||||||
@@ -261,8 +262,14 @@ public class GroupAuthzService implements IGroupAuthzService {
|
|||||||
GroupAuthzAppVo appVo = new GroupAuthzAppVo();
|
GroupAuthzAppVo appVo = new GroupAuthzAppVo();
|
||||||
BeanUtils.copyProperties(app, appVo);
|
BeanUtils.copyProperties(app, appVo);
|
||||||
List<AppModule> modules = appModuleMap.get(app.getId());
|
List<AppModule> modules = appModuleMap.get(app.getId());
|
||||||
if (modules != null) {
|
if (modules != null && !modules.isEmpty()) {
|
||||||
appVo.setModules(buildModuleTree(modules, permMap));
|
appVo.setModules(buildModuleTree(modules, permMap));
|
||||||
|
} else {
|
||||||
|
// 无子模块应用(如「一周安排」):授权挂在 -appId 的整应用哨兵记录上(见 AppLevelModuleId),
|
||||||
|
// 有记录即视为已授权,inherit_flag 同步回显,否则该应用勾选态永远无回显。
|
||||||
|
SysGroupModulePerm appPerm = permMap.get(AppLevelModuleId.of(app.getId()));
|
||||||
|
appVo.setChecked(appPerm != null);
|
||||||
|
appVo.setInheritFlag(appPerm != null && Boolean.TRUE.equals(appPerm.getInheritFlag()));
|
||||||
}
|
}
|
||||||
result.add(appVo);
|
result.add(appVo);
|
||||||
}
|
}
|
||||||
@@ -520,6 +527,8 @@ public class GroupAuthzService implements IGroupAuthzService {
|
|||||||
/**
|
/**
|
||||||
* 解析某条授权项实际要操作的模块ID集合。<br>
|
* 解析某条授权项实际要操作的模块ID集合。<br>
|
||||||
* - moduleId 为 null 或 0:返回该应用下全部真实模块的 ID(「整应用」语义)。<br>
|
* - moduleId 为 null 或 0:返回该应用下全部真实模块的 ID(「整应用」语义)。<br>
|
||||||
|
* - 应用下没有任何模块时:返回 {@link AppLevelModuleId#of(Integer)}(-appId)——
|
||||||
|
* 把「整应用」授权落到哨兵记录上,否则这类应用永远写不进授予、也永远回显为空。<br>
|
||||||
* - 具体 moduleId:校验其确实存在且属于该应用,越权写别的应用的模块会被拒绝。<br>
|
* - 具体 moduleId:校验其确实存在且属于该应用,越权写别的应用的模块会被拒绝。<br>
|
||||||
*/
|
*/
|
||||||
private List<Integer> resolveModuleIds(Integer appId, Integer moduleId) {
|
private List<Integer> resolveModuleIds(Integer appId, Integer moduleId) {
|
||||||
@@ -528,7 +537,7 @@ public class GroupAuthzService implements IGroupAuthzService {
|
|||||||
for (AppModule m : appModuleRepository.findByAppIdIn(Collections.singletonList(appId))) {
|
for (AppModule m : appModuleRepository.findByAppIdIn(Collections.singletonList(appId))) {
|
||||||
ids.add(m.getId());
|
ids.add(m.getId());
|
||||||
}
|
}
|
||||||
return ids;
|
return ids.isEmpty() ? Collections.singletonList(AppLevelModuleId.of(appId)) : ids;
|
||||||
}
|
}
|
||||||
AppModule m = appModuleRepository.findById(moduleId).orElse(null);
|
AppModule m = appModuleRepository.findById(moduleId).orElse(null);
|
||||||
if (m == null) {
|
if (m == null) {
|
||||||
@@ -1893,6 +1902,11 @@ public class GroupAuthzService implements IGroupAuthzService {
|
|||||||
siblings.sort(siblingOrder);
|
siblings.sort(siblingOrder);
|
||||||
}
|
}
|
||||||
List<AppModule> roots = childrenMap.getOrDefault(0, Collections.emptyList());
|
List<AppModule> roots = childrenMap.getOrDefault(0, Collections.emptyList());
|
||||||
|
if (roots.isEmpty() && !modules.isEmpty()) {
|
||||||
|
// 脏数据兜底:parent_id 指向已删除模块时根节点全丢,会把「有模块的应用」误判成无模块应用
|
||||||
|
// (前端表现为该应用没有 + 号、勾选态恒为空),此时把所有模块提升为根节点展示
|
||||||
|
roots = modules;
|
||||||
|
}
|
||||||
List<GroupAuthzModuleVo> tree = new ArrayList<>();
|
List<GroupAuthzModuleVo> tree = new ArrayList<>();
|
||||||
for (AppModule root : roots) {
|
for (AppModule root : roots) {
|
||||||
tree.add(buildModuleNode(root, childrenMap, permMap));
|
tree.add(buildModuleNode(root, childrenMap, permMap));
|
||||||
|
|||||||
Reference in New Issue
Block a user