Web管理后台绝大部分都会有左侧导航菜单功能,同时使用管理后台会有多组用户,每组用户所拥有的菜单权限是存在差异的,这就涉及到用户角色权限。
系统在设计时就需要考虑用户,角色,菜单 三者之间的关系。如果一个用户有多种角色,则至少涉及5张表,如果一个用户只属于一种角色,则会涉及4张表。
业务流程
用户登录,可以拿到用户信息 。
通过用户信息可以拿到用户的角色ID 。
通过角色ID可以查到用户所拥有的菜单ID 。
通过菜单ID可以查到菜单的信息 。
数据库表设计 用户表
如果需求是用户只有一种角色,则用户表中增加一个角色ID的字段(role_id
),用于关联角色表。
如果需求是用户可以有多种角色,则需要建一个中间关联表,创建用户ID(user_id
)和角色ID(role_id
)两个字段,表示用户与角色的关联关系。
菜单表 菜单表(menu
)主要包含菜单ID, 菜单名称, 菜单URL, 父菜单ID, 状态等字段。
1 2 3 4 5 6 7 8 9 10 11 12 CREATE TABLE `tbs_menu` ( `id` bigint (20 ) unsigned NOT NULL AUTO_INCREMENT COMMENT '菜单ID' , `name` varchar (50 ) DEFAULT NULL COMMENT '菜单名称' , `parent_id` bigint (20 ) unsigned DEFAULT '0' COMMENT '上级ID' , `url` varchar (255 ) DEFAULT NULL COMMENT '菜单URL' , `state` int (2 ) unsigned DEFAULT NULL COMMENT '菜单状态' , `icon` varchar (100 ) DEFAULT NULL COMMENT '菜单图标' , `create_time` datetime DEFAULT NULL COMMENT '创建时间' , `last_update` datetime DEFAULT NULL COMMENT '最后更新时间' , `operator_id` bigint (20 ) unsigned DEFAULT NULL COMMENT '操作者ID' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 4004 DEFAULT CHARSET= utf8mb4 COMMENT= '系统导航菜单表'
角色表 角色表(role
)主要包含角色ID, 角色名称, 角色描述, 角色类型, 角色状态等字段。
1 2 3 4 5 6 7 8 9 10 11 CREATE TABLE `tbs_role` ( `id` bigint (20 ) unsigned NOT NULL AUTO_INCREMENT COMMENT '角色ID' , `role_name` varchar (50 ) DEFAULT NULL COMMENT '角色名称' , `role_desc` varchar (200 ) DEFAULT NULL COMMENT '角色描述' , `role_type` int (2 ) unsigned DEFAULT NULL COMMENT '角色类型(1-系统角色,2-业务角色)' , `state` int (2 ) unsigned DEFAULT NULL COMMENT '角色状态(0-禁用,1-启用)' , `create_time` datetime DEFAULT NULL COMMENT '创建日期' , `last_update` datetime DEFAULT NULL COMMENT '最后更新时间' , `operator_id` bigint (20 ) DEFAULT NULL COMMENT '操作者ID' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 5 DEFAULT CHARSET= utf8mb4 COMMENT= '用户角色表'
角色菜单关联表 一个用户可以有多个菜单,一个菜单也可属于多个用户,多对多的关系,需要一张中间表来映射关联关系。 系统一般都会有配置用户菜单权限的功能。
1 2 3 4 5 6 7 8 9 CREATE TABLE `tbs_role_menu` ( `id` bigint (20 ) unsigned NOT NULL AUTO_INCREMENT COMMENT '角色菜ID' , `role_id` bigint (20 ) unsigned DEFAULT NULL COMMENT '角色ID' , `menu_id` bigint (20 ) unsigned DEFAULT NULL COMMENT '菜单ID' , `create_time` datetime DEFAULT NULL COMMENT '创建时间' , `last_update` datetime DEFAULT NULL COMMENT '最后更新时间' , `operator_id` bigint (20 ) unsigned DEFAULT NULL COMMENT '操作者ID' , PRIMARY KEY (`id`) ) ENGINE= InnoDB AUTO_INCREMENT= 23 DEFAULT CHARSET= utf8mb4 COMMENT= '角色菜单关联表'
用户角色关联表 如果一个用户有多个角色,需要创建一张中间表来映射关联关系,类似与角色菜单关联表 ,字段不同。
菜单实体类 菜单实体类有个自己类型的集合属性,用于包装子菜单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class SysMenu implements Serializable { private static final long serialVersionUID = -3254583720331660709L ; private Long id; private String name; private Long parentId; private String url; private Integer state; private String icon; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime; @JsonIgnore private Date lastUpdate; @JsonIgnore private long operatorId; private List<SysMenu> subSysMenuList; }
SQL语句编写 此SQL 代码示例是基于Mybatis
的mapper.xml
文件编写的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.xxx.xxx.mapper.SysMenuMapper" > <sql id ="tbs_menu_columns" > id, name, parent_id, url, state, icon, create_time, last_update, operator_id </sql > <resultMap id ="userMenuResultMap" type ="SysMenu" > <id column ="p_id" property ="id" /> <result column ="p_name" property ="name" /> <result column ="p_url" property ="url" /> <result column ="p_icon" property ="icon" /> <collection property ="subSysMenuList" ofType ="SysMenu" > <id column ="id" property ="id" /> <result column ="name" property ="name" /> <result column ="url" property ="url" /> <result column ="icon" property ="icon" /> </collection > </resultMap > <select id ="querySysMenuByUserRoleId" parameterType ="Long" resultMap ="userMenuResultMap" > SELECT a.parent_id p_id, b.name p_name, b.url p_url, b.icon p_icon, a.id, a.name, a.url, a.icon FROM (SELECT <include refid ="tbs_menu_columns" /> FROM tbs_menu WHERE id IN (SELECT menu_id FROM tbs_role_menu WHERE role_id = #{roleId}) AND parent_id > 0 AND state = 1) a LEFT JOIN tbs_menu b ON a.parent_id = b.id ORDER BY p_id, id ASC; </select > </mapper >
说明: 先查子菜单,左连接父菜单,相当于两张表,得到子菜单字段和父菜单字段,再挑选字段分别映射到实体类中。 如果用户与角色是多对多的关系,就不是where role_id = #{roleId}
, 而是通过用户角色关联表,取出用户所有的角色ID,where role_id in (select role_id from user_role where user_id = #{userId})group by menu_id
,为排除权限重复的菜单,需要对菜单ID分组。 也以在代码层面两层循环查询,先查父菜单,再拿父菜单ID 做为子菜单的父ID 查询子菜单,将得到的子菜单封装到父菜单实体类中的子菜单属性集合中。
最终执行的SQL是:
1 2 3 4 5 6 7 8 9 10 11 SELECT a.parent_id p_id, b.name p_name, b.url p_url, b.icon p_icon, a.id, a.name, a.url, a.icon FROM ( SELECT id, name, parent_id, url, state, icon, create_time, last_update, operator_id FROM tbs_menu WHERE id IN ( SELECT menu_id FROM tbs_role_menu WHERE role_id = 3 ) AND parent_id > 0 AND state = 1 ) a LEFT JOIN tbs_menu b ON a.parent_id = b.id ORDER BY p_id, id ASC ;
最终菜单JSON格式数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 [{ "id" : 10 , "name" : "产品方管理" , "operatorId" : 0 , "subSysMenuList" : [{ "id" : 1001 , "name" : "产品方列表" , "operatorId" : 0 , "url" : "/ProductSide/list" }] },{ "id" : 35 , "name" : "订单管理" , "operatorId" : 0 , "subSysMenuList" : [{ "id" : 3501 , "name" : "订单列表" , "operatorId" : 0 , "url" : "/Order/list" }] }, { "id" : 40 , "name" : "系统管理" , "operatorId" : 0 , "subSysMenuList" : [{ "id" : 4001 , "name" : "用户管理" , "operatorId" : 0 , "url" : "/User/list" }, { "id" : 4002 , "name" : "角色管理" , "operatorId" : 0 , "url" : "/sysRole/list" }] }]
页面加载菜单 主要是使用AJAX
请求,通过拼接html
标签来实现,下面代码例子需要引入jquery.js
。 页面加载时执行初始化菜单的方法,方法里是个ajax
请求,返回的数据进行两层遍历,外层遍历父菜单,内层遍历子菜单。JSP页面也可用<c:forEach>
两层循环达到同样效果。注意 JSP内嵌非同一目录下的JSP, 可能导致内嵌JSP的 js 或 css 引用路径失效的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 <%@ page isELIgnored="false" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <script type="text/javascript" src="../dist/js/jquery.min.js" ></script> <script> $(function () { initSidebar(); }); function initSidebar () { $.ajax({ url: '/sysMenu/querySysMenuByUserRoleId' , type: 'get' , async: false , dataType: 'json' , success: function (data) { $.each(data, function (i, e) { var submenus = e.subSysMenuList; var p = "<li class='treeview'><a href='#'><i class='fa fa-pie-chart'></i><span>" + e.name + "</span><span class='pull-right-container'><i class='fa fa-angle-left pull-right'></i></span></a><ul class='treeview-menu'>" ; $.each(submenus, function (index, el) { p += "<li><a href='" + el.url + "'><i class='fa fa-circle-o'></i>" + el.name + "</a></li>" ; }); p += "</ul></li>" ; $('.sidebar-menu' ).append(p); }); } }); } </script> <!-- 左侧导航 --> <aside class ="main-sidebar" > <section class ="sidebar" > <ul class ="sidebar-menu" data-widget="tree" > <li class ="header" > 导 航</li> <!-- 下面是导航菜单的静态样式 --> <%--<li class ="treeview" > <a href="" > <i class ="fa fa-pie-chart" ></i> <span>多菜单</span> <span class ="pull-right-container" > <i class ="fa fa-angle-left pull-right" ></i> </span> </a> <ul class ="treeview-menu" > <li><a href="charts/chartjs.html" ><i class ="fa fa-circle-o" ></i>列表项1 </a></li> <li><a href="charts/morris.html" ><i class ="fa fa-circle-o" ></i>列表项2 </a></li> <li><a href="charts/flot.html" ><i class ="fa fa-circle-o" ></i>列表项3 </a></li> <li><a href="charts/inline.html" ><i class ="fa fa-circle-o" ></i>列表项4 </a></li> <li><a href="charts/inline.html" ><i class ="fa fa-circle-o" ></i>列表项5 </a></li> </ul> </li>--%> </ul> </section> </aside>