一、需求及数据库说明

在在线教育项目中,我们使用Spring Security作为安全框架,在权限管理需求中,不同角色的用户登录后台管理系统拥有不同的菜单权限与功能权限,权限管理包含三个功能模块:菜单管理、角色管理和用户管理。

在菜单管理中,要求我们使用树形结构展示菜单列表。

创建acl_permission表,省略部分数据

  • 在permission表中,pid为0的为权限菜单顶层数据
  • 根据pid = id来确定数据间的父子关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE TABLE `acl_permission` (
`id` char(19) NOT NULL DEFAULT '' COMMENT '编号',
`pid` char(19) NOT NULL DEFAULT '' COMMENT '所属上级',
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '名称',
`type` tinyint(3) NOT NULL DEFAULT '0' COMMENT '类型(1:菜单,2:按钮)',
`permission_value` varchar(50) DEFAULT NULL COMMENT '权限值',
`path` varchar(100) DEFAULT NULL COMMENT '访问路径',
`component` varchar(100) DEFAULT NULL COMMENT '组件路径',
`icon` varchar(50) DEFAULT NULL COMMENT '图标',
`status` tinyint(4) DEFAULT NULL COMMENT '状态(0:禁止,1:正常)',
`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',
`gmt_create` datetime DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_pid` (`pid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限';

#
# Data for table "acl_permission"
#

INSERT INTO `acl_permission` VALUES ('1','0','全部数据',0,NULL,NULL,NULL,NULL,NULL,0,'2019-11-15 17:13:06','2019-11-15 17:13:06'),('1195268474480156673','1','权限管理',1,NULL,'/acl','Layout',NULL,NULL,0,'2019-11-15 17:13:06','2019-11-18 13:54:25'),('1195268616021139457','1195268474480156673','用户管理',1,NULL,'user/list','/acl/user/list',NULL,NULL,0,'2019-11-15 17:13:40','2019-11-18 13:53:12'),('1195268788138598401','1195268474480156673','角色管理',1,NULL,'role/list','/acl/role/list',NULL,NULL,0,'2019-11-15 17:14:21','2019-11-15 17:14:21'),('1195268893830864898','1195268474480156673','菜单管理',1,NULL,'menu/list','/acl/menu/list',NULL,NULL,0,'2019-11-15 17:14:46','2019-11-15 17:14:46'),('1195269143060602882','1195268616021139457','查看',2,'user.list','','',NULL,NULL,0,'2019-11-15 17:15:45','2019-11-17 21:57:16'),('1195269295926206466','1195268616021139457','添加',2,'user.add','user/add','/acl/user/form',NULL,NULL,0,'2019-11-15 17:16:22','2019-11-15 17:16:22'),('1195269473479483394','1195268616021139457','修改',2,'user.update','user/update/:id','/acl/user/form',NULL,NULL,0,'2019-11-15 17:17:04','2019-11-15 17:17:04'),('1195269547269873666','1195268616021139457','删除',2,'user.remove','','',NULL,NULL,0,'2019-11-15 17:17:22','2019-11-15 17:17:22'),('1195269821262782465','1195268788138598401','修改',2,'role.update','role/update/:id','/acl/role/form',NULL,NULL,0,'2019-11-15 17:18:27','2019-11-15 17:19:53'),('1195269903542444034','1195268788138598401','查看',2,'role.list','','',NULL,NULL,0,'2019-11-15 17:18:47','2019-11-15 17:18:47');

二、使用递归查询嵌套列表

2.1、实体类

为了实现嵌套列表的查询,我们需要为实体类添加一些额外的属性

  • 添加一个Integer类型的level属性,这个属性用于表示当前permission对象在哪一级菜单
  • 添加一个List< Permission >类型的children属性,这个属性用于存放当前permission对象的子菜单
  • 由于这两个属性不属于数据库中的属性,所以我们需要添加@TableField(exist = false) 注解
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
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
@TableName("acl_permission")
@ApiModel(value="Permission对象", description="权限")
public class Permission extends BaseEntity {

private static final long serialVersionUID=1L;

@ApiModelProperty(value = "所属上级")
private String pid;

@ApiModelProperty(value = "名称")
private String name;

@ApiModelProperty(value = "类型(1:菜单,2:按钮)")
private Integer type;

@ApiModelProperty(value = "权限值")
private String permissionValue;

@ApiModelProperty(value = "访问路径")
private String path;

@ApiModelProperty(value = "组件路径")
private String component;

@ApiModelProperty(value = "图标")
private String icon;

@ApiModelProperty(value = "状态(0:禁止,1:正常)")
private Integer status;

@ApiModelProperty(value = "逻辑删除 1(true)已删除, 0(false)未删除")
@TableLogic
private Integer isDeleted;

/***
* 下面是业务中手动添加的属性
* 并非数据库中的字段
*/


@TableField(exist = false)
@ApiModelProperty("表示当前permission对象是哪一级菜单")
private Integer level;

@TableField(exist = false)
@ApiModelProperty("下级")
private List<Permission> children;

@TableField(exist = false)
@ApiModelProperty("是否被选中")
private boolean isSelect;
}

2.2、Controller层

1
2
3
4
5
6
@GetMapping
@ApiOperation("查询所有菜单")
public R indexAllPermission() {
List<Permission> list = permissionService.queryAllMenu();
return R.ok().data("children",list);
}

2.3、Service层

第一步,先查询出permission表中所有数据,根据id排序

1
2
3
4
5
6
7
8
9
10
11
12
13
/***
* 递归查询所有菜单
* @return
*/
@Override
public List<Permission> queryAllMenu() {
//1 查询菜单表中所有数据,根据id排序
QueryWrapper<Permission> wrapper = new QueryWrapper<>();
wrapper.orderByAsc("id");
List<Permission> permissionList = baseMapper.selectList(wrapper);
//2 将查询出来的菜单List集合按照要求进行封装
return buildPermission(permissionList);
}

第二步,创建一个buildPermission方法,这个方法将查询出来的permission集合转换为嵌套列表集合

  • 先查询出所有permission对象中pid为0,即最顶层的元素
  • 将该permission对象的level设置为1
  • 将这个顶层元素加入到要返回给Controller层的List集合中
  • 写一个selectChildren方法,传入当前permission和第一步查询出的所有permission集合,这个方法用于递归组装顶层元素的子元素和后代元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/***
* 封装传入的菜单集合
* @param permissionList 要封装的菜单列表
* @return 封装好的嵌套菜单列表
*/
private static List<Permission> buildPermission(List<Permission> permissionList) {
List<Permission> finalNode = new ArrayList<>();
//遍历传入的菜单列表,得到最顶层元素,即pid = 0的菜单对象,设置其level值为1
for (Permission permission : permissionList) {
if("0".equals(permission.getPid())) {
//设置顶层菜单的level值为1
permission.setLevel(1);
//根据顶层菜单,向里面进行查询子菜单,封装到finalNode
finalNode.add(selectChildren(permission,permissionList));
}
}
return finalNode;
}

第三步,写一个selectChildren方法,根据传入的permission对象和permission集合递归组装嵌套列表。

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
/***
* 真正的递归在这里进行
* @param permission
* @param permissionList
* @return
*/
private static Permission selectChildren(Permission permission, List<Permission> permissionList) {
//1 由于我们要向上一层菜单对象的children中放置对象,所以我们先初始化对象
permission.setChildren(new ArrayList<Permission>());
//2 遍历所有菜单列表,进行判断比较,比较id值与pid是否相同
permissionList.forEach(per -> {
//判断id和pid是否相同
if(permission.getId().equals(per.getPid())) {
//此时证明当前per对象是permission对象的子节点
//所以子节点的level值应该为父节点的level+1
per.setLevel(permission.getLevel() + 1);
if(CollectionUtils.isEmpty(permission.getChildren())) {
permission.setChildren(new ArrayList<>());
}
//把查询出来的当前菜单放在上一层菜单对象的children中
permission.getChildren().add(selectChildren(per,permissionList));
}
});
return permission;
}

2.4、测试

使用Swagger进行测试

  • 界面

image-20210210233136804

  • 结果

image-20210210233104615

image-20210210233608819

三、通过id递归删除嵌套菜单

需要删除自身及所有的后代元素

3.1、Controller层

1
2
3
4
5
6
@DeleteMapping("{id}")
@ApiOperation("递归删除菜单")
public R deletePermission(@PathVariable String id) {
permissionService.recurDeletePermissionById(id);
return R.ok();
}

3.2、Service层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/***
* 根据id递归删除菜单及子菜单
* 获取id为传入id的菜单及子菜单
* @param id
*/
@Override
public void recurDeletePermissionById(String id) {
//1 创建一个list集合,用于封装所有删除菜单id值
List<String> idList = new ArrayList<>();
//2 向idList中设置要删除的菜单id
this.selectPermissionChildById(id,idList);
//放入要删除的id,递归封装的只是子菜单的id
idList.add(id);
permissionMapper.deleteBatchIds(idList);
}

编写selectPermissionChildById方法,传入父级id和用于批量删除的id列表,这个方法用于查询所有待删除元素的后代元素的id

  • 使用Mybatis-Plus自带的QueryWrapper查询出pid为传入id的permission元素
  • 遍历上一步查询到的permission列表,然后以列表中每一个元素的id作为pid,进行递归查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/***
* 根据传入的菜单id,查询菜单中的所有子菜单id,然后封装到list集合中
* @param id
* @param idList
*/
private void selectPermissionChildById(String id, List<String> idList) {
QueryWrapper<Permission> wrapper = new QueryWrapper<>();
wrapper.eq("pid",id);
wrapper.select("id");
List<Permission> childIdList = permissionMapper.selectList(wrapper);

//把childIdList中的菜单id一一取出,封装到idList中,做递归查询
childIdList.forEach(item -> {
idList.add(item.getId());
//递归查询
this.selectPermissionChildById(item.getId(),idList);
});
}

3.3、测试

在数据库中插入几条测试数据

image-20210210235953698

打开swagger,我们测试删除id为2的菜单及其后代。

image-20210211000114430

查看数据库,发现测试数据已经被逻辑删除。

image-20210211000230171