feat(oapi): GetUserMod 下发嵌套模块树,层级经 BoaModule.children 承载
build / build-push (push) Successful in 2m42s

模块表 app_module 是 level<=5 的树,管理端下发时带 children,但开放接口这条链
转成平台 BoaModule 时层级被拍平(且旧 SDK 契约无 children 字段可承载),导致每个
子应用各自复制一份 buildModuleTree 重建父子关系。

现按同目录 BoaGroup.children 的既有口径补齐:findUserModulesForApp 的三个返回点
(超管短路 / 应用无模块 / 正常路径)统一收口到 assembleModuleTree 建树。

语义:顶层只放真根(parentId 为空),兄弟沿用开放接口原有 customId 升序口径(不与
管理端 sortNo+id 统一),叶子 children 为空列表以便前端无判空递归;父模块不在本次已
授权集合内的孤儿子节点连同整枝子孙一并丢弃,并逐条打 warn([oapi:modules] drop
orphan module)避免静默少菜单——授权配置须保证「授权子模块必先授权父模块」。

新增 GroupAuthzServiceModuleTreeTest 9 例:三级嵌套与排序、sortNo 不参与、叶子空
列表、parentId 成环不死循环、全 level-1 时与改造前扁平输出等价、无模块返空、孤儿整
枝丢弃、父子同授权则嵌套、JSON 往返证明层级确实经线格式下发且可反序列化。
This commit is contained in:
chenxing
2026-09-15 16:20:05 +08:00
parent 0b67382076
commit b0a02e56b0
3 changed files with 442 additions and 13 deletions
@@ -123,19 +123,24 @@ public interface IGroupAuthzService {
List<UserGroupNodeVo> findUserGroupTree(Integer userId);
/**
* 开放接口专用:查询用户在<b>指定调用方应用</b>下的已授权模块(扁平化 BoaModule 列表)。<br>
* 开放接口专用:查询用户在<b>指定调用方应用</b>下的已授权模块(<b>嵌套 BoaModule 树</b>,层级由
* {@link BoaModule#getChildren()} 承载,消费方无需再自行组树)。<br>
* 与 {@link #findUserPermissionTree(Integer, Integer)} 的核心差别:<br>
* - 后者返回用户在全部已开通应用下的权限聚合(供管理端权限一览使用);<br>
* - 本方法只返回 {@code appId} 对应这一个应用下的模块,所有 SQL/集合构建均限定该应用(数据库层面过滤)。<br>
* <br>
* 上下文组(schId)语义与开放接口一致:schId 非空=只认该组链路;schId 为空=取用户主学校组(若仍空=用户主归属组 id)。<br>
* 超级管理员:不做组/继承判定,直接返回该应用下全部模块(但仍只返回该应用,不会跨应用返回)。<br>
* 角色授权并集:最终权限 = 组授权模块 ∪ 启用角色下属于该应用的模块(逻辑与权限一览保持一致)。
* 角色授权并集:最终权限 = 组授权模块 ∪ 启用角色下属于该应用的模块(逻辑与权限一览保持一致)。<br>
* <br>
* 树形语义(2026-09-15 定稿):顶层只放真根(parentId 为空),兄弟按 customId 升序,叶子 children 为空列表;
* <b>父模块未授权(不在本次结果集内)的子节点连同其整枝子孙一并丢弃</b>并打 warn——
* 故授权配置须保证「授权子模块必先授权父模块」。
*
* @param appId 调用方应用ID(必填;查不到或该应用无开通=返回空列表)
* @param schId 学校/组上下文(可选;为 null 时按"主学校→主归属组"规则推断)
* @param userId 用户ID(必填)
* @return 该应用下用户有权限的模块(扁平化;无权限时返回空列表)
* @return 该应用下用户有权限的模块树(顶层=根节点;无权限时返回空列表)
*/
List<BoaModule> findUserModulesForApp(Integer appId, Integer schId, Integer userId);
@@ -37,6 +37,8 @@ import com.yunhai.userbase.service.IApplicationInfoService;
import com.yunhai.userbase.service.IGroupAuthzService;
import com.yunhai.userbase.service.impl.UserCenterService;
import com.yunhai.userbase.service.resolve.MainGroupResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
@@ -71,6 +73,8 @@ import java.util.stream.Collectors;
@Service
public class GroupAuthzService implements IGroupAuthzService {
private static final Logger log = LoggerFactory.getLogger(GroupAuthzService.class);
/** 虚拟父节点:根节点的 parentId,表示「无上级」(与 SysGroupService 保持一致)。 */
private static final int ROOT_PARENT_ID = 0;
@@ -545,7 +549,7 @@ public class GroupAuthzService implements IGroupAuthzService {
// 2. 超级管理员短路:不再返回全应用模块=仅返回该应用下全部模块(数据库过滤第二层:appId 限定)
if (isSuperAdmin(user.getType(), userId)) {
List<AppModule> allModules = appModuleRepository.findByAppIdIn(Collections.singletonList(appId));
return flattenToBoaModules(allModules, appIcon);
return assembleModuleTree(allModules, appIcon);
}
// 3. 收集该用户启用角色下=属于目标应用的模块ID(角色授权并集,不要求组先开通应用)
Set<Integer> roleModuleIdsInApp = collectEnabledRoleModuleIdsForApp(userId, appId);
@@ -594,7 +598,7 @@ public class GroupAuthzService implements IGroupAuthzService {
List<AppModule> appModules = appModuleRepository.findByAppIdIn(Collections.singletonList(appId));
if (appModules.isEmpty()) {
return (roleModuleIdsInApp.isEmpty()) ? Collections.emptyList()
: flattenToBoaModulesById(roleModuleIdsInApp, appIcon, buildAppModuleMap(appModules));
: assembleModuleTreeById(roleModuleIdsInApp, appIcon, buildAppModuleMap(appModules));
}
Set<Integer> appModuleIds = new LinkedHashSet<>();
for (AppModule m : appModules) {
@@ -636,7 +640,7 @@ public class GroupAuthzService implements IGroupAuthzService {
if (permittedModuleIds.isEmpty()) {
return Collections.emptyList();
}
return flattenToBoaModulesById(permittedModuleIds, appIcon, buildAppModuleMap(appModules));
return assembleModuleTreeById(permittedModuleIds, appIcon, buildAppModuleMap(appModules));
}
/**
@@ -668,19 +672,82 @@ public class GroupAuthzService implements IGroupAuthzService {
return map;
}
private static List<BoaModule> flattenToBoaModules(List<AppModule> modules, String icon) {
/**
* GetUserMod/oapi 模块树组装(本服务三处返回的唯一收口):扁平 AppModule → 嵌套 BoaModule 树。<br>
* 层级由平台契约 {@link BoaModule#getChildren()} 承载,消费方(Notice/svc-hall/flow-center 等)不再各自重建子树。<br>
* <br>
* 语义(2026-09-15 定稿,勿随手改):<br>
* 1. 顶层只放真根(parentId 为 null 或 0);<b>父模块不在本次已授权集合内的孤儿子节点,连同其整枝子孙一并丢弃</b>,
* 「授权子模块必先授权父模块」由授权配置侧保证,被丢弃项统一经 {@link #warnDropped} 告警,不静默;<br>
* 2. 兄弟节点按 {@link #moduleOrder()}(customId 升序)排序——沿用开放接口既有口径,<b>不与管理端
* {@code buildModuleTree} 的 sortNo(空=999)+id 统一</b>(后者是权限回显树、绑 permMap,语义不同,勿复用);<br>
* 3. 叶子节点 children 置<b>空列表</b>(非 null),使前端可无判空递归;null 只表示「本接口未展开」。<br>
* 采用自顶向下遍历真根,故 parentId 脏数据成环的节点天然不可达(等同孤儿被丢弃),不存在无限递归。
*/
private static List<BoaModule> assembleModuleTree(List<AppModule> modules, String icon) {
if (modules == null || modules.isEmpty()) {
return Collections.emptyList();
}
modules.sort(moduleOrder());
List<BoaModule> result = new ArrayList<>(modules.size());
// id → 已转换节点(每个模块只 copy 一次)
Map<Integer, BoaModule> nodeMap = new LinkedHashMap<>();
// parentId → 兄弟桶(null 与 0 同桶,对齐 ROOT_PARENT_ID 语义)
Map<Integer, List<AppModule>> childrenMap = new HashMap<>();
for (AppModule m : modules) {
result.add(toBoaModule(m, icon));
if (m.getId() == null) {
continue;
}
nodeMap.put(m.getId(), toBoaModule(m, icon));
Integer parentKey = (m.getParentId() == null) ? ROOT_PARENT_ID : m.getParentId();
childrenMap.computeIfAbsent(parentKey, k -> new ArrayList<>()).add(m);
}
return result;
Comparator<AppModule> siblingOrder = moduleOrder();
for (List<AppModule> siblings : childrenMap.values()) {
siblings.sort(siblingOrder);
}
Set<Integer> emitted = new HashSet<>();
List<BoaModule> roots = new ArrayList<>();
for (AppModule root : childrenMap.getOrDefault(ROOT_PARENT_ID, Collections.emptyList())) {
roots.add(buildModuleNode(root, childrenMap, nodeMap, emitted));
}
warnDropped(modules, emitted);
return roots;
}
private static List<BoaModule> flattenToBoaModulesById(Set<Integer> moduleIds, String icon,
/** 递归挂子:children 一律赋值(无子=空列表),保证前端无需判空即可递归。 */
private static BoaModule buildModuleNode(AppModule node, Map<Integer, List<AppModule>> childrenMap,
Map<Integer, BoaModule> nodeMap, Set<Integer> emitted) {
BoaModule bm = nodeMap.get(node.getId());
emitted.add(node.getId());
List<BoaModule> children = new ArrayList<>();
List<AppModule> subs = childrenMap.get(node.getId());
if (subs != null) {
for (AppModule sub : subs) {
// 同一 id 只挂一次,防脏数据重复行让同一实例被挂两处形成环
if (sub.getId() != null && !emitted.contains(sub.getId())) {
children.add(buildModuleNode(sub, childrenMap, nodeMap, emitted));
}
}
}
bm.setChildren(children);
return bm;
}
/**
* 对被丢弃(父未授权 → 自根不可达)的模块各打一条 warn,使「某角色/某组菜单少一项」能直接定位到授权配置缺口。<br>
* 只告警不抛异常:丢弃本身是本接口的既定语义。
*/
private static void warnDropped(List<AppModule> modules, Set<Integer> emitted) {
for (AppModule m : modules) {
if (m.getId() == null || emitted.contains(m.getId())) {
continue;
}
log.warn("[oapi:modules] drop orphan module id={} customId={} name={} parentId={} appId={}"
+ "(父模块不在本次已授权集合内,整枝未下发;请在角色/组授权中一并勾选其父模块)",
m.getId(), m.getCustomId(), m.getName(), m.getParentId(), m.getAppId());
}
}
private static List<BoaModule> assembleModuleTreeById(Set<Integer> moduleIds, String icon,
Map<Integer, AppModule> moduleMap) {
if (moduleIds == null || moduleIds.isEmpty()) {
return Collections.emptyList();
@@ -692,7 +759,7 @@ public class GroupAuthzService implements IGroupAuthzService {
list.add(m);
}
}
return flattenToBoaModules(list, icon);
return assembleModuleTree(list, icon);
}
private static BoaModule toBoaModule(AppModule m, String icon) {
@@ -0,0 +1,357 @@
package com.yunhai.userbase.service.impl;
import com.yunhai.platform.utils.model.BoaModule;
import com.yunhai.userbase.entity.AppModule;
import com.yunhai.userbase.entity.ApplicationInfo;
import com.yunhai.userbase.entity.SysGroup;
import com.yunhai.userbase.entity.SysGroupAppRel;
import com.yunhai.userbase.entity.SysGroupModulePerm;
import com.yunhai.userbase.entity.SysUser;
import com.yunhai.userbase.entity.SysUserGroupRel;
import com.yunhai.userbase.enums.GroupTypeEnum;
import com.yunhai.userbase.enums.UserTypeEnum;
import com.yunhai.userbase.model.GroupSnapshot;
import com.yunhai.userbase.repository.AppCategoryRepository;
import com.yunhai.userbase.repository.AppModuleRepository;
import com.yunhai.userbase.repository.ApplicationInfoRepository;
import com.yunhai.userbase.repository.SysAppRoleModuleRelRepository;
import com.yunhai.userbase.repository.SysAppRoleRepository;
import com.yunhai.userbase.repository.SysGroupAppRelRepository;
import com.yunhai.userbase.repository.SysGroupModulePermRepository;
import com.yunhai.userbase.repository.SysGroupRepository;
import com.yunhai.userbase.repository.SysUserAppRoleRelRepository;
import com.yunhai.userbase.repository.SysUserGroupRelRepository;
import com.yunhai.userbase.repository.SysUserRepository;
import com.yunhai.userbase.service.IApplicationInfoService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.context.ApplicationEventPublisher;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.when;
/**
* GetUserMod({@code findUserModulesForApp})模块树组装单元测试。<br>
* 锁定 2026-09-15 定稿语义:层级经 {@code BoaModule.children} 下发;顶层只放真根;
* <b>父未授权的孤儿子节点连同整枝子孙一并丢弃</b>;兄弟按 customId(数值优先)升序,sortNo 不参与;
* 叶子 children 为空列表(非 null);parentId 脏数据成环不死循环。<br>
* 多数用例走「超管短路」分支({@code GroupAuthzService:546}),它与正常权限分支共用同一个建树收口
* {@code assembleModuleTree},故可最小化桩;另有一例走真实组授权分支验证丢弃语义。
*/
class GroupAuthzServiceModuleTreeTest {
private static final int APP_ID = 100003;
private static final String APP_ICON = "/icon/notice.png";
@InjectMocks
private GroupAuthzService groupAuthzService;
@Mock
private GroupSnapshotService groupSnapshotService;
@Mock
private SysGroupAppRelRepository sysGroupAppRelRepository;
@Mock
private SysGroupModulePermRepository sysGroupModulePermRepository;
@Mock
private SysGroupRepository sysGroupRepository;
@Mock
private ApplicationInfoRepository applicationInfoRepository;
@Mock
private IApplicationInfoService applicationInfoService;
@Mock
private AppCategoryRepository appCategoryRepository;
@Mock
private AppModuleRepository appModuleRepository;
@Mock
private SysUserGroupRelRepository sysUserGroupRelRepository;
@Mock
private SysUserRepository sysUserRepository;
@Mock
private SysAppRoleRepository sysAppRoleRepository;
@Mock
private SysAppRoleModuleRelRepository sysAppRoleModuleRelRepository;
@Mock
private SysUserAppRoleRelRepository sysUserAppRoleRelRepository;
@Mock
private ApplicationEventPublisher eventPublisher;
@Mock
private com.yunhai.userbase.service.resolve.MainGroupResolver mainGroupResolver;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
ApplicationInfo app = new ApplicationInfo();
app.setId(APP_ID);
app.setName("通知公告");
app.setPcIcon(APP_ICON);
when(applicationInfoService.getById(APP_ID)).thenReturn(app);
}
// ===================== 超管短路:纯建树逻辑 =====================
@Test
void findUserModulesForApp_whenThreeLevels_shouldNestAndOrderByCustomIdNumerically() {
// 树:100(工作台) > 101(我的办件) > 102(在办) ;100 另有兄弟 200(设置)
AppModule r1 = module(100, "100", null, 1, 900);
AppModule c1 = module(101, "101", 100, 2, 10);
AppModule g1 = module(102, "102", 101, 3, 10);
AppModule r2 = module(200, "200", null, 1, 10);
stubSuperAdmin(r1, c1, g1, r2);
List<BoaModule> roots = groupAuthzService.findUserModulesForApp(APP_ID, null, superAdminId());
assertEquals(2, roots.size(), "顶层只应有两个真根");
assertEquals("100", roots.get(0).getCustomId(), "customId 数值序:100 在 200 前");
assertEquals(APP_ICON, roots.get(0).getIcon(), "icon 仍按应用级下发到每个节点");
BoaModule child = only(roots.get(0).getChildren());
assertEquals(101, child.getId());
BoaModule grand = only(child.getChildren());
assertEquals(102, grand.getId(), "三级层级应完整保留");
assertEquals("3", String.valueOf(grand.getLevel()));
}
@Test
void findUserModulesForApp_whenSortNoContradictsCustomId_shouldStillOrderByCustomId() {
// 故意让 sortNo 与 customId 反序:证明开放接口不采用管理端的 sortNo(空=999)+id 口径
AppModule a = module(11, "2", null, 1, 1);
AppModule b = module(12, "10", null, 1, 999);
stubSuperAdmin(a, b);
List<BoaModule> roots = groupAuthzService.findUserModulesForApp(APP_ID, null, superAdminId());
assertEquals("2", roots.get(0).getCustomId(), "兄弟序按 customId 数值升序,sortNo 不参与");
assertEquals("10", roots.get(1).getCustomId());
}
@Test
void findUserModulesForApp_whenLeaf_shouldEmitEmptyChildrenListNotNull() {
stubSuperAdmin(module(100, "100", null, 1, 10));
List<BoaModule> roots = groupAuthzService.findUserModulesForApp(APP_ID, null, superAdminId());
BoaModule leaf = only(roots);
assertNotNull(leaf.getChildren(), "叶子 children 不得为 null,前端需无判空递归");
assertTrue(leaf.getChildren().isEmpty(), "叶子 children 应为空列表");
}
@Test
void findUserModulesForApp_whenParentIdCycle_shouldNotHangAndDropCycleBranch() {
AppModule root = module(100, "100", null, 1, 10);
AppModule a = module(201, "201", 202, 2, 10); // a 的父是 b
AppModule b = module(202, "202", 201, 2, 10); // b 的父是 a → 成环,自根不可达
AppModule self = module(203, "203", 203, 2, 10); // 自环
stubSuperAdmin(root, a, b, self);
List<BoaModule> roots = groupAuthzService.findUserModulesForApp(APP_ID, null, superAdminId());
assertEquals(1, roots.size(), "成环/自环节点整体不下发,只剩真根");
assertEquals(100, only(roots).getId());
}
@Test
void findUserModulesForApp_whenOnlyLevelOne_shouldKeepLegacyFlatSetAndOrder() {
// 回归护栏:全 level-1(现网常态)时,顶层集合与顺序应与改造前的扁平输出一致,仅多出空 children
AppModule m1 = module(5, "1005", null, 1, 50);
AppModule m2 = module(6, "1006", null, 1, 10);
stubSuperAdmin(m1, m2);
List<BoaModule> roots = groupAuthzService.findUserModulesForApp(APP_ID, null, superAdminId());
assertEquals(Arrays.asList(1005, 1006), customIds(roots), "全扁平模块时下发顺序=customId 升序,等价旧行为");
for (BoaModule m : roots) {
assertTrue(m.getChildren().isEmpty());
}
}
@Test
void findUserModulesForApp_whenAppHasNoModule_shouldReturnEmptyList() {
stubSuperAdmin();
List<BoaModule> roots = groupAuthzService.findUserModulesForApp(APP_ID, null, superAdminId());
assertTrue(roots.isEmpty(), "应用无模块时返回空列表而非 null");
}
/**
* 症结回归护栏:层级必须<b>经 wire 下发</b>。序列化后 JSON 里要能看到 children 嵌套,
* 反序列化回来子节点仍可读——否则等于本次修复失效(消费方又得自己组树)。
*/
@Test
void findUserModulesForApp_treeSurvivesJsonRoundTripOnTheWire() throws Exception {
stubSuperAdmin(module(100, "100", null, 1, 10), module(101, "101", 100, 2, 10));
List<BoaModule> roots = groupAuthzService.findUserModulesForApp(APP_ID, null, superAdminId());
String json = new com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(roots);
assertTrue(json.contains("\"children\":["), "JSON 里必须带 children 嵌套,实际=" + json);
assertTrue(json.indexOf("\"customId\":\"101\"") > json.indexOf("\"customId\":\"100\""),
"子节点应嵌在父节点内部(出现在父的 children 里)");
List<BoaModule> parsed = new com.fasterxml.jackson.databind.ObjectMapper().readValue(json,
new com.fasterxml.jackson.core.type.TypeReference<List<BoaModule>>() {
});
assertEquals(1, parsed.size(), "反序列化后顶层只应有父节点");
assertEquals("101", only(parsed.get(0).getChildren()).getCustomId(), "反序列化后子节点仍可读出");
}
// ===================== 真实权限分支:孤儿丢弃 =====================
@Test
void findUserModulesForApp_whenChildGrantedButParentNot_shouldDropWholeBranch() {
// 云海中学(16) 归属用户;应用 100003 已开通
SysUser normal = user(2, UserTypeEnum.NORMAL);
when(sysUserRepository.findById(2)).thenReturn(Optional.of(normal));
when(sysUserGroupRelRepository.findByUserId(2))
.thenReturn(Collections.singletonList(rel(2, 16)));
initGroups();
when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv ->
((List<Integer>) inv.getArgument(0)).contains(16)
? Collections.singletonList(appRel(16, APP_ID)) : Collections.emptyList());
when(sysUserAppRoleRelRepository.findByUserId(2)).thenReturn(Collections.emptyList());
// 模块:父 100 → 子 101 → 孙 102;另有独立根 200
AppModule parent = module(100, "100", null, 1, 10);
AppModule child = module(101, "101", 100, 2, 10);
AppModule grand = module(102, "102", 101, 3, 10);
AppModule otherRoot = module(200, "200", null, 1, 20);
when(appModuleRepository.findByAppIdIn(anyList()))
.thenReturn(new ArrayList<>(Arrays.asList(parent, child, grand, otherRoot)));
// 关键:只授权了子 101/孙 102 与根 200,父 100 未授权
when(sysGroupModulePermRepository.findByModuleIdInAndGroupIdIn(anyList(), anyList()))
.thenReturn(Arrays.asList(perm(16, 101), perm(16, 102), perm(16, 200)));
List<BoaModule> roots = groupAuthzService.findUserModulesForApp(APP_ID, 16, 2);
assertEquals(1, roots.size(), "父未授权 → 子与孙整枝丢弃,只剩已授权的独立根");
assertEquals("200", roots.get(0).getCustomId());
assertTrue(roots.get(0).getChildren().isEmpty());
}
@Test
void findUserModulesForApp_whenParentAndChildBothGranted_shouldNestChildUnderParent() {
SysUser normal = user(3, UserTypeEnum.NORMAL);
when(sysUserRepository.findById(3)).thenReturn(Optional.of(normal));
when(sysUserGroupRelRepository.findByUserId(3))
.thenReturn(Collections.singletonList(rel(3, 16)));
initGroups();
when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv ->
((List<Integer>) inv.getArgument(0)).contains(16)
? Collections.singletonList(appRel(16, APP_ID)) : Collections.emptyList());
when(sysUserAppRoleRelRepository.findByUserId(3)).thenReturn(Collections.emptyList());
AppModule parent = module(100, "100", null, 1, 10);
AppModule child = module(101, "101", 100, 2, 10);
when(appModuleRepository.findByAppIdIn(anyList()))
.thenReturn(new ArrayList<>(Arrays.asList(parent, child)));
when(sysGroupModulePermRepository.findByModuleIdInAndGroupIdIn(anyList(), anyList()))
.thenReturn(Arrays.asList(perm(16, 100), perm(16, 101)));
List<BoaModule> roots = groupAuthzService.findUserModulesForApp(APP_ID, 16, 3);
assertEquals(1, roots.size(), "父子均授权时子应挂在父下而非浮到顶层");
assertEquals("100", roots.get(0).getCustomId());
assertEquals("101", only(roots.get(0).getChildren()).getCustomId());
}
// ===================== 夹具 =====================
/** 超管短路:只需 app 存在 + 用户为超管 + 该应用下模块清单。 */
private void stubSuperAdmin(AppModule... modules) {
int userId = superAdminId();
when(sysUserRepository.findById(userId)).thenReturn(Optional.of(user(userId, UserTypeEnum.SUPER_ADMIN)));
when(appModuleRepository.findByAppIdIn(anyList()))
.thenReturn(new ArrayList<>(Arrays.asList(modules)));
}
private int superAdminId() {
return com.yunhai.userbase.utils.DicUtil.SUPER_ADMIN_ID;
}
/** 固定组层级:5 教育云中心 → 27 信阳教育局 → 16 云海中学。 */
private void initGroups() {
List<SysGroup> groups = Arrays.asList(
group(5, "教育云中心", 0, GroupTypeEnum.NORMAL_GROUP),
group(27, "信阳教育局", 5, GroupTypeEnum.AREA_GROUP),
group(16, "云海中学", 27, GroupTypeEnum.SCHOOL_GROUP));
when(groupSnapshotService.snapshot()).thenReturn(new GroupSnapshot(groups));
}
private static List<Integer> customIds(List<BoaModule> list) {
List<Integer> ids = new ArrayList<>();
for (BoaModule m : list) {
ids.add(Integer.parseInt(m.getCustomId()));
}
return ids;
}
private static <T> T only(List<T> list) {
assertEquals(1, list.size(), "断言前置:列表应恰好 1 个元素");
return list.get(0);
}
private static SysGroup group(Integer id, String name, Integer parentId, GroupTypeEnum type) {
SysGroup g = new SysGroup();
g.setId(id);
g.setName(name);
g.setParentId(parentId);
g.setType(type);
return g;
}
private static SysUser user(Integer id, UserTypeEnum type) {
SysUser u = new SysUser();
u.setId(id);
u.setType(type);
u.setAccount("u" + id);
u.setRealName("用户" + id);
return u;
}
private static SysUserGroupRel rel(Integer userId, Integer groupId) {
SysUserGroupRel r = new SysUserGroupRel();
r.setUserId(userId);
r.setGroupId(groupId);
r.setIsMain(1);
return r;
}
private static SysGroupAppRel appRel(Integer groupId, Integer appId) {
SysGroupAppRel r = new SysGroupAppRel();
r.setGroupId(groupId);
r.setAppId(appId);
return r;
}
private static SysGroupModulePerm perm(Integer groupId, Integer moduleId) {
SysGroupModulePerm p = new SysGroupModulePerm();
p.setGroupId(groupId);
p.setModuleId(moduleId);
p.setInheritFlag(Boolean.FALSE);
return p;
}
private static AppModule module(Integer id, String customId, Integer parentId, int level, Integer sortNo) {
AppModule m = new AppModule();
m.setId(id);
m.setAppId(APP_ID);
m.setName("模块" + customId);
m.setCustomId(customId);
m.setParentId(parentId);
m.setLevel(level);
m.setSortNo(sortNo);
return m;
}
}