package com.yunhai.userbase.service.impl; import com.yunhai.userbase.entity.SysGroup; import com.yunhai.platform.utils.exception.DException; import com.yunhai.platform.utils.model.BoaGroup; import com.yunhai.userbase.enums.GroupTypeEnum; import com.yunhai.platform.utils.enums.IEnum; import com.yunhai.userbase.event.DataSyncEvent; import com.yunhai.userbase.common.context.CallerAppContext; import com.yunhai.userbase.model.push.PushInterfaceType; import org.springframework.context.ApplicationEventPublisher; import com.yunhai.userbase.model.sysgroup.SysGroupBatchRequest; import com.yunhai.userbase.model.sysgroup.SysGroupMoveRequest; import com.yunhai.userbase.model.sysgroup.SysGroupQuery; import com.yunhai.userbase.model.sysgroup.SysGroupRequest; import com.yunhai.userbase.model.sysgroup.SysGroupSortRequest; import com.yunhai.userbase.model.sysgroup.SysGroupVo; import com.yunhai.userbase.repository.SysGroupAppRelRepository; import com.yunhai.userbase.repository.SysGroupModulePermRepository; import com.yunhai.userbase.repository.SysGroupRepository; import com.yunhai.userbase.repository.SysUserGroupRelRepository; import com.yunhai.userbase.service.ISysGroupService; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import javax.transaction.Transactional; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** * @author chenx * @date 2026/7/20 16:00 * @description 系统组服务实现:组织架构树(仅 parentId 逻辑外键,不使用 path;后续可用 MySQL8 WITH 优化递归查询)。
*/ @Service public class SysGroupService implements ISysGroupService { /** 虚拟父节点:根节点(教育云中心)的 parentId,表示「无上级」。 */ private static final int ROOT_PARENT_ID = 0; /** 开放接口/oapi 写操作无登录态时使用的审计人哨兵值(非真实用户ID),用于兜底 create_user/update_user 的 NOT NULL 约束。 */ private static final int SYSTEM_USER_ID = 0; /** * 层级类型约束:父类型 -> 允许直接创建的子类型集合。 * key 为 {@code null} 表示「根节点(教育云中心)」。 * 集合为空 = 该类型禁止创建任何子组(班级组叶子节点、学段组占位等由空集自然拒绝)。 *

规则来源:用户中心组层级约束(根-区域-学段-学校-教师|学生-年级-班级)。

*/ private static final Map> ALLOWED_CHILDREN = new HashMap<>(); static { ALLOWED_CHILDREN.put(null, EnumSet.of( GroupTypeEnum.SCHOOL_GROUP, GroupTypeEnum.NORMAL_GROUP, GroupTypeEnum.AREA_GROUP)); // 区域组:只能挂在根节点下(由根节点的白名单约束),其下可挂 学段组/学校组/普通组 ALLOWED_CHILDREN.put(GroupTypeEnum.AREA_GROUP, EnumSet.of( GroupTypeEnum.STAGE_GROUP, GroupTypeEnum.SCHOOL_GROUP, GroupTypeEnum.NORMAL_GROUP)); // 学段组(高中/初中/小学):学校组的上级,可含 学校组/年级组 ALLOWED_CHILDREN.put(GroupTypeEnum.STAGE_GROUP, EnumSet.of( GroupTypeEnum.SCHOOL_GROUP, GroupTypeEnum.GRADE_GROUP)); // 学校组:可含 教师组/学生组/普通组 ALLOWED_CHILDREN.put(GroupTypeEnum.SCHOOL_GROUP, EnumSet.of( GroupTypeEnum.TEACHER_GROUP, GroupTypeEnum.STUDENT_GROUP, GroupTypeEnum.NORMAL_GROUP)); // 教师组:维持原规范,仅含 普通组 ALLOWED_CHILDREN.put(GroupTypeEnum.TEACHER_GROUP, EnumSet.of(GroupTypeEnum.NORMAL_GROUP)); // 学生组:可含 年级组/学段组/普通组(学段组既可在区域组下、也可在学生组下) ALLOWED_CHILDREN.put(GroupTypeEnum.STUDENT_GROUP, EnumSet.of( GroupTypeEnum.GRADE_GROUP, GroupTypeEnum.STAGE_GROUP, GroupTypeEnum.NORMAL_GROUP)); // 年级组:可含 班级组/普通组 ALLOWED_CHILDREN.put(GroupTypeEnum.GRADE_GROUP, EnumSet.of( GroupTypeEnum.CLASS_GROUP, GroupTypeEnum.NORMAL_GROUP)); // 班级组:叶子节点,禁止创建任何子组(⛔ 强约束,空集自然拒绝) ALLOWED_CHILDREN.put(GroupTypeEnum.CLASS_GROUP, EnumSet.noneOf(GroupTypeEnum.class)); // 普通组:可含 学校组/教师组/学段组/普通组(年级组/班级组/学生组 受限,区域组受「只能挂根」硬约束排除) ALLOWED_CHILDREN.put(GroupTypeEnum.NORMAL_GROUP, EnumSet.of( GroupTypeEnum.SCHOOL_GROUP, GroupTypeEnum.TEACHER_GROUP, GroupTypeEnum.STAGE_GROUP, GroupTypeEnum.NORMAL_GROUP)); } /** 层级类型约束校验:parentType 为 null 表示根节点;非法组合抛 {@link DException}。 */ private void validateParentChild(GroupTypeEnum parentType, GroupTypeEnum childType) { Set allowed = ALLOWED_CHILDREN.get(parentType); if (allowed == null) { allowed = Collections.emptySet(); } if (allowed.contains(childType)) { return; } String parentName = (parentType == null) ? "根节点" : parentType.getValue(); if (childType == GroupTypeEnum.CLASS_GROUP) { throw new DException(parentName + "下不允许创建班级组(班级组为叶子节点,禁止创建子组)"); } if (childType == GroupTypeEnum.AREA_GROUP) { throw new DException("区域组只能建立在根节点下"); } throw new DException(parentName + "下不允许创建" + childType.getValue()); } @Autowired private SysGroupRepository sysGroupRepository; @Autowired private SysUserGroupRelRepository sysUserGroupRelRepository; @Autowired private SysGroupAppRelRepository sysGroupAppRelRepository; @Autowired private SysGroupModulePermRepository sysGroupModulePermRepository; @Autowired private ApplicationEventPublisher eventPublisher; // ===================== 基础增删改(含根节点保护) ===================== @Override @Transactional public BoaGroup add(SysGroupRequest request) { Integer parentId = request.getParentId(); // parentId 为 null 或 0 表示「挂在根节点(教育云中心)下」; // 新组必须挂到真实根节点的 id 之下,绝不能把 parentId 写成 0(否则会生成第二个伪根节点)。 GroupTypeEnum parentType = null; // null 表示根节点 int realPid; if (parentId != null && parentId != ROOT_PARENT_ID) { SysGroup parent = sysGroupRepository.findById(parentId) .orElseThrow(() -> new DException("父组不存在")); parentType = parent.getType(); realPid = parentId; } else { // 解析唯一真实根节点,取其真实 id 作为挂载点 realPid = resolveRootGroup().getId(); } GroupTypeEnum typeEnum = resolveType(request.getType()); // 层级类型约束:当前父组是否允许创建该子类型 validateParentChild(parentType, typeEnum); SysGroup entity = new SysGroup(); entity.setName(request.getName()); entity.setParentId(realPid); entity.setType(typeEnum); entity.setSort(nextSort(realPid)); entity.setCreateUser(SYSTEM_USER_ID); entity.setUpdateUser(SYSTEM_USER_ID); SysGroup saved = sysGroupRepository.save(entity); publishGroupEvent(PushInterfaceType.ADD_GROUP, saved.getId()); return convertToBoaGroup(saved); } @Override @Transactional public BoaGroup edit(Integer id, SysGroupRequest request) { SysGroup entity = sysGroupRepository.findById(id) .orElseThrow(() -> new DException("系统组不存在")); // 根节点不可编辑 if (isRoot(entity)) { throw new DException("根节点不可编辑"); } entity.setName(request.getName()); if (request.getType() != null) { GroupTypeEnum newType = resolveType(request.getType()); if (!Objects.equals(newType, entity.getType())) { // 1) 新类型须被当前父允许(根节点 parentId=0,parentType 视为 null) GroupTypeEnum parentType = null; if (entity.getParentId() != null && entity.getParentId() != ROOT_PARENT_ID) { SysGroup parent = sysGroupRepository.findById(entity.getParentId()).orElse(null); parentType = (parent != null) ? parent.getType() : null; } validateParentChild(parentType, newType); // 2) 新类型的允许子类型须覆盖现有直接子类型,避免把「含子组的组」改成叶子/不兼容类型 Set newAllowed = ALLOWED_CHILDREN.get(newType); if (newAllowed == null) { newAllowed = Collections.emptySet(); } List children = sysGroupRepository.findByParentIdOrderBySortAsc(entity.getId()); for (SysGroup child : children) { GroupTypeEnum ct = child.getType(); if (ct != null && !newAllowed.contains(ct)) { throw new DException("该组下已存在「" + ct.getValue() + "」,不可变更为「" + newType.getValue() + "」(" + newType.getValue() + "不允许包含" + ct.getValue() + ")"); } } } entity.setType(newType); } if (request.getSort() != null) { entity.setSort(request.getSort()); } // 注:移动请走 move 接口,edit 不改 parentId,避免与移动逻辑混淆 entity.setUpdateUser(SYSTEM_USER_ID); sysGroupRepository.save(entity); publishGroupEvent(PushInterfaceType.UPDATE_GROUP, id); return convertToBoaGroup(entity); } @Override @Transactional public void delete(Integer id) { deleteInternal(id); } @Override @Transactional public void delete(Integer id, Integer schoolId) { // 开放接口边界:id 必须属于 schoolId 子树且不能等于 schoolId(禁止删学校根组/跨校删组) assertWithinSchool(id, schoolId); deleteInternal(id); } /** * 公共学校锚点校验:schoolId 非空、存在、且必须为学校组(SCHOOL_GROUP)。
* 供本服务 delete/move 及 {@code SysUserService} 等带 schoolId 边界的开放接口统一调用。 */ @Override public void assertSchoolId(Integer schoolId) { if (schoolId == null) { throw new DException("学校ID不能为空"); } SysGroup school = sysGroupRepository.findById(schoolId) .orElseThrow(() -> new DException("学校分组不存在")); if (school.getType() != GroupTypeEnum.SCHOOL_GROUP) { throw new DException("学校ID必须为学校分组"); } } private void deleteInternal(Integer id) { SysGroup entity = sysGroupRepository.findById(id) .orElseThrow(() -> new DException("系统组不存在")); // 根节点不可删除 if (isRoot(entity)) { throw new DException("根节点不可删除"); } if (sysGroupRepository.countByParentId(id) > 0) { throw new DException("该组下存在子组,无法删除"); } if (sysUserGroupRelRepository.countByGroupId(id) > 0) { throw new DException("有用户,不能删除组"); } // AI-FIX: 删除组时级联清理其应用开通记录与模块权限记录(逻辑外键无物理 FK,避免残留孤儿数据) sysGroupAppRelRepository.deleteByGroupIdBulk(id); sysGroupModulePermRepository.deleteByGroupIdBulk(id); sysGroupRepository.deleteById(id); publishGroupEvent(PushInterfaceType.DELETE_GROUP, id); } /** * 学校边界校验:确保待删除的组属于指定学校范围,且不是学校根组本身。 * * @param groupId 待删除组ID * @param schoolId 学校组ID(限定范围) */ private void assertWithinSchool(Integer groupId, Integer schoolId) { // 公共锚点校验:schoolId 非空、存在、必须为学校组 assertSchoolId(schoolId); if (groupId == null) { throw new DException("组ID不能为空"); } if (groupId.equals(schoolId)) { throw new DException("禁止删除学校根组,请指定具体子组"); } List schoolScope = listSelfAndDescendantIds(schoolId); if (!schoolScope.contains(groupId)) { throw new DException("组不属于该学校范围,禁止跨校操作"); } } @Override public List list(SysGroupQuery query) { List list; if (StringUtils.hasText(query.getName())) { list = sysGroupRepository.findByNameContainingOrderBySortAsc(query.getName()); } else if (query.getParentId() != null) { list = sysGroupRepository.findByParentIdOrderBySortAsc(query.getParentId()); } else { list = sysGroupRepository.findAllByOrderBySortAsc(); } return list.stream().map(this::convertToVo).collect(Collectors.toList()); } // ===================== 组织架构树 ===================== @Override public List getTree() { // 根节点由 SysGroupRootInitializer 在启动时幂等创建,此处直接读取组装 List all = sysGroupRepository.findAll(); if (all.isEmpty()) { return new ArrayList<>(); } Map> childrenMap = buildSortedChildrenMap(all); // 从根(pid=0)开始组装树 List roots = childrenMap.getOrDefault(ROOT_PARENT_ID, new ArrayList<>()); return roots.stream().map(r -> buildNode(r, childrenMap)).collect(Collectors.toList()); } @Override public List getSchoolTree() { List all = sysGroupRepository.findAll(); if (all.isEmpty()) { return new ArrayList<>(); } Map> childrenMap = buildSortedChildrenMap(all); List roots = childrenMap.getOrDefault(ROOT_PARENT_ID, new ArrayList<>()); List result = new ArrayList<>(); for (SysGroup root : roots) { SysGroupVo vo = buildSchoolNode(root, childrenMap); if (vo == null) { // 根节点始终保留(即使其下没有任何学校组),children 为空 vo = convertToVo(root); } result.add(vo); } return result; } /** * 递归组装「学校组」剪枝节点。
* 学校组:直接作为叶子返回(不再向下递归,其下所有组不显示);
* 非学校组:递归剪枝后仍有子节点才保留,否则返回 null(整枝剪掉)。
*/ private SysGroupVo buildSchoolNode(SysGroup node, Map> childrenMap) { if (node.getType() == GroupTypeEnum.SCHOOL_GROUP) { return convertToVo(node); } List children = childrenMap.get(node.getId()); if (children == null) { return null; } SysGroupVo vo = convertToVo(node); for (SysGroup child : children) { SysGroupVo childVo = buildSchoolNode(child, childrenMap); if (childVo != null) { vo.getChildren().add(childVo); } } return vo.getChildren().isEmpty() ? null : vo; } /** * 构建 parentId -> 直接子节点列表 的映射(null 视为 0),同层级按 sort 升序、再按 id 升序。
*/ private Map> buildSortedChildrenMap(List all) { Map> childrenMap = new HashMap<>(); for (SysGroup g : all) { Integer key = (g.getParentId() == null) ? ROOT_PARENT_ID : g.getParentId(); childrenMap.computeIfAbsent(key, k -> new ArrayList<>()).add(g); } Comparator siblingOrder = (a, b) -> { int sa = a.getSort() == null ? 999 : a.getSort(); int sb = b.getSort() == null ? 999 : b.getSort(); if (sa != sb) { return Integer.compare(sa, sb); } return Integer.compare(a.getId(), b.getId()); }; for (List siblings : childrenMap.values()) { siblings.sort(siblingOrder); } return childrenMap; } private SysGroupVo buildNode(SysGroup node, Map> childrenMap) { SysGroupVo vo = convertToVo(node); List children = childrenMap.get(node.getId()); if (children != null) { for (SysGroup child : children) { vo.getChildren().add(buildNode(child, childrenMap)); } } return vo; } // ===================== 开放接口:组树(BoaGroup) ===================== /** * 获取整个平台的组树:从根节点(pid=0)开始,children 递归嵌套全部后代。
* 根节点唯一,取最小 id 作为树入口。表为空或无根时返回 null。 */ @Override public BoaGroup getGroups() { List all = sysGroupRepository.findAll(); if (all.isEmpty()) { return null; } Map> childrenMap = buildSortedChildrenMap(all); List roots = childrenMap.getOrDefault(ROOT_PARENT_ID, new ArrayList<>()); if (roots.isEmpty()) { return null; } // 根节点唯一,取最小 id 作为整棵树入口 SysGroup root = roots.stream() .min(Comparator.comparingInt(SysGroup::getId)) .orElse(roots.get(0)); return buildBoaNode(root, childrenMap, true); } /** * 获取单个组;withSub=true 时递归挂上全部子孙(整棵子树),false/不传时只返回节点本身(children 为 null)。
* 组不存在时返回 null。 */ @Override public BoaGroup getGroupById(Integer groupId, Boolean withSub) { if (groupId == null) { return null; } SysGroup entity = sysGroupRepository.findById(groupId).orElse(null); if (entity == null) { return null; } if (!Boolean.TRUE.equals(withSub)) { return convertToBoaGroup(entity); } List all = sysGroupRepository.findAll(); Map> childrenMap = buildSortedChildrenMap(all); return buildBoaNode(entity, childrenMap, true); } /** * 批量获取组;ids 逗号/空格分隔,按入参顺序返回,不存在的 id 跳过。
* withSub 语义同 {@link #getGroupById(Integer, Boolean)}。 */ @Override public List getGroupList(String ids, Boolean withSub) { List idList = parseIds(ids); if (idList.isEmpty()) { return new ArrayList<>(); } boolean expand = Boolean.TRUE.equals(withSub); List all = sysGroupRepository.findAll(); Map allMap = all.stream() .collect(Collectors.toMap(SysGroup::getId, g -> g, (a, b) -> a)); Map> childrenMap = buildSortedChildrenMap(all); List result = new ArrayList<>(); for (Integer id : idList) { SysGroup g = allMap.get(id); if (g == null) { continue; } result.add(expand ? buildBoaNode(g, childrenMap, true) : convertToBoaGroup(g)); } return result; } // ===================== 批量新增 ===================== @Override @Transactional public List batchAdd(SysGroupBatchRequest request) { Integer pid = request.getPid(); GroupTypeEnum parentType = null; // null 表示根节点 int realPid; if (pid != null && pid != ROOT_PARENT_ID) { SysGroup parent = sysGroupRepository.findById(pid) .orElseThrow(() -> new DException("父组不存在")); parentType = parent.getType(); realPid = pid; } else { // pid 为 null 或 0 表示「挂在根节点(教育云中心)下」:解析真实根节点 id 作为挂载点, // 绝不能写成 ROOT_PARENT_ID(0),否则批量建出的组都会变成伪根节点。 realPid = resolveRootGroup().getId(); } GroupTypeEnum typeEnum = resolveType(request.getType()); // 层级类型约束 validateParentChild(parentType, typeEnum); List items = request.getItems(); if (items == null || items.isEmpty()) { throw new DException("批量新增的子组列表不能为空"); } // 追加到当前父组子节点之后 int base = nextSort(realPid); List batch = new ArrayList<>(); for (int i = 0; i < items.size(); i++) { String name = items.get(i).getName(); if (name == null || name.trim().isEmpty()) { throw new DException("第 " + (i + 1) + " 个子组名称为空"); } SysGroup g = new SysGroup(); g.setName(name.trim()); g.setParentId(realPid); g.setType(typeEnum); g.setSort(base + i); g.setCreateUser(SYSTEM_USER_ID); g.setUpdateUser(SYSTEM_USER_ID); batch.add(g); } sysGroupRepository.saveAll(batch); publishGroupEvent(PushInterfaceType.ADD_GROUP, batch.stream().map(SysGroup::getId).collect(Collectors.toList())); return batch.stream().map(this::convertToBoaGroup).collect(Collectors.toList()); } // ===================== 子组排序 ===================== @Override @Transactional public void sortChildren(SysGroupSortRequest request) { Integer pid = request.getPid(); List ids = request.getIds(); if (pid == null || !sysGroupRepository.existsById(pid)) { throw new DException("父组不存在"); } if (ids == null || ids.isEmpty()) { throw new DException("子节点ID列表不能为空"); } if (new HashSet<>(ids).size() != ids.size()) { throw new DException("子节点ID列表存在重复"); } // 取出 pid 的直接子节点,校验 ids 全是其直接子节点 List children = sysGroupRepository.findByParentIdOrderBySortAsc(pid); Set childIdSet = children.stream().map(SysGroup::getId).collect(Collectors.toSet()); for (Integer id : ids) { if (!childIdSet.contains(id)) { throw new DException("ids 中存在不属于该父组的直接子节点"); } } // 按传入顺序落库排序(1 起) Map childMap = children.stream() .collect(Collectors.toMap(SysGroup::getId, c -> c, (a, b) -> a)); for (int i = 0; i < ids.size(); i++) { childMap.get(ids.get(i)).setSort(i + 1); } sysGroupRepository.saveAll(children); } // ===================== 移动组 ===================== @Override @Transactional public void move(SysGroupMoveRequest request) { Integer moveId = request.getMoveId(); Integer pid = request.getPid(); if (moveId == null || !sysGroupRepository.existsById(moveId)) { throw new DException("待移动节点不存在"); } if (pid == null) { throw new DException("目标父节点不存在"); } // 目标父为根节点时 pid=0;其余情况校验目标父存在并取其类型 GroupTypeEnum parentType = null; // null 表示根节点 if (pid != ROOT_PARENT_ID) { SysGroup target = sysGroupRepository.findById(pid) .orElseThrow(() -> new DException("目标父节点不存在")); parentType = target.getType(); } SysGroup moveEntity = sysGroupRepository.findById(moveId).get(); // 根节点不可移动 if (isRoot(moveEntity)) { throw new DException("根节点不可移动"); } if (Objects.equals(pid, moveId)) { throw new DException("不能移动到自身下"); } // 环检测:目标父节点不能是待移动节点的后代(含自身) Map allMap = sysGroupRepository.findAll().stream() .collect(Collectors.toMap(SysGroup::getId, g -> g, (a, b) -> a)); if (isDescendantOf(pid, moveId, allMap)) { throw new DException("不能移动到自身子节点下"); } // 层级类型约束:移动后目标父类型须允许该节点类型 validateParentChild(parentType, moveEntity.getType()); // 移动后追加到目标父节点的子节点末尾 moveEntity.setParentId(pid); moveEntity.setSort(nextSort(pid)); sysGroupRepository.save(moveEntity); publishGroupEvent(PushInterfaceType.UPDATE_GROUP, moveId); } @Override @Transactional public void move(SysGroupMoveRequest request, Integer schoolId) { // 开放接口边界:moveId/pid 必须落在 schoolId 子树内(禁止跨校/移学校根组/移出平台根) assertMoveWithinSchool(request.getMoveId(), request.getPid(), schoolId); move(request); } /** * 学校边界校验:确保待移动节点与目标父节点都属于指定学校范围,且不越界。 * * @param moveId 待移动节点ID * @param pid 目标父节点ID * @param schoolId 学校组ID(限定移动范围) */ private void assertMoveWithinSchool(Integer moveId, Integer pid, Integer schoolId) { // 公共锚点校验:schoolId 非空、存在、必须为学校组 assertSchoolId(schoolId); if (moveId == null) { throw new DException("待移动节点ID不能为空"); } if (pid == null) { throw new DException("目标父节点ID不能为空"); } if (moveId.equals(schoolId)) { throw new DException("禁止移动学校根组,请指定具体子组"); } if (pid == ROOT_PARENT_ID) { throw new DException("禁止移动到学校范围之外(根节点)"); } List schoolScope = listSelfAndDescendantIds(schoolId); if (!schoolScope.contains(moveId)) { throw new DException("待移动节点不属于该学校范围,禁止跨校移动"); } if (!schoolScope.contains(pid)) { throw new DException("目标父节点不属于该学校范围,禁止跨校移动"); } } /** * 判断 ancestorCandidate 是否为 nodeId 的后代(沿 parentId 向上回溯)。
*/ private boolean isDescendantOf(Integer ancestorCandidate, Integer nodeId, Map allMap) { Integer cursor = ancestorCandidate; while (cursor != null && cursor != ROOT_PARENT_ID) { if (Objects.equals(cursor, nodeId)) { return true; } SysGroup g = allMap.get(cursor); if (g == null) { return false; } cursor = g.getParentId(); } return false; } // ===================== 子树聚合 ===================== @Override public List listSelfAndDescendantIds(Integer groupId) { List result = new ArrayList<>(); if (groupId == null) { return result; } result.add(groupId); // parentId -> 直接子节点ID列表 Map> childrenMap = new HashMap<>(); for (SysGroup g : sysGroupRepository.findAll()) { Integer parent = (g.getParentId() == null) ? ROOT_PARENT_ID : g.getParentId(); childrenMap.computeIfAbsent(parent, k -> new ArrayList<>()).add(g.getId()); } // 广度优先向下收集所有子孙 List frontier = new ArrayList<>(); frontier.add(groupId); while (!frontier.isEmpty()) { List next = new ArrayList<>(); for (Integer id : frontier) { List children = childrenMap.get(id); if (children != null) { result.addAll(children); next.addAll(children); } } frontier = next; } return result; } // ===================== 私有工具 ===================== /** SysGroup -> BoaGroup(id/name/parentId/type/sort);children 默认 null(未展开)。 */ private BoaGroup convertToBoaGroup(SysGroup entity) { BoaGroup bg = new BoaGroup(); bg.setId(entity.getId()); bg.setName(entity.getName()); bg.setParentId(entity.getParentId()); bg.setType(entity.getType() == null ? null : entity.getType().getKey()); bg.setSort(entity.getSort()); return bg; } /** * 递归组装 BoaGroup 树节点。withSub=true 时挂上全部子孙(子树),children 为列表(无子则空列表); * withSub=false 时直接返回节点,children 保持 null(表示未展开)。 */ private BoaGroup buildBoaNode(SysGroup node, Map> childrenMap, boolean withSub) { BoaGroup bg = convertToBoaGroup(node); if (!withSub) { return bg; } List childBgs = new ArrayList<>(); List children = childrenMap.get(node.getId()); if (children != null) { for (SysGroup child : children) { childBgs.add(buildBoaNode(child, childrenMap, true)); } } bg.setChildren(childBgs); return bg; } /** 解析逗号/空格分隔的 id 串为 Integer 列表;非数字片段忽略。 */ private List parseIds(String ids) { List result = new ArrayList<>(); if (ids == null || ids.trim().isEmpty()) { return result; } for (String s : ids.split("[,\\s]+")) { if (s == null || s.trim().isEmpty()) { continue; } try { result.add(Integer.parseInt(s.trim())); } catch (NumberFormatException ignored) { // 跳过非数字片段 } } return result; } /** pid 下已有子节点的最大 sort +1;无子节点返回 1。 */ private int nextSort(Integer pid) { Integer max = sysGroupRepository.findMaxSortByParentId(pid); return (max == null ? 0 : max) + 1; } /** 校验并返回组类型枚举;非法类型抛异常。 */ private GroupTypeEnum resolveType(Integer type) { GroupTypeEnum typeEnum = IEnum.getByKey(GroupTypeEnum.class, type); if (typeEnum == null) { throw new DException("组类型不存在"); } return typeEnum; } /** 是否为系统根节点(唯一的 pid=0 节点)。 */ private boolean isRoot(SysGroup entity) { return entity.getParentId() != null && entity.getParentId() == ROOT_PARENT_ID; } /** * 解析唯一真实根节点(parentId=0 中 id 最小者,与 {@link #getGroups()} 约定一致)。
* 用于「parentId/ pid 为 0 或 null 表示挂根下」场景:必须把新组挂到该根节点的真实 id 下, * 而非把 parentId 写成 0(0 仅属于根本身,写成 0 会生成第二个伪根节点)。 * * @return 根节点实体 * @throws DException 当不存在任何根节点时 */ private SysGroup resolveRootGroup() { List roots = sysGroupRepository.findByParentIdOrderBySortAsc(ROOT_PARENT_ID); if (roots.isEmpty()) { throw new DException("根节点不存在,无法在根下建组"); } // 与 getGroups 约定一致:pid=0 中 id 最小者为唯一真实根节点 return roots.stream() .min(Comparator.comparingInt(SysGroup::getId)) .orElse(roots.get(0)); } private SysGroupVo convertToVo(SysGroup entity) { SysGroupVo vo = new SysGroupVo(); BeanUtils.copyProperties(entity, vo); vo.setType(entity.getType() == null ? null : entity.getType().getKey()); return vo; } // ===================== 推送事件(AFTER_COMMIT 由 PushDispatcher 消费,push_enabled 默认 0 不触发) ===================== private void publishGroupEvent(PushInterfaceType type, Integer groupId) { DataSyncEvent e = new DataSyncEvent(); e.setType(type); e.setGroupIds(Collections.singletonList(groupId)); e.setSourceAppKey(CallerAppContext.getAppKey()); eventPublisher.publishEvent(e); } private void publishGroupEvent(PushInterfaceType type, List groupIds) { if (groupIds == null || groupIds.isEmpty()) { return; } DataSyncEvent e = new DataSyncEvent(); e.setType(type); e.setGroupIds(groupIds); e.setSourceAppKey(CallerAppContext.getAppKey()); eventPublisher.publishEvent(e); } }