根据用户角色权限,渲染菜单的一个问题记录

个人博客:无奈何杨(wnhyang)

个人语雀:wnhyang

共享语雀:在线知识共享

Github:wnhyang - Overview


背景

之前一直讲过自己独立在做一个中后台管理系统,当然这个只是开始,未来会基于此开发其他项目,因为时间的原因,这项目算是搁置了一段时间,最近又重新拾取来完善。

项目链接如下

GitHub - wnhyang/okay-boot

GitHub - wnhyang/okay-vben-admin

其前端采用vben中后台开发框架,后端就是常用的Spirng Boot那一套,用户角色菜单设计也是最常用RABC的方案。

问题

如下是菜单管理查询到的菜单列表,展示为树形结构。

在给角色分配菜单权限时,使用的是一个TreeSelect的组件,该组件提供了可多选的树形结构菜单,当然这个组件本身就有很多配置项,可以自定义很多内容。

选中如上菜单时,收集的数据是如上“用户更新”、“角色新增”、“角色更新”、“角色删除”、“角色查询”这些菜单对应的id。然后通过类似于下面的方法新增或修改角色。

@Transactional(rollbackFor = Exception.class)
public Long createRole(RoleCreateVO reqVO) {validateRoleForCreateOrUpdate(null, reqVO.getName(), reqVO.getValue());RolePO role = RoleConvert.INSTANCE.convert(reqVO);roleMapper.insert(role);if (CollectionUtil.isNotEmpty(reqVO.getMenuIds())) {roleMenuMapper.insertBatch(CollectionUtils.convertList(reqVO.getMenuIds(),menuId -> new RoleMenuPO().setRoleId(role.getId()).setMenuId(menuId)));}return role.getId();
}@Transactional(rollbackFor = Exception.class)
public void updateRole(RoleUpdateVO reqVO) {// 校验是否可以更新validateRoleForUpdate(reqVO.getId());// 校验角色的唯一字段是否重复validateRoleForCreateOrUpdate(reqVO.getId(), reqVO.getName(), reqVO.getValue());// 更新到数据库RolePO role = RoleConvert.INSTANCE.convert(reqVO);roleMenuMapper.deleteByRoleId(role.getId());if (CollectionUtil.isNotEmpty(reqVO.getMenuIds())) {roleMenuMapper.insertBatch(CollectionUtils.convertList(reqVO.getMenuIds(),menuId -> new RoleMenuPO().setRoleId(role.getId()).setMenuId(menuId)));}roleMapper.updateById(role);
}

这样本身是没有问题,数据的修改和回显都是可以的。

问题是用户关联角色,角色关联菜单,如果角色关联的菜单不是顶级菜单,前端动态渲染菜单时就会有问题。如:用户A有“用户查询”权限,应该能正确显示系统管理/用户管理页面,只是只有查询权限,但是如果按上面的树形菜单收集数据并通过新增和修改角色的关联菜单后,角色关联表里只有选中的角色id和菜单id的数据,没有指定菜单父菜单的关联关系,所以这里要处理一下,不然会有问题。

方案

1、利用前端Tree组件相关方法,选中子节点时,带上其父节点,这个角色就能关联上虽有需要的菜单节点了。唯一要注意的是数据回显,因为Tree组件通常是粘性的,选中父节点的同时会选中其所有子节点,所以回显时要注意些,要么数据查回时去除一些父节点再渲染,要么利用组件的其他方法。

2、前面所有方案不变,数据存储还是只是选中菜单节点和角色的关联,只是在用户第一次登录需要根据权限渲染菜单时把菜单节点处理一下,也就是把所有菜单查到根结点后返回。

查菜单时带上父菜单直到根结点

以下仅供参考

public List<UserInfoVO.MenuVO> getLoginUserMenuTreeList(boolean removeButton) {Login loginUser = LoginUtil.getLoginUser();if (loginUser == null) {throw exception(UNAUTHORIZED);}Long id = loginUser.getId();List<MenuPO> all = menuMapper.selectList();if (LoginUtil.isAdministrator(id)) {return buildUserMenuTree(all, removeButton);}Set<Long> menuIds = convertSet(roleMenuMapper.selectListByRoleId(loginUser.getRoleIds()), RoleMenuPO::getMenuId);Set<MenuPO> menuSet = findMenusWithParentsOrChildrenByIds(all, menuIds, true, false);return buildUserMenuTree(new ArrayList<>(menuSet), removeButton);
}

查询菜单的父/子菜单

