筛选树形菜单时关联其父节点和子节点
1.云服务器使用指南2.Sa-Token介绍与SpringBoot环境下使用3.Sa-Token组件介绍4.Sa-Token登录pre5.Sa-Token登录详解6.类支付宝积分系统设计方案(过期、兑奖)7.Sa-Token事件发布,观察者模式8.风控系统指标计算/特征提取分析与实现01,Redis、Zset、模版方法
9.筛选树形菜单时关联其父节点和子节点
10.风控系统之普通规则条件,使用LiteFlow实现11.风控系统/规则引擎,策略集/策略/规则组/规则是什么?都有哪些功能?12.身份证/手机号解析服务13.IP/GPS解析服务,ip2region,逆地理编码14.geoHelper15.风控系统之通用规则条件设计,算术单元/逻辑单元/函数式接口16.规则引擎LiteFlow发布v2.12.1版本,决策路由特性17.Elasticsearch集群运维,重平衡、分片、宕节点、扩容18.交易事件的生命周期,事前事中事后风控,结果通知/回调19.记一次IP数据处理过程,文本(CSV文件)处理,IP解析20.风控系统建设,指标策略规则流程设计,LiteFlow隐式子流程,构造EL和Chain21.LiteFlow条件组件的设计组件标签|组件参数,EL与或非表达式正反解析,元数据管理22.商业软件许可证介绍|简单原理探究23.风控系统之事件溯源,决策流程记录与版本控制24.风控系统之指标回溯,历史数据重跑25.Sa-Token的v1.39.0自定义鉴权注解怎么玩26.风控系统之规则重复触发27.基于LiteFlow的风控系统开源了!指标策略规则28.GeoHash处理经纬度,降维,空间填充曲线29.基于LiteFlow的风控系统指标版本控制30.MybatisPlus字段类型处理器TypeHandler31.规则引擎可以应用于哪些系统,用户画像、触达、风控、推荐、监控...32.LiteFlow上下文与组件设计,数据依赖梳理33.LiteFlow决策系统的策略模式,顺序、最坏、投票、权重34.Vben5登录过期无法再次登录问题,http状态码35.业务链指标,用户行为模式识别,埋点系统个人博客:无奈何杨(wnhyang)
个人语雀:wnhyang
共享语雀:在线知识共享
Github:wnhyang - Overview
树形菜单
在很多系统管理/菜单管理中经常会出现下面这样的树形菜单,它是通过前端的Tree
组件来渲染的。
后端返回的树形结构数据如下图所示。
在已有的Element
、ant-vue
等前端框架中这种组件都是有的,用起来也是非常简单。
问题
现在需求是这样的,如上图菜单搜索有两个条件,状态和菜单名称,在查询菜单时需要通过模糊匹配菜单名称和精确匹配菜单状态来查询。这个本身很简单,问题是在树形菜单搜索时需要带上父菜单和子菜单,而不是只展示匹配到的菜单。
如:搜索“用户”时应该有下面的结果,在查询到相关节点后同时带上父节点和子节点返回。
Ant Design Vue在这里前端组件是有这样的实现的,如下。
前端是有这样的实现,而且很简单,那么有没有后端的实现方案呢?
如何实现
最直接的思路就是查询满足条件的菜单,然后去查这些菜单的父菜单和子菜单,讲起来很容易,但是这里要很注意重复节点和死循环。
重复节点问题:同级菜单具有相同的父节点,在查完第一个子节点的父节点,将父节点收集后,查第二就没有必要的,可以省去。
死循环:查子节点时不要再去查其父节点的子节点和父节点,有可能会死循环。
下面贴上了所有代码仅供参考。
查询菜单列表
public List<MenuTreeRespVO> getMenuTreeList(MenuListVO reqVO) {
// 1、查询所有菜单
List<MenuPO> all = menuMapper.selectList();
// 2、查询满足条件的菜单
List<MenuPO> menus = menuMapper.selectList(reqVO);
Set<Long> menuIds = menus.stream().map(MenuPO::getId).collect(Collectors.toSet());
Set<MenuPO> menuSet = findMenusWithParentsOrChildrenByIds(all, menuIds, true, true);
// 3、形成树形结合
return buildMenuTree(new ArrayList<>(menuSet), false);
}
查询菜单的父/子菜单
/**
* 查找菜单的父/子菜单集合
*
* @param all 所有菜单
* @param menuIds 需要的菜单集合
* @param withParent 是否包含父菜单
* @param withChildren 是否包含子菜单
* @return 结果
*/
private Set<MenuPO> findMenusWithParentsOrChildrenByIds(List<MenuPO> all, Set<Long> menuIds, boolean withParent, boolean withChildren) {
Map<Long, MenuPO> menuMap = new HashMap<>();
for (MenuPO menu : all) {
menuMap.put(menu.getId(), menu);
}
// 使用LinkedHashSet保持插入顺序
Set<MenuPO> result = new LinkedHashSet<>();
// 存储已处理过的菜单ID
Set<Long> processedIds = new HashSet<>();
for (Long menuId : menuIds) {
if (withParent) {
collectMenuParents(result, menuMap, menuId, processedIds);
}
if (withChildren) {
collectMenuChildren(result, menuMap, menuId);
}
}
return result;
}
递归查找当前菜单的所有父菜单
/**
* 递归查找当前菜单的所有父菜单
*
* @param resultSet 结果
* @param menuMap menuMap
* @param menuId 需要的菜单id
* @param processedIds 存储已处理过的菜单id
*/
private void collectMenuParents(Set<MenuPO> resultSet, Map<Long, MenuPO> menuMap, Long menuId, Set<Long> processedIds) {
if (processedIds.contains(menuId)) {
return; // 如果已经处理过此菜单,则不再处理
}
processedIds.add(menuId);
MenuPO menu = menuMap.get(menuId);
if (menu != null) {
resultSet.add(menu);
// 如果当前菜单不是根节点(即parentId不为0),继续查找其父菜单
if (!Objects.equals(menu.getParentId(), ID_ROOT) && !processedIds.contains(menu.getParentId())) {
collectMenuParents(resultSet, menuMap, menu.getParentId(), processedIds);
}
}
}
递归查找当前菜单的所有子菜单
/**
* 递归查找当前菜单的所有子菜单
*
* @param resultSet 结果
* @param menuMap menuMap
* @param menuId 需要的菜单id
*/
private void collectMenuChildren(Set<MenuPO> resultSet, Map<Long, MenuPO> menuMap, Long menuId) {
MenuPO menu = menuMap.get(menuId);
if (menu != null) {
resultSet.add(menu);
// 添加当前菜单的所有子菜单
for (MenuPO child : menuMap.values()) {
if (child.getParentId().equals(menu.getId())) {
collectMenuChildren(resultSet, menuMap, child.getId());
}
}
}
}
构建菜单树
public List<MenuTreeRespVO> buildMenuTree(List<MenuPO> menuList, boolean removeButton) {
if (removeButton) {
// 移除按钮
menuList.removeIf(menu -> menu.getType().equals(MenuType.BUTTON.getType()));
}
List<MenuTreeRespVO> convert = MenuConvert.INSTANCE.convert2TreeRespList(menuList);
Map<Long, MenuTreeRespVO> menuTreeMap = new HashMap<>();
for (MenuTreeRespVO menu : convert) {
menuTreeMap.put(menu.getId(), menu);
}
menuTreeMap.values().stream().filter(menu -> !ID_ROOT.equals(menu.getParentId())).forEach(childMenu -> {
MenuTreeRespVO parentMenu = menuTreeMap.get(childMenu.getParentId());
if (parentMenu == null) {
log.info("id:{} 找不到父菜单 parentId:{}", childMenu.getId(), childMenu.getParentId());
return;
}
// 将自己添加到父节点中
if (parentMenu.getChildren() == null) {
parentMenu.setChildren(new ArrayList<>());
}
parentMenu.getChildren().add(childMenu);
}
);
return menuTreeMap.values().stream().filter(menu -> ID_ROOT.equals(menu.getParentId())).collect(Collectors.toList());
}
总结
基于以上后端查询树形菜单的代码就可以实现“筛选树形菜单时关联其父节点和子节点”了,效果还是可以的。
写在最后
拙作艰辛,字句心血,望诸君垂青,多予支持,不胜感激。
个人博客:无奈何杨(wnhyang)
个人语雀:wnhyang
共享语雀:在线知识共享
Github:wnhyang - Overview
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)