一、介绍
在实际的项目开发过程中,菜单权限功能可以说是后端管理系统中必不可少的一个环节,根据业务的复杂度,设计的时候可深可浅,但无论怎么变化,设计的思路基本都是围绕着用户、角色、菜单进行相应的扩展。
今天小编就和大家一起来讨论一下,怎么设计一套可以精确到按钮级别的菜单权限功能,废话不多说,直接开撸!
二、数据库设计
先来看一下,用户、角色、菜单表对应的ER图,如下:
其中,用户和角色是多对多的关系,角色与菜单也是多对多的关系,用户通过角色来关联到菜单,当然也有的业务系统菜单权限模型,是可以直接通过用户关联到菜单,对菜单权限可以直接控制到用户级别,不过这个都不是问题,这个也可以进行扩展。
对于用户、角色表比较简单,下面,我们重点来看看菜单表的设计,如下:
可以看到,整个菜单表就是一个树型结构,关键字段说明:
- menu_code:菜单编码,用于后端权限控制
- parent_id:菜单父节点ID,方便递归遍历菜单
- node_type:节点类型,可以是文件夹、页面或者按钮类型
- link_url:页面对应的地址,如果是文件夹或者按钮类型,可以为空
- level:菜单树的层次,以便于查询指定层级的菜单
- path:树id的路径,主要用于存放从根节点到当前树的父节点的路径,逗号分隔,想要找父节点会特别快
为了后面方便开发,我们先创建一个名为menu_auth_db
的数据库,初始脚本如下:
CREATE DATABASE IF NOT EXISTS menu_auth_db default charset utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE menu_auth_db.tb_user ( id bigint(20) unsigned NOT NULL COMMENT '消息给过来的ID', mobile varchar(20) NOT NULL DEFAULT '' COMMENT '手机号', name varchar(100) NOT NULL DEFAULT '' COMMENT '姓名', password varchar(128) NOT NULL DEFAULT '' COMMENT '密码', is_delete tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除', PRIMARY KEY (id), KEY idx_name (name) USING BTREE, KEY idx_mobile (mobile) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; CREATE TABLE menu_auth_db.tb_user_role ( id bigint(20) unsigned NOT NULL COMMENT '主键', user_id bigint(20) NOT NULL COMMENT '用户ID', role_id bigint(20) NOT NULL COMMENT '角色ID', PRIMARY KEY (id), KEY idx_user_id (user_id) USING BTREE, KEY idx_role_id (role_id) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色表'; CREATE TABLE menu_auth_db.tb_role ( id bigint(20) unsigned NOT NULL COMMENT '主键', code varchar(100) NOT NULL DEFAULT '' COMMENT '编码', name varchar(100) NOT NULL DEFAULT '' COMMENT '名称', is_delete tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除', PRIMARY KEY (id), KEY idx_code (code) USING BTREE, KEY idx_name (name) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表'; CREATE TABLE menu_auth_db.tb_role_menu ( id bigint(20) unsigned NOT NULL COMMENT '主键', role_id bigint(20) NOT NULL COMMENT '角色ID', menu_id bigint(20) NOT NULL COMMENT '菜单ID', PRIMARY KEY (id), KEY idx_role_id (role_id) USING BTREE, KEY idx_menu_id (menu_id) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色菜单关系表'; CREATE TABLE menu_auth_db.tb_menu ( id bigint(20) NOT NULL COMMENT '主键', name varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名称', menu_code varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '菜单编码', parent_id bigint(20) DEFAULT NULL COMMENT '父节点', node_type tinyint(4) NOT NULL DEFAULT '1' COMMENT '节点类型,1文件夹,2页面,3按钮', icon_url varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '图标地址', sort int(11) NOT NULL DEFAULT '1' COMMENT '排序号', link_url varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '页面对应的地址', level int(11) NOT NULL DEFAULT '0' COMMENT '层次', path varchar(2500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '树id的路径 整个层次上的路径id,逗号分隔,想要找父节点特别快', is_delete tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除', PRIMARY KEY (id) USING BTREE, KEY idx_parent_id (parent_id) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单表';
三、后端开发
菜单权限模块的数据库设计,一般5张表就可以搞定,真正有点复杂的地方在于数据的写入和渲染,当然如果老板突然让你来开发一套菜单权限系统,我们也没必要慌张,下面,我们一起来看看后端应该如何开发。
3.1、创建项目
为了方便快捷,小编我采用的是springboot+mybatisPlus
组件来快速开发,直接利用mybatisPlus
官方提供的快速生成代码的demo
,一键生成所需的dao
、service
、web
层的代码,结果如下:
3.2、编写菜单添加服务
@Override public void addMenu(Menu menu) { //如果插入的当前节点为根节点,parentId指定为0 if(menu.getParentId().longValue() == 0){ menu.setLevel(1);//根节点层级为1 menu.setPath(null);//根节点路径为空 }else{ Menu parentMenu = baseMapper.selectById(menu.getParentId()); if(parentMenu == null){ throw new CommonException("未查询到对应的父节点"); } menu.setLevel(parentMenu.getLevel().intValue() + 1); if(StringUtils.isNotEmpty(parentMenu.getPath())){ menu.setPath(parentMenu.getPath() + "," + parentMenu.getId()); }else{ menu.setPath(parentMenu.getId().toString()); } } //可以使用雪花算法,生成ID menu.setId(System.currentTimeMillis()); super.save(menu); }
新增菜单比较简单,直接将数据插入即可,需要注意的地方是parent_id
、level
、path
,这三个字段的写入,如果新建的是根节点,默认parent_id
为0
,方便后续递归遍历。
3.3、编写菜单后端查询服务
- 新建一个菜单视图实体类
@Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) public class MenuVo implements Serializable { private static final long serialVersionUID = -4559267810907997111L; /** * 主键 */ private Long id; /** * 名称 */ private String name; /** * 菜单编码 */ private String menuCode; /** * 父节点 */ private Long parentId; /** * 节点类型,1文件夹,2页面,3按钮 */ private Integer nodeType; /** * 图标地址 */ private String iconUrl; /** * 排序号 */ private Integer sort; /** * 页面对应的地址 */ private String linkUrl; /** * 层次 */ private Integer level; /** * 树id的路径 整个层次上的路径id,逗号分隔,想要找父节点特别快 */ private String path; /** * 子菜单集合 */ List<MenuVo> childMenu; }
- 编写菜单查询服务,使用递归重新封装菜单视图
@Override public List<MenuVo> queryMenuTree() { Wrapper queryObj = new QueryWrapper<>().orderByAsc("level","sort"); List<Menu> allMenu = super.list(queryObj); // 0L:表示根节点的父ID List<MenuVo> resultList = transferMenuVo(allMenu, 0L); return resultList; }
/** * 封装菜单视图 * @param allMenu * @param parentId * @return */ private List<MenuVo> transferMenuVo(List<Menu> allMenu, Long parentId){ List<MenuVo> resultList = new ArrayList<>(); if(!CollectionUtils.isEmpty(allMenu)){ for (Menu source : allMenu) { if(parentId.longValue() == source.getParentId().longValue()){ MenuVo menuVo = new MenuVo(); BeanUtils.copyProperties(source, menuVo); //递归查询子菜单,并封装信息 List<MenuVo> childList = transferMenuVo(allMenu, source.getId()); if(!CollectionUtils.isEmpty(childList)){ menuVo.setChildMenu(childList); } resultList.add(menuVo); } } } return resultList; }
- 编写一个菜单树查询接口,如下:
@RestController @RequestMapping("/menu") public class MenuController { @Autowired private MenuService menuService; @PostMapping(value = "/queryMenuTree") public List<MenuVo> queryTreeMenu(){ return menuService.queryMenuTree(); } }
为了便于演示,我们先初始化7条数据,如下图:
其中最后三条是按钮类型,等下会用于后端权限控制,接口查询结果如下:
这个服务是针对后端管理界面查询的,会将所有的菜单全部查询出来以便于进行管理,展示结果类似如下图:
这个图片截图于小编正在开发的一个项目,内容可能不一致,但是数据结构基本都是一致的。