diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/OpenBaseController.java b/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java similarity index 87% rename from src/main/java/com/yunhai/userbase/controller/oapi/OpenBaseController.java rename to src/main/java/com/yunhai/userbase/controller/oapi/UserController.java index 4abe136..de6438d 100644 --- a/src/main/java/com/yunhai/userbase/controller/oapi/OpenBaseController.java +++ b/src/main/java/com/yunhai/userbase/controller/oapi/UserController.java @@ -3,6 +3,7 @@ package com.yunhai.userbase.controller.oapi; 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; @@ -20,16 +21,18 @@ import java.util.Collections; import java.util.List; /** - * 用户中心开放接口:供其他业务服务经 yh-app-auth 签名调用({@code /open/api/user/...})。

- * 路径与基础平台 {@code DefaultUserCenterClient} 硬编码的契约严格一致: - * {@code GET /open/api/user/byId?id=}、{@code /byLoginName?loginName=}、{@code /byPhone?phone=}、 - * {@code /open/api/user/modules?userId=&schId=}。
+ * 用户中心开放接口:供其他业务服务经 yh-app-auth 签名调用({@code /oapi/user/...})。
+ *
+ * 路径复用 {@link Config#OPEN_API_PATH} 统一前缀,与基础平台 {@code DefaultUserCenterClient} + * 硬编码的契约严格一致: + * {@code GET /oapi/user/byId?id=}、{@code /byLoginName?loginName=}、{@code /byPhone?phone=}、 + * {@code /oapi/user/modules?userId=&schId=}。
* 接口本身不做验签——签名由 {@code OpenApiAuthEnforceConfiguration} 在过滤器层强制要求, * 未带 {@code X-App-Key/X-Timestamp/X-Signature} 的请求直接 401。 */ @RestController -@RequestMapping("/open/api/user/") -public class OpenBaseController { +@RequestMapping(Config.OPEN_API_PATH + "user/") +public class UserController { @Autowired private UserCenterService userCenterService; @@ -68,7 +71,7 @@ public class OpenBaseController { */ @GetMapping("modules") public List modules(@RequestParam("userId") Integer userId, - @RequestParam(value = "schId", required = false) Integer schId) { + @RequestParam(value = "schId", required = false) Integer schId) { if (userId == null) { return Collections.emptyList(); } @@ -82,7 +85,8 @@ public class OpenBaseController { } List result = new ArrayList<>(); for (UserPermissionCategoryVo category : tree) { - if (category.getApps() == null) continue; + if (category.getApps() == null) + continue; for (UserPermissionAppVo appVo : category.getApps()) { String icon = appVo.getPcIcon(); collectModules(appVo.getModules(), icon, result); @@ -95,8 +99,8 @@ public class OpenBaseController { * 递归收集已授权模块树到扁平列表,并把应用 PC 图标作为模块的 icon 兜底(BoaModule.icon)。 */ private void collectModules(List modules, - String appIcon, - List collector) { + String appIcon, + List collector) { if (modules == null || modules.isEmpty()) { return; } diff --git a/src/main/java/com/yunhai/userbase/repository/SysGroupAppRelRepository.java b/src/main/java/com/yunhai/userbase/repository/SysGroupAppRelRepository.java index d6bff29..8ec1200 100644 --- a/src/main/java/com/yunhai/userbase/repository/SysGroupAppRelRepository.java +++ b/src/main/java/com/yunhai/userbase/repository/SysGroupAppRelRepository.java @@ -18,6 +18,8 @@ public interface SysGroupAppRelRepository extends JpaRepository findByGroupId(Integer groupId); + List findByGroupIdIn(List groupIds); + boolean existsByGroupIdAndAppId(Integer groupId, Integer appId); long countByAppId(Integer appId); 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 0a27eac..5196c6b 100644 --- a/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java +++ b/src/main/java/com/yunhai/userbase/service/impl/GroupAuthzService.java @@ -97,6 +97,29 @@ public class GroupAuthzService implements IGroupAuthzService { @Autowired private SysUserAppRoleRelRepository sysUserAppRoleRelRepository; + // ===================== 新增:沿祖先链收集「本组+所有祖先组开通的应用ID并集」(应用池联动GetSchoolAppId) ===================== + + /** + * 按用户需求:父组(学校组)若开通了应用(GetSchoolAppId返回非空),则其所有子孙组的模块权限分配面板也能看到这些应用(无需子组重复开通)。
+ * 取应用范围=sys_group_app_rel里group_id属于「本组+全部祖先组」的app_id并集;开通不继承,模块权限池继承。
+ * 对 hasModulePermission / UserPermissionTree 不做修改,避免越权(最终用户拿到权限仍需要自己的归属组开通应用+继承到模块权限,双保险)。 + */ + private Set collectAppIdsForAuthzScope(Integer groupId) { + Map allGroupMap = buildAllGroupMap(); + List chain = ancestorChain(groupId, allGroupMap); + if (chain == null || chain.isEmpty()) { + return Collections.emptySet(); + } + List rels = sysGroupAppRelRepository.findByGroupIdIn(chain); + Set appIds = new LinkedHashSet<>(); + for (SysGroupAppRel rel : rels) { + if (rel.getAppId() != null) { + appIds.add(rel.getAppId()); + } + } + return appIds; + } + // ===================== 功能1:应用开通 ===================== @Override @@ -105,7 +128,6 @@ public class GroupAuthzService implements IGroupAuthzService { if (!sysGroupRepository.existsById(groupId)) { throw new DException("系统组不存在"); } - // 系统全量应用 + 该组已开通集合,标记 checked 回显 List apps = applicationInfoRepository.findAll(); apps.sort(appOrder()); Set openedAppIds = new HashSet<>(); @@ -177,29 +199,22 @@ public class GroupAuthzService implements IGroupAuthzService { if (!sysGroupRepository.existsById(groupId)) { throw new DException("系统组不存在"); } - // 1. 该组已开通的应用;无记录则右侧不显示任何应用 - List rels = sysGroupAppRelRepository.findByGroupId(groupId); - if (rels.isEmpty()) { + Set appIdSet = collectAppIdsForAuthzScope(groupId); + if (appIdSet.isEmpty()) { return Collections.emptyList(); } - List appIds = new ArrayList<>(); - for (SysGroupAppRel rel : rels) { - appIds.add(rel.getAppId()); - } + List appIds = new ArrayList<>(appIdSet); List apps = applicationInfoRepository.findAllById(appIds); apps.sort(appOrder()); - // 2. 已开通应用下的全部模块,按 appId 分桶 List allModules = appModuleRepository.findByAppIdIn(appIds); Map> appModuleMap = new HashMap<>(); for (AppModule m : allModules) { appModuleMap.computeIfAbsent(m.getAppId(), k -> new ArrayList<>()).add(m); } - // 3. 权限回显:该组已勾选的模块权限记录 Map permMap = new HashMap<>(); for (SysGroupModulePerm perm : sysGroupModulePermRepository.findByGroupId(groupId)) { permMap.put(perm.getModuleId(), perm); } - // 4. 逐应用组装模块树 List result = new ArrayList<>(); for (ApplicationInfo app : apps) { GroupAuthzAppVo appVo = new GroupAuthzAppVo(); @@ -290,15 +305,10 @@ public class GroupAuthzService implements IGroupAuthzService { } List perms = request.getPerms() == null ? Collections.emptyList() : request.getPerms(); - // 勾选的模块必须属于该组已开通应用(边界情况:未开通应用的模块权限无效,不允许写入) + Set appIdSet = collectAppIdsForAuthzScope(groupId); Set validModuleIds = new HashSet<>(); - List rels = sysGroupAppRelRepository.findByGroupId(groupId); - if (!rels.isEmpty()) { - List appIds = new ArrayList<>(); - for (SysGroupAppRel rel : rels) { - appIds.add(rel.getAppId()); - } - for (AppModule m : appModuleRepository.findByAppIdIn(appIds)) { + if (!appIdSet.isEmpty()) { + for (AppModule m : appModuleRepository.findByAppIdIn(new ArrayList<>(appIdSet))) { validModuleIds.add(m.getId()); } } @@ -418,31 +428,22 @@ public class GroupAuthzService implements IGroupAuthzService { Integer moduleId = item.getModuleId(); int mode = item.getMode(); boolean isAll = item.getIsAll() != null && item.getIsAll() == 1; - // 基础校验 if (!sysGroupRepository.existsById(groupId)) { throw new DException("系统组不存在: " + groupId); } if (!applicationInfoRepository.existsById(appId)) { throw new DException("应用不存在: " + appId); } - // 操作模式:仅 0=追加 / 2=删除 合法(同 SetSchoolApp,经 IEnum.getByKey 解析) SysModeEnum modeEnum = IEnum.getByKey(SysModeEnum.class, mode); if (modeEnum == null || (modeEnum != SysModeEnum.APPEND && modeEnum != SysModeEnum.DELETE)) { throw new DException("不支持的操作模式: " + mode + ",模块授权仅支持 0(追加)/2(删除)"); } - // 解析本次要操作的模块集合:moduleId=0 → 该应用下全部模块(展开,不写 module_id=0 伪记录) List targetModuleIds = resolveModuleIds(appId, moduleId); - // isAll 语义(重点):映射 sys_group_module_perm.inherit_flag。 - // isAll=1 → inheritFlag=true:子孙组在 hasModulePermission 判定时沿祖先链自动继承(逻辑继承,不物理写子组)。 - // isAll=0 → inheritFlag=false:仅本组生效。 - // —— 与「应用不继承 / 模块可继承」对称,复用现有权限判定,不破坏其他功能。 - // —— 若需「物理写子组」:将下方 upsert/delete 作用范围由 [groupId] 改为 [groupId + 其全部子孙组] 即可 - // (子孙组集合可由 sys_group 全量构建 parentId→children 映射后 DFS 得到,参考本类 ancestorChain 的上行写法)。 + Set scopeAppIds = collectAppIdsForAuthzScope(groupId); for (Integer mid : targetModuleIds) { if (modeEnum == SysModeEnum.APPEND) { - // 边界一致:该组须已开通该应用,未开通应用的模块权限无意义 - if (!sysGroupAppRelRepository.existsByGroupIdAndAppId(groupId, appId)) { - throw new DException("组 " + groupId + " 未开通应用 " + appId + ",无法授权其模块 " + mid); + if (!scopeAppIds.contains(appId)) { + throw new DException("组及其祖先均未开通应用 " + appId + ",无法授权其模块 " + mid); } SysGroupModulePerm existing = sysGroupModulePermRepository.findByGroupIdAndModuleId(groupId, mid); if (existing != null) { @@ -457,7 +458,7 @@ public class GroupAuthzService implements IGroupAuthzService { perm.setInheritFlag(isAll); sysGroupModulePermRepository.save(perm); } - } else { // DELETE 取消模块授权 + } else { sysGroupModulePermRepository.deleteByGroupIdAndModuleIdBulk(groupId, mid); } }