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

star2017 1年前 ⋅ 286 阅读

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, 状态等字段。

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, 角色名称, 角色描述, 角色类型, 角色状态等字段。

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='用户角色表'

角色菜单关联表

一个用户可以有多个菜单,一个菜单也可属于多个用户,多对多的关系,需要一张中间表来映射关联关系。
系统一般都会有配置用户菜单权限的功能。

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='角色菜单关联表'

用户角色关联表

如果一个用户有多个角色,需要创建一张中间表来映射关联关系,类似与角色菜单关联表,字段不同。

菜单实体类

菜单实体类有个自己类型的集合属性,用于包装子菜单。

/**
 * @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文件编写的。

<?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是:

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格式数据:

[{
    "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 引用路径失效的问题。

<%@ 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>
更多内容请访问:IT源点

相关文章推荐

全部评论: 0

    我有话说: