文章目录
- 一、表结构设计
- 二、菜单管理接口
- 2.1 查询菜单
- 2.2 添加菜单
- 2.3 修改菜单
- 2.4 删除菜单
- 三、分配菜单
- 3.1 查询菜单
- 3.2 保存菜单(批量插入)
- 四、动态菜单
- 五、解决bug
一、表结构设计
菜单管理就是对系统的首页中的左侧菜单进行维护。
一个用户可以担任多个角色,反之亦然,因此用户表与角色表是多对多的关系;一个角色操作多个菜单,一个菜单可以被多个角色操作,因此角色表与菜单表也是多对多的关系。为了建立用户表与角色表的联系,需要建立角色用户关系表,用来存储uid与roleid;同样,也需要建立角色菜单关系表,通过roleid与mid来建立角色表与菜单表的联系。
此次来完成菜单功能模块,因此来看看角色表具体是如何设计的:
首先是菜单表,表中含有一个重要字段parent_id
,当parent_id
值为0时,表明该菜单是第一层。
CREATE TABLE `sys_menu` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
`parent_id` bigint NOT NULL DEFAULT '0' COMMENT '所属上级',
`title` varchar(20) NOT NULL DEFAULT '' COMMENT '菜单标题',
`component` varchar(100) DEFAULT NULL COMMENT '组件名称',
`sort_value` int NOT NULL DEFAULT '1' COMMENT '排序',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(0:禁止,1:正常)',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '删除标记(0:可用 1:不可用)',
PRIMARY KEY (`id`),
KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='菜单表'
最后就是角色菜单表,用role_id
与menu_id
来建立角色表和菜单表之间的联系,字段is_half
表示菜单节点是否是半选中状态。
CREATE TABLE `sys_role_menu` (
`id` bigint NOT NULL AUTO_INCREMENT,
`role_id` bigint NOT NULL DEFAULT '0',
`menu_id` bigint NOT NULL DEFAULT '0',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`is_deleted` tinyint NOT NULL DEFAULT '0' COMMENT '删除标记(0:可用 1:不可用)',
`is_half` tinyint DEFAULT NULL, '(0:否 1:是)'
PRIMARY KEY (`id`),
KEY `idx_role_id` (`role_id`),
KEY `idx_menu_id` (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=114 DEFAULT CHARSET=utf8mb3 COMMENT='角色菜单'
二、菜单管理接口
由于菜单表结构是一种树形结构,因此在进行数据展示的时候需要按照树形表格的方式进行数据展示。 效果图如下:
2.1 查询菜单
首先来看一下实体类,这里添加了一个children字段,是为了实现下级列表
让我们来debug一遍,首先来到controller层:
接着来到业务层,为了实现功能,我首先是查询菜单表,获取所有的菜单数据,可以看到,我查询到了19个菜单数据。
最后通过MenuHelper.buildTree(sysMenuList)
调用,返回正确的数据格式给前端处理
类MenuHelper具体方法实现如下: buildTree()用来构建菜单,循环遍历所有的菜单数据,当某一个菜单的parent_id值为0时,代表着这个菜单是第一级;然后调用findChildren()来递归找其子节点。同样,findChildren()中判断是其子节点的条件是:循环遍历整个菜单数据,当当前菜单的id值与遍历的菜单的parent_id值相等。
public class MenuHelper {
/**
* 使用递归方法建菜单
* @param sysMenuList
* @return
*/
public static List<SysMenu> buildTree(List<SysMenu> sysMenuList) {
List<SysMenu> trees = new ArrayList<>();
for (SysMenu sysMenu : sysMenuList) {
//if为true即代表是第一级
if (sysMenu.getParentId() == 0) {
trees.add(findChildren(sysMenu,sysMenuList));
}
}
return trees;
}
/**
* 递归查找子节点
* @param treeNodes
* @return
*/
private static SysMenu findChildren(SysMenu sysMenu, List<SysMenu> treeNodes) {
sysMenu.setChildren(new ArrayList<SysMenu>());
for (SysMenu it : treeNodes) {
//即当前菜单的id值与所有菜单的parent_id值相等
if(sysMenu.getId().longValue() == it.getParentId().longValue()) {
sysMenu.getChildren().add(findChildren(it,treeNodes));
}
}
return sysMenu;
}
}
SQL语句编写如下:
@Select("select * from sys_menu where is_deleted = 0 order by sort_value")
List<SysMenu> selectAll();
2.2 添加菜单
需求说明:
当用户点击添加按钮的时候,弹出对话框,当用户在该表单中点击提交按钮的时候此时就需要将表单进行提交,在后端需要将提交过来的表单数据保存到数据库中即可。页面效果如下所示:
添加菜单的功能很简单,就是向菜单表中插入一条数据,添加二级节点也是如此;这里就不过多介绍了
来看看SQL语句是如何写的:
@Mapper
public interface SysMenuMapper {
@Insert("insert into sys_menu (id, parent_id, title, component, sort_value, status)\n" +
"values (#{id},#{parentId},#{title},#{component},#{sortValue},#{status})")
void save(SysMenu sysMenu);
}
2.3 修改菜单
需求说明:
当用户点击修改按钮的时候,弹出对话框,在该对话框中需要将当前行所对应的菜单数据在该表单页面进行展示。当用户在该表单中点击提交按钮的时候那么此时就需要将表单进行提交,在后端需要提交过来的表单数据修改数据库中的即可。页面效果如下所示:
这里的业务逻辑跟添加类似,不同的是SQL语句的编写,来看看SQL语句是如何编写的:
<!-- void updateById(SysMenu sysMenu);-->
<update id="updateById">
update sys_menu set
<if test="parentId != null">parent_id = #{parentId},</if>
<if test="title != null and title != ''">title = #{title},</if>
<if test="component != null and component != ''">component = #{component},</if>
<if test="sortValue != null">sort_value = #{sortValue},</if>
<if test="status != null">status = #{status},</if>
update_time = now()
where
id = #{id}
</update>
2.4 删除菜单
需求说明:
当点击删除按钮的时候此时需要弹出一个提示框,询问是否需要删除数据?如果用户点击是,那么此时向后端发送请求传递id参数,后端接收id参数进行逻辑删除。页面效果如下所示:
让我们debug一下,来到controller层,获取要删除菜单的id值
接着进入业务层,要先判断要删除的菜单是否存在子菜单,只需要从菜单表中查询,如果parent_id等于要删除菜单的id值,说明有子菜单,不能删除,抛出异常。
SQL语句编写如下:
@Select("select count(*) from sys_menu where parent_id = #{id} and is_deleted = 0")
int countByParentId(Long id);
三、分配菜单
需求说明:
在角色列表页面,当用户点击分配菜单按钮的时候,此时就会弹出一个对话框。在该对话框中会将系统中所涉及到的所有的菜单都展示出来。并且将当前角色所对应的菜单进行选中。效果如下图所示:
3.1 查询菜单
需求:根据角色的id查询出其对应的菜单id,并且需要将系统中所有的菜单数据查询出来。
让我们debug一遍,首先来到controller层,获取到前端传来的角色id值。
业务层首先查询所有菜单,这个功能前面已经实现了;接着在角色菜单表中,查询角色分配过的菜单id,放入到list集合中。
对应的SQL语句如下:
@Select("select menu_id from sys_role_menu where role_id = #{roleId} and is_deleted = 0 and is_half = 0")
List<Long> findSysRoleMenuByRoleId(Long roleId);
3.2 保存菜单(批量插入)
思路分析: 前端请求后端接口的时候需要将角色的id和用户所选中的菜单id传递到后端。后端需要先根据角色的id从sys_role_menu表中删除其所对应的菜单数据,然后添加新的菜单数据到sys_role_menu表中。
先来看看请求参数实体类
debug一遍,来到controller层,请求参数实体类接收到前端传来的参数:roleId值为44,字段menuIdList有6个map数据,分别存菜单id与菜单isHalf。
进入到业务层,首先是删除角色之前分配过的菜单数据;最后通过批量插入,保存分配菜单的数据。
业务层的两个SQL分别如下:
@Delete("delete from sys_role_menu where role_id = #{roleId}")
void deleteByRoleId(Long roleId);
<!--void doAssign(Long roleId, List<Map<String, Number>> menuIdList);-->
<insert id="doAssign">
insert into sys_role_menu (role_id, menu_id, create_time, update_time, is_deleted, is_half) values
<foreach collection="menuIdList" item="menuInfo" separator=",">
(#{roleId} , #{menuInfo.id} , now() , now() , 0 , #{menuInfo.isHalf})
</foreach>
</insert>
四、动态菜单
需求说明: 不同用户其所对应的权限是不同的,因此关于左侧菜单需要根据当前登录的用户所对应的角色动态进行获取。
系统菜单响应结果实体类长这样:
通过debug,来到controller层
在业务层,首先通过userId查询可以操作的菜单,此时我查询到了6个菜单数据。
接着通过前面的工具类,将菜单数据封装成树形数据。
最后调用buildMenus(),将树形数据转化为前端需要的数据格式。
对应的SQL语句如下,采用的是三表内连接。
@Select(" SELECT DISTINCT m.* FROM sys_menu m\n" +
" INNER JOIN sys_role_menu rm ON rm.menu_id = m.id\n" +
" INNER JOIN sys_user_role ur ON ur.role_id = rm.role_id\n" +
" WHERE ur.user_id=#{userId} and m.is_deleted = 0")
List<SysMenu> selectListByUserId(Long userId);
五、解决bug
当我为角色分配菜单,全部选中,比如系统管理全部选中;然后在系统管理下面添加新的子菜单,点击分配角色,新添加的子菜单默认也会选中,这显然是不被允许的。
解决办法: 在SysMenuServiceImpl
更改添加菜单方法,代码如下:
//添加菜单
@Override
public void save(SysMenu sysMenu) {
sysMenuMapper.save(sysMenu);
updateSysRoleMenuIsHalf(sysMenu);
}
//新添加子菜单,把父菜单isHalf设置为半开状态1
private void updateSysRoleMenuIsHalf(SysMenu sysMenu) {
//获取当前添加菜单的父菜单
SysMenu parentMenu = sysMenuMapper.selectById(sysMenu.getParentId());
if (parentMenu != null) {
//将父菜单isHalf设置为1
sysRoleMenuMapper.updateSysRoleMenuIsHalf(parentMenu.getId());
//递归调用
updateSysRoleMenuIsHalf(parentMenu);
}
}
对应的两个SQL语句分别如下:
@Select("select * from sys_menu where id = #{parentId} ")
SysMenu selectById(Long parentId);
@Update("update sys_role_menu srm set srm.is_half = 1 where menu_id = #{menuId}")
void updateSysRoleMenuIsHalf(Long id);