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

个人博客:无奈何杨(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.mzph.cn/news/790158.shtml

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

相关文章

Java复习第十四天学习笔记(CSS),附有道云笔记链接

【有道云笔记】十四 3.30 CSS https://note.youdao.com/s/3VormGXs 一、CSS定义和基本选择器 CSS定义&#xff1a;cascading style sheet 层叠样式表。 语法&#xff1a; 选择器 { 属性名1:属性值1; 属性名2:属性值2; 属性名3:属性值3; 属性名4:属性值4; } CSS使用&a…

实现顺序表(增、删、查、改)

引言&#xff1a;顺序表是数据结构中的一种形式&#xff0c;就是存储数据的一种结构。 这里会用到动态内存开辟&#xff0c;指针和结构体的知识 1.什么是数据结构 数据结构就是组织和存储数据的结构。 数据结构的特性&#xff1a; 物理结构&#xff1a;在内存中存储的数据是否连…

通过vite创建项目

一、VUE3官网 Vue.js - 渐进式 JavaScript 框架 | Vue.js (vuejs.org) 二、通过Vite创建项目 1、在cmd窗口下&#xff0c;全局安装vite //使用国内镜像源 npm config set registryhttps://registry.npmmirror.com//安装最新版vite npm install -g vitelatest Vite | 下一代…

Pygame基础11-mask 蒙版

蒙版 蒙版是二值化的图像&#xff0c;每个像素的值只能是0或1。 mask(蒙版)的用途&#xff1a; 碰撞检测部分着色 案例 和字母的碰撞检测 当玩家碰到字母 α \alpha α时&#xff0c;改变玩家颜色为绿色&#xff0c;否则为红色。 注意&#xff1a;我们希望碰到字母 α \alp…

考研数学1800还是660还是880?

24考完&#xff0c;大家都发现&#xff0c;没有一本习题册&#xff0c;覆盖了考试的所有知识点。 主流的模拟卷&#xff0c;都没有达到24卷的难度。 这就意味着&#xff1a; 一本习题册不够了&#xff01; 刷主流模拟卷不够了&#xff01; 这会需要整个考研复习的安排&…

C++(set和map详解,包含常用函数的分析)

set set是关联性容器 set的底层是在极端情况下都不会退化成单只的红黑树,也就是平衡树,本质是二叉搜索树. set的性质:set的key是不允许被修改的 使用set需要包含头文件 set<int> s;s.insert(1);s.insert(1);s.insert(1);s.insert(1);s.insert(2);s.insert(56);s.inser…

制造业工厂怎么通过MES系统来升级改造车间管理

在当今高度竞争的市场环境下&#xff0c;制造业企业需要不断提高生产效率&#xff0c;以在激烈的竞争中立于不败之地。而一种被广泛应用的方法就是利用MES控制系统&#xff0c;通过数字化管理和自动化控制来改造生产车间提升生产效率。 1、MES管理系统能够实现对生产过程的全面…

Navicat Premium 16 Mac/win---数据库设计、管理与维护轻松掌握数据库管理精髓

Navicat Premium是一款功能强大的数据库开发工具&#xff0c;支持多种数据库系统&#xff0c;如MySQL、Redis、MariaDB、Oracle等&#xff0c;并可与云数据库兼容&#xff0c;如Amazon RDS、Microsoft Azure等。它提供了直观易用的用户界面&#xff0c;使得开发者能够轻松上手并…

k8s calico由IPIP模式切换为BGP模式

按照官网calico.yaml部署后&#xff0c;默认是IPIP模式 查看route -n &#xff0c; 看到是tunl0口进行转发 怎么切换到BGP模式呢&#xff1f; kubectl edit ippool 将ipipMode由Always修改为Never &#xff0c;修改后保存文件即可。无需做任何操作&#xff0c;自动就切换为BG…

MySql实战--普通索引和唯一索引,应该怎么选择

在前面的基础篇文章中&#xff0c;我给你介绍过索引的基本概念&#xff0c;相信你已经了解了唯一索引和普通索引的区别。今天我们就继续来谈谈&#xff0c;在不同的业务场景下&#xff0c;应该选择普通索引&#xff0c;还是唯一索引&#xff1f; 假设你在维护一个市民系统&…

stm32cubeMX_io输入输出讲解

1创建项目&#xff08;可在专栏里找到&#xff09; 2进入当前页面点击引脚将弹出下图选项选择输入输出 带点击GPIO 点击引脚弹出如下选项根据需求选择 如有需要可以使用外部时钟&#xff1b;设置如图使用外部时钟 生成代码 将会弹出一个提示点击中间项//打开项目

HarmonyOS NEXT应用开发之MVVM模式

应用通过状态去渲染更新UI是程序设计中相对复杂&#xff0c;但又十分重要的&#xff0c;往往决定了应用程序的性能。程序的状态数据通常包含了数组、对象&#xff0c;或者是嵌套对象组合而成。在这些情况下&#xff0c;ArkUI采取MVVM Model View ViewModel模式&#xff0c;其…

clickhouse 源码编译部署

clickhouse 源码编译部署 版本 21.7.9.7 点击build project&#xff0c;编译工程&#xff0c;经过一定时间&#xff08;第一次编译可能几个小时&#xff0c;后续再编译&#xff0c;只编译有改动的文件&#xff09;生成release目录 在cmake-build-release → programs目录下…

vivado eFUSE 寄存器访问和编程

eFUSE 寄存器访问和编程 注释 &#xff1a; 在 MPSoC 和 Versal 器件上不支持以下 eFUSE 访问和编程方法。 7 系列、 UltraScale 和 UltraScale 器件具有一次性可编程位用于执行特定功能 &#xff0c; 称为 eFUSE 位。不同 eFUSE 位类型如 下所述&#xff1a; • …

单例(Singleton)设计模式

2.1 设计模式概述 设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模式免去我们自己再思考和摸索。就像是经典的棋谱&#xff0c;不同的棋局&#xff0c;我们用不同的棋谱。"套路" 经典的设计模式共有23种。每个…

语音识别:基于HMM

HMM语音识别的解码过程 从麦克风采集的输入音频波形被转换为固定尺寸的一组声学向量&#xff1a; 其中是维的语音特征向量&#xff08;例如MFCC&#xff09;。 解码器尝试去找到上述特征向量序列对应的单词&#xff08;word&#xff09;的序列&#xff1a; 单词序列的长度是。…

【大数据存储】实验4 NoSQL数据库

实验4 NoSQL数据库 NoSQL数据库的安装和使用实验环境&#xff1a; Ubuntu 22.04.3 Jdk 1.8.0_341 Hadoop 3.2.3 Hbase 2.4.17 Redis 6.0.6 mongdb 6.0.12 mogosh 2.1.0 Redis 安装redis完成 新建终端启动redisredis-server新建一个终端redis-cli 建表操作 尝…

超越传统时序!多模态+时间序列8个创新方案,刷新SOTA

传统时间序列无法有效捕捉数据中复杂的非线性关系&#xff0c;导致在处理具有复杂动力学特性的系统时效果不佳。为解决此问题&#xff0c;研究者提出了多模态时间序列。 在预测任务中&#xff0c;多模态时间序列能够整合来自不同类型数据源的信息&#xff0c;从而提供更全面的洞…

笔记: JavaSE day15 笔记

第十五天课堂笔记 数组 可变长参数★★★ 方法 : 返回值类型 方法名(参数类型 参数名 , 参数类型 … 可变长参数名){}方法体 : 变长参数 相当于一个数组一个数组最多只能有一个可变长参数, 并放到列表的最后parameter : 方法参数 数组相关算法★★ 冒泡排序 由小到大: 从前…

JavaScript(六)---【回调、异步、promise、Async】

零.前言 JavaScript(一)---【js的两种导入方式、全局作用域、函数作用域、块作用域】-CSDN博客 JavaScript(二)---【js数组、js对象、this指针】-CSDN博客 JavaScript(三)---【this指针&#xff0c;函数定义、Call、Apply、函数绑定、闭包】-CSDN博客 JavaScript(四)---【执…