业务实践系列(2):后台管理系统加载动态权限菜单

Web管理后台绝大部分都会有左侧导航菜单功能,同时使用管理后台会有多组用户,每组用户所拥有的菜单权限是存在差异的,这就涉及到用户角色权限。

系统在设计时就需要考虑用户,角色,菜单三者之间的关系。如果一个用户有多种角色,则至少涉及5张表,如果一个用户只属于一种角色,则会涉及4张表。

业务流程

  1. 用户登录,可以拿到用户信息
  2. 通过用户信息可以拿到用户的角色ID
  3. 通过角色ID可以查到用户所拥有的菜单ID
  4. 通过菜单ID可以查到菜单的信息

数据库表设计

用户表

  1. 如果需求是用户只有一种角色,则用户表中增加一个角色ID的字段(role_id),用于关联角色表。
  2. 如果需求是用户可以有多种角色,则需要建一个中间关联表,创建用户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
/**
* @Name: SysMenu
* @Desc: 系统菜单
* @User: gxing
* @Date: 2018-06-28 16:33
**/
public class SysMenu implements Serializable {
private static final long serialVersionUID = -3254583720331660709L;

private Long id; //菜单ID
private String name; //菜单名称
private Long parentId; //父菜单ID
private String url; //菜单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; //操作者ID
private List<SysMenu> subSysMenuList; //子菜单

//---------set/get方法---------------
}

SQL语句编写

SQL代码示例是基于Mybatismapper.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>

<!-- 此处使用的是一对多的映射,多的一方是自己的子菜单
把子菜单的数据封装到菜单实体类的子菜单集合属性中(subSysMenuList)
-->
<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>

<!-- 根据用户roleId获取菜单 -->
<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) {
// console.log(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>";

// console.log(p);
$('.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>

业务实践系列(2):后台管理系统加载动态权限菜单

http://blog.gxitsky.com/2018/07/03/Business-02-dynamic-load-nav-menu/

作者

光星

发布于

2018-07-03

更新于

2023-03-07

许可协议

评论