From fe6c07cc4b8c2ef56f3eb76c3068d94693a2182b Mon Sep 17 00:00:00 2001 From: chenxing Date: Tue, 20 Oct 2026 12:18:22 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E6=8E=88=E6=9D=83):=20=E7=88=B6=E7=BA=A7?= =?UTF-8?q?=E5=8B=BE=E9=80=89=E6=80=81=E6=94=B9=E3=80=8C=E4=BB=BB=E4=B8=80?= =?UTF-8?q?=E5=AD=90=E8=8A=82=E7=82=B9=E9=80=89=E4=B8=AD=E5=8D=B3=E9=80=89?= =?UTF-8?q?=E4=B8=AD=E3=80=8D+=20=E6=97=A0=E6=A8=A1=E5=9D=97=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E6=95=B4=E5=BA=94=E7=94=A8=E6=8E=88=E6=9D=83=EF=BC=88?= =?UTF-8?q?-appId=20=E5=93=A8=E5=85=B5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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)。 --- .../userbase/constant/AppLevelModuleId.java | 45 +++++++++++++++++++ .../model/approle/AppRoleAuthzAppVo.java | 7 +++ .../model/groupauthz/GroupAuthzAppVo.java | 10 +++++ .../groupauthz/GroupModuleAuthzItem.java | 4 +- .../userbase/service/impl/AppRoleService.java | 19 +++++++- .../service/impl/GroupAuthzService.java | 18 +++++++- 6 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/yunhai/userbase/constant/AppLevelModuleId.java diff --git a/src/main/java/com/yunhai/userbase/constant/AppLevelModuleId.java b/src/main/java/com/yunhai/userbase/constant/AppLevelModuleId.java new file mode 100644 index 0000000..713432b --- /dev/null +++ b/src/main/java/com/yunhai/userbase/constant/AppLevelModuleId.java @@ -0,0 +1,45 @@ +package com.yunhai.userbase.constant; + +/** + * 「整应用授权」的 module_id 哨兵编码。
+ *

+ * 授权关系表 {@code sys_group_module_perm} / {@code sys_app_role_module_rel} 都只有 + * (owner_id, module_id) 两列,没有 app_id。当某个应用下没有任何 {@code app_module} 记录时 + * (如「一周安排」这类单页应用),模块级授权无处可挂,于是用 -appId 作为该应用 + * 「整体已授权」的哨兵值: + *

+ * 写入侧由 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; + } +} diff --git a/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzAppVo.java b/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzAppVo.java index 0d41d16..13c5dfd 100644 --- a/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzAppVo.java +++ b/src/main/java/com/yunhai/userbase/model/approle/AppRoleAuthzAppVo.java @@ -29,6 +29,13 @@ public class AppRoleAuthzAppVo { /** 排序号。 */ private Integer sortNo; + /** + * 应用级勾选态:仅当该应用下没有任何模块时才有效。
+ * 此时授权落在 sys_app_role_module_rel 的 -appId 哨兵记录上(见 {@code AppLevelModuleId}); + * 有模块的应用恒为 false,勾选态由 modules 树逐节点表达。 + */ + private Boolean checked = Boolean.FALSE; + /** 该应用下的模块权限树(含勾选回显)。 */ private List modules = new ArrayList<>(); } diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzAppVo.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzAppVo.java index c90d124..87f9cfa 100644 --- a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzAppVo.java +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupAuthzAppVo.java @@ -30,6 +30,16 @@ public class GroupAuthzAppVo { private Integer sortNo; + /** + * 应用级勾选态:仅当该应用下没有任何模块时才有效。
+ * 此时授权落在 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。 */ private List modules = new ArrayList<>(); } diff --git a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModuleAuthzItem.java b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModuleAuthzItem.java index ef3b2c4..52bbb9e 100644 --- a/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModuleAuthzItem.java +++ b/src/main/java/com/yunhai/userbase/model/groupauthz/GroupModuleAuthzItem.java @@ -32,7 +32,9 @@ public class GroupModuleAuthzItem { /** * 模块ID;传 0 表示「该应用下全部模块」。
* 注意:后端不会向 sys_group_module_perm 写入 module_id=0 的伪记录,
- * 而是把 0 展开为应用下每个真实模块各写一条权限记录。 + * 而是把 0 展开为应用下每个真实模块各写一条权限记录。
+ * 特殊情形:应用下一个模块都没有时,改写成 -appId 的整应用哨兵记录 + * (见 {@code AppLevelModuleId}),保证这类应用也能被授权与回显。 */ @NotNull(message = "模块ID不能为空(整应用传 0)") private Integer moduleId; diff --git a/src/main/java/com/yunhai/userbase/service/impl/AppRoleService.java b/src/main/java/com/yunhai/userbase/service/impl/AppRoleService.java index 94dd524..3ad25ed 100644 --- a/src/main/java/com/yunhai/userbase/service/impl/AppRoleService.java +++ b/src/main/java/com/yunhai/userbase/service/impl/AppRoleService.java @@ -9,6 +9,7 @@ import com.yunhai.userbase.entity.SysGroup; import com.yunhai.userbase.entity.SysUser; import com.yunhai.userbase.entity.SysUserAppRoleRel; import com.yunhai.userbase.entity.SysUserGroupRel; +import com.yunhai.userbase.constant.AppLevelModuleId; import com.yunhai.userbase.constant.AppNature; import com.yunhai.userbase.event.AppAuthzChangedEvent; import com.yunhai.userbase.model.approle.AppRoleAuthzAppVo; @@ -242,8 +243,12 @@ public class AppRoleService implements IAppRoleService { AppRoleAuthzAppVo appVo = new AppRoleAuthzAppVo(); BeanUtils.copyProperties(app, appVo); List modules = appModuleMap.get(app.getId()); - if (modules != null) { + if (modules != null && !modules.isEmpty()) { appVo.setModules(buildModuleTree(modules, checkedModuleIds)); + } else { + // 无子模块应用:授权挂在 -appId 的整应用哨兵记录上(见 AppLevelModuleId), + // 否则这类应用勾选态永远无回显(勾了刷新就丢)。 + appVo.setChecked(checkedModuleIds.contains(AppLevelModuleId.of(app.getId()))); } result.add(appVo); } @@ -266,6 +271,18 @@ public class AppRoleService implements IAppRoleService { moduleMap.put(m.getId(), m); } 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); if (m == null) { throw new DException("模块不存在: " + moduleId); diff --git a/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java b/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java index 03b8d2b..2941e99 100644 --- a/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java +++ b/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java @@ -3,6 +3,7 @@ package com.yunhai.userbase.service.impl; import com.yunhai.platform.utils.exception.DException; import com.yunhai.platform.utils.model.BoaModule; import com.yunhai.platform.utils.model.BoaRoleInfo; +import com.yunhai.userbase.constant.AppLevelModuleId; import com.yunhai.userbase.entity.*; import com.yunhai.userbase.enums.GroupTypeEnum; import com.yunhai.platform.utils.enums.IEnum; @@ -261,8 +262,14 @@ public class GroupAuthzService implements IGroupAuthzService { GroupAuthzAppVo appVo = new GroupAuthzAppVo(); BeanUtils.copyProperties(app, appVo); List modules = appModuleMap.get(app.getId()); - if (modules != null) { + if (modules != null && !modules.isEmpty()) { 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); } @@ -520,6 +527,8 @@ public class GroupAuthzService implements IGroupAuthzService { /** * 解析某条授权项实际要操作的模块ID集合。
* - moduleId 为 null 或 0:返回该应用下全部真实模块的 ID(「整应用」语义)。
+ * - 应用下没有任何模块时:返回 {@link AppLevelModuleId#of(Integer)}(-appId)—— + * 把「整应用」授权落到哨兵记录上,否则这类应用永远写不进授予、也永远回显为空。
* - 具体 moduleId:校验其确实存在且属于该应用,越权写别的应用的模块会被拒绝。
*/ private List resolveModuleIds(Integer appId, Integer moduleId) { @@ -528,7 +537,7 @@ public class GroupAuthzService implements IGroupAuthzService { for (AppModule m : appModuleRepository.findByAppIdIn(Collections.singletonList(appId))) { ids.add(m.getId()); } - return ids; + return ids.isEmpty() ? Collections.singletonList(AppLevelModuleId.of(appId)) : ids; } AppModule m = appModuleRepository.findById(moduleId).orElse(null); if (m == null) { @@ -1893,6 +1902,11 @@ public class GroupAuthzService implements IGroupAuthzService { siblings.sort(siblingOrder); } List roots = childrenMap.getOrDefault(0, Collections.emptyList()); + if (roots.isEmpty() && !modules.isEmpty()) { + // 脏数据兜底:parent_id 指向已删除模块时根节点全丢,会把「有模块的应用」误判成无模块应用 + // (前端表现为该应用没有 + 号、勾选态恒为空),此时把所有模块提升为根节点展示 + roots = modules; + } List tree = new ArrayList<>(); for (AppModule root : roots) { tree.add(buildModuleNode(root, childrenMap, permMap));