(一)案例背景介绍
这里的“权限管理系统”不是依托于Shiro框架,而是依托于“ZTree”插件,纯手工打造。
在做这部分功能时,主要在整个权限系统的设计方面走了一些弯路,最后总结出来的经验就是:先将整个ZTree展示出来,然后再分别实现整个功能。最终效果图如下:
如上图所示,整个权限管理界面分为三颗树:用户树,用户组树和菜单树。基本功能就是:点击用户,出现关联的用户组,可以为该用户分配用户组;点击用户组,出现关联的菜单,可以为该用户组分配菜单。
(二)重难点
用户树和用户组树展示起来很简单,查询所有数据,结合ZTree的简单JSON数据格式,即可构造成树。而菜单树就没有看起来的那么简单了,这里我主要以菜单树为例,讲解设计的基本思路,以及注意事项。
(三)具体实现
1.设计表
这里主要有两张表:菜单表(sys_menu)和Action表(sys_action)。
这里涉及到一个问题:为什么设计成两张表,而不是一张菜单表(sys_menu)搞定所有?
关于这个问题,其实有很多原因,这里我就讲下主要原因:我们在为一个用户组分配可操作的Action节点时,我们可能只给该用户分配某个菜单项下的部分Action,从而实现相对细粒度的控制。所以我们在构建关联关系时,既要建立用户组和Menu项的关联关系(sys_group_menu表),又要建立用户组和Action项的关联关系(sys_group_action)。从前面的分析可知,Menu(菜单项)更多的是作为一系列Action的容器,如下图:
比如“内容管理”菜单项下就包括“广告管理。。。”等一系列 的Action。这光靠一张表是难以实现上述所有功能的。
(1)菜单表(sys_menu)
CREATE TABLE `sys_menu` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(50) DEFAULT NULL COMMENT '组名',
`url` varchar(200) DEFAULT NULL COMMENT '访问地址',
`parent_id` int(11) DEFAULT '0' COMMENT '父菜单ID',
`order_num` int(2) DEFAULT NULL COMMENT '排序数字',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
(2)Action表(sys_action)
CREATE TABLE `sys_action` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`menu_id` int(11) DEFAULT NULL COMMENT '菜单id',
`name` varchar(50) DEFAULT NULL COMMENT '操作名',
`url` varchar(200) DEFAULT NULL COMMENT '访问地址',
`method` varchar(50) DEFAULT NULL COMMENT '操作',
PRIMARY KEY (`id`),
KEY `fk_menu_id` (`menu_id`),
CONSTRAINT `fk_menu_id` FOREIGN KEY (`menu_id`) REFERENCES `sys_menu` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.构建树
这里又涉及到另外一个问题:怎么样在一棵树里展示两种节点,同时还要建立父子关系?
关于这个问题,我们需要从ZTree的特性出发。在利用简单JSON数据格式建立树时,我们可以设置“显示节点”以及“父节点”ID的样式:
这里我们无法再设置“
idKey : "id"
”,因为菜单表(sys_menu)和Action表(sys_action)都有id,两张表查出的id很容易出现重复,导致ZTree树的展示混乱。所以这里我们设置为“
idKey : "comboId"
”和“
pIdKey : "comboParentId"
”,combo即混合,顾名思义,将两张表的id混合起来。
具体实现就是:所有的菜单表(sys_menu)查询出来的id和parentId全部进行处理,加上“MENU_”前缀;所有的Action表(sys_action)查询出来的id全部进行处理,加上“ACTION_”前缀,查询出来的menuId(其实也就是父节点的意思,不过菜单节点的父节点依然是菜单节点,而Action的父节点是菜单节点,所以不叫parentId,而叫menuId)全部进行处理,加上“MENU_”前缀。处理过后出现的JSON数据如下:
data": [
{
"id": 1,
"name": "内容管理",
"comboId": "MENU_1",
"comboParentId": "MENU_0",
"open": true,
"isParent": "true"
},
{
"id": 1,
"name": "广告管理",
"comboId": "ACTION_1",
"comboParentId": "MENU_1",
"open": false
},
{
"id": 2,
"name": "商户管理",
"comboId": "ACTION_2",
"comboParentId": "MENU_1",
"open": false
}
]
data.data.push({
id : 0,
//这是菜单项,加上菜单前缀
comboId : common.menuPrefix.PREFIX_MENU + "0",
name : "菜单组",
open : true,
isParent : true,
nocheck:true
});