/*** 查找菜单的父/子菜单集合** @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<>();// 存储已处理过的菜单IDSet<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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/588872.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

IP地址如何修改?分享操作技巧

在互联网世界中&#xff0c;IP地址是每台计算机或网络设备的唯一标识&#xff0c;它决定了设备在网络中的位置以及与其他设备的通信方式。然而&#xff0c;有时出于特定需求&#xff0c;我们可能需要修改设备的IP地址。虎观代理将详细阐述如何修改IP地址&#xff0c;并探讨在修…

通过nvtx和Nsight Compute分析pytorch算子的耗时

通过nvtx和Nsight Compute分析pytorch算子的耗时 一.效果二.代码 本文演示了如何借助nvtx和Nsight Compute分析pytorch算子的耗时 一.效果 第一次执行,耗时很长 小规模的matmul,调度耗时远大于算子本身 大规模的matmul,对资源的利用率高小规模matmul,各层调用的耗时 二.代码…

RuntimeError: Error compiling objects for extension虚拟环境和系统环境——添加、删除、修改环境变量

前言&#xff1a;因为一个报错RuntimeError: Error compiling objects for extension 没有配置cl.exe环境变量&#xff0c;我的应用场景是需要搞定虚拟环境变量配置 RuntimeError: Error compiling objects for extension手把手带你解决&#xff08;超详细&#xff09;-CSDN博…

RJ61BT11 三菱iQ-R系列CC-Link系统主站/本地站模块

RJ61BT11 三菱iQ-R系列CC-Link系统主站/本地站模块 RJ61BT11参数说明&#xff1a;CC-Link Ver.2版本&#xff0c;主站/本地站 三菱iQ-R系列CC-Link系统主站/本地站模块RJ61BT11产品规格&#xff1a; [传送速度] 156K/625K/2.5M/5M/l0Mbps(可选) [最多连接个数(主站时)] 64个 …

Vue项目中引入外部字体文件

1、导入字体文件&#xff08; .ttf格式&#xff09; 1.下载相应的字体文件&#xff0c;或者找ui设计师要一份。一般字体文件使用 .ttf 格式的即可。 将准备好的字体文件&#xff0c;放在项目中&#xff0c;文件目录示例如下&#xff1a; 2.创建一个font.css文件用于定义这个字…

zookeeper如何管理客户端与服务端之间的链接?(zookeeper sessions)

zookeeper客户端与服务端之间的链接用zookeeper session表示。 zookeeper session有三个状态&#xff1a; CONNECTING, ASSOCIATING, CONNECTED, CONNECTEDREADONLY, CLOSED, AUTH_FAILED, NOT_CONNECTED&#xff08;start时的状态&#xff09; 1、CONNECTING 。 表明客户…

开源大模型AI代理操作系统:像Windows一样,操控AI代理

去年&#xff0c;AutoGPT的出现让我们见识到了AI代理强大的自动化能力&#xff0c;并开创了一个全新的AI代理赛道。但在子任务调度、资源分配以及AI之间协作还有不少的难题。 因此&#xff0c;罗格斯大学的研究人员开源了AIOS&#xff0c;这是一种以大模型为核心的AI代理操作系…

UE5 C++ Interface接口

一.创建接口 声明Attack() 和 Calculatehealth()虚函数 UINTERFACE(MinimalAPI) class UMyInterface : public UInterface {GENERATED_BODY() };/*** */ class PRACTICEC_API IMyInterface {GENERATED_BODY()// Add interface functions to this class. This is the class tha…

C++ 静态库与动态库的生成和使用:基于 VS Studio 生成 newmat 矩阵库的静态库与动态库

文章目录 Part.I IntroductionChap.I 预备知识Chap.II 静态库与动态库区分 Part.II 静态库的生成与使用 (newmat)Chap.I 生成静态库Chap.II 使用静态库 Part.III 动态库的生成与使用 (newmat)Chap.I 生成动态库Chap.II 使用动态库 Part.IV 文件内容Chap.I test.cpp (静态库)Cha…

探索iPhone GPU架构:了解其硬件设计与特性

摘要 了解你的显卡对于在电脑上玩现代图形要求高的游戏非常重要。本文介绍了如何轻松查看你的显卡型号以及为什么显卡在玩电脑游戏时如此关键。 引言 随着电脑游戏的发展&#xff0c;现代游戏对硬件性能的要求越来越高。十年前发布的显卡已经无法满足当前游戏的需求。因此&…

鸿蒙分布式音乐播放-如何完成播放、暂停、上一曲、下一曲功能

介绍 本示例使用fileIo获取指定音频文件&#xff0c;并通过AudioPlayer完成了音乐的播放完成了基本的音乐播放、暂停、上一曲、下一曲功能&#xff1b;并使用DeviceManager完成了分布式设备列表的显示和分布式能力完成了音乐播放状态的跨设备分享。 本示例用到了与用户进行交…

《书生·浦语大模型全链路开源开放体系》学习笔记

书生浦语大模型全链路开源开放体系-学习笔记 大模型成为发展通用人工智能的重要途径专用模型通用大模型 书生大模型开源历程InternLM2回归语言建模的本质主要亮点性能全方位提升强大的内生计算能力 从模型到应用典型流程全链条开源开放体系数据数据集获取预训练微调XTuner 评测…