提示:只是介绍后端树形结构的功能开发


前言

在管理系统开发中,我们都会涉及到菜单页面展示功能,如下图开源项目若依页面。本篇文章就是记录如何构造树形结构功能

在这里插入图片描述


一、若依项目菜单表与实体类

(一)表结构

要实现树形结构,表设计必须加上一个 parent_id来表示级联关系

CREATE TABLE `sys_menu` (
  `menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `menu_name` varchar(50) NOT NULL COMMENT '菜单名称',
  `parent_id` bigint(20) DEFAULT '0' COMMENT '父菜单ID',
  `order_num` int(4) DEFAULT '0' COMMENT '显示顺序',
  `path` varchar(200) DEFAULT '' COMMENT '路由地址',
  `component` varchar(255) DEFAULT NULL COMMENT '组件路径',
  `query` varchar(255) DEFAULT NULL COMMENT '路由参数',
  `is_frame` int(1) DEFAULT '1' COMMENT '是否为外链(0是 1否)',
  `is_cache` int(1) DEFAULT '0' COMMENT '是否缓存(0缓存 1不缓存)',
  `menu_type` char(1) DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
  `visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
  `status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
  `perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
  `create_by` varchar(64) DEFAULT '' COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) DEFAULT '' COMMENT '更新者',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) DEFAULT '' COMMENT '备注',
  PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1061 DEFAULT CHARSET=utf8mb4 COMMENT='菜单权限表';

(一)实体类

实体类的子菜单表示形式 private List children = new ArrayList()

package com.ruoyi.system.domain;

import java.util.ArrayList;
import java.util.List;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.core.web.domain.BaseEntity;

/**
 * 菜单权限表 sys_menu
 * 
 * @author ruoyi
 */
public class SysMenu extends BaseEntity
{
    private static final long serialVersionUID = 1L;

    /** 菜单ID */
    private Long menuId;

    /** 菜单名称 */
    private String menuName;

    /** 父菜单名称 */
    private String parentName;

    /** 父菜单ID */
    private Long parentId;

    /** 显示顺序 */
    private Integer orderNum;

    /** 路由地址 */
    private String path;

    /** 组件路径 */
    private String component;

    /** 路由参数 */
    private String query;

    /** 是否为外链(0是 1否) */
    private String isFrame;

    /** 是否缓存(0缓存 1不缓存) */
    private String isCache;

    /** 类型(M目录 C菜单 F按钮) */
    private String menuType;

    /** 显示状态(0显示 1隐藏) */
    private String visible;
    
    /** 菜单状态(0正常 1停用) */
    private String status;

    /** 权限字符串 */
    private String perms;

    /** 菜单图标 */
    private String icon;

    /** 子菜单 */
    private List<SysMenu> children = new ArrayList<SysMenu>();

    public Long getMenuId()
    {
        return menuId;
    }

    public void setMenuId(Long menuId)
    {
        this.menuId = menuId;
    }

    @NotBlank(message = "菜单名称不能为空")
    @Size(min = 0, max = 50, message = "菜单名称长度不能超过50个字符")
    public String getMenuName()
    {
        return menuName;
    }

    public void setMenuName(String menuName)
    {
        this.menuName = menuName;
    }

    public String getParentName()
    {
        return parentName;
    }

    public void setParentName(String parentName)
    {
        this.parentName = parentName;
    }

    public Long getParentId()
    {
        return parentId;
    }

    public void setParentId(Long parentId)
    {
        this.parentId = parentId;
    }

    @NotNull(message = "显示顺序不能为空")
    public Integer getOrderNum()
    {
        return orderNum;
    }

    public void setOrderNum(Integer orderNum)
    {
        this.orderNum = orderNum;
    }

    @Size(min = 0, max = 200, message = "路由地址不能超过200个字符")
    public String getPath()
    {
        return path;
    }

    public void setPath(String path)
    {
        this.path = path;
    }

    @Size(min = 0, max = 200, message = "组件路径不能超过255个字符")
    public String getComponent()
    {
        return component;
    }

    public void setComponent(String component)
    {
        this.component = component;
    }

    public String getQuery()
    {
        return query;
    }

    public void setQuery(String query)
    {
        this.query = query;
    }

    public String getIsFrame()
    {
        return isFrame;
    }

    public void setIsFrame(String isFrame)
    {
        this.isFrame = isFrame;
    }

    public String getIsCache()
    {
        return isCache;
    }

    public void setIsCache(String isCache)
    {
        this.isCache = isCache;
    }

    @NotBlank(message = "菜单类型不能为空")
    public String getMenuType()
    {
        return menuType;
    }

    public void setMenuType(String menuType)
    {
        this.menuType = menuType;
    }

    public String getVisible()
    {
        return visible;
    }

    public void setVisible(String visible)
    {
        this.visible = visible;
    }

    public String getStatus()
    {
        return status;
    }

    public void setStatus(String status)
    {
        this.status = status;
    }

    @Size(min = 0, max = 100, message = "权限标识长度不能超过100个字符")
    public String getPerms()
    {
        return perms;
    }

    public void setPerms(String perms)
    {
        this.perms = perms;
    }

    public String getIcon()
    {
        return icon;
    }

    public void setIcon(String icon)
    {
        this.icon = icon;
    }

    public List<SysMenu> getChildren()
    {
        return children;
    }

    public void setChildren(List<SysMenu> children)
    {
        this.children = children;
    }
    
    @Override
    public String toString() {
        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
            .append("menuId", getMenuId())
            .append("menuName", getMenuName())
            .append("parentId", getParentId())
            .append("orderNum", getOrderNum())
            .append("path", getPath())
            .append("component", getComponent())
            .append("isFrame", getIsFrame())
            .append("IsCache", getIsCache())
            .append("menuType", getMenuType())
            .append("visible", getVisible())
            .append("status ", getStatus())
            .append("perms", getPerms())
            .append("icon", getIcon())
            .append("createBy", getCreateBy())
            .append("createTime", getCreateTime())
            .append("updateBy", getUpdateBy())
            .append("updateTime", getUpdateTime())
            .append("remark", getRemark())
            .toString();
    }
}

Entity基类中 private Map<String, Object> params 额外为每个实体构建封装参数的 map 对象

/**
 * Entity基类
 * 
 * @author ruoyi
 */
public class BaseEntity implements Serializable
{
    private static final long serialVersionUID = 1L;

    /** 搜索值 */
    @JsonIgnore
    private String searchValue;

    /** 创建者 */
    private String createBy;

    /** 创建时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;

    /** 更新者 */
    private String updateBy;

    /** 更新时间 */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;

    /** 备注 */
    private String remark;

    /** 请求参数 */
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private Map<String, Object> params;

    public String getSearchValue()
    {
        return searchValue;
    }

    public void setSearchValue(String searchValue)
    {
        this.searchValue = searchValue;
    }

    public String getCreateBy()
    {
        return createBy;
    }

    public void setCreateBy(String createBy)
    {
        this.createBy = createBy;
    }

    public Date getCreateTime()
    {
        return createTime;
    }

    public void setCreateTime(Date createTime)
    {
        this.createTime = createTime;
    }

    public String getUpdateBy()
    {
        return updateBy;
    }

    public void setUpdateBy(String updateBy)
    {
        this.updateBy = updateBy;
    }

    public Date getUpdateTime()
    {
        return updateTime;
    }

    public void setUpdateTime(Date updateTime)
    {
        this.updateTime = updateTime;
    }

    public String getRemark()
    {
        return remark;
    }

    public void setRemark(String remark)
    {
        this.remark = remark;
    }

    public Map<String, Object> getParams()
    {
        if (params == null)
        {
            params = new HashMap<>();
        }
        return params;
    }

    public void setParams(Map<String, Object> params)
    {
        this.params = params;
    }
}

二、若依项目菜单功能

(一)controller

获取当前请求的 userId ,通过 userId 来查询用户所拥有的全部菜单权限(用户-角色-菜单)

@GetMapping("/treeselect")
public AjaxResult treeselect(SysMenu menu)
  {
      Long userId = SecurityUtils.getUserId();
      List<SysMenu> menus = menuService.selectMenuList(menu, userId);
      return success(menuService.buildMenuTreeSelect(menus));
  }

(二)service

通过 userId 查询菜单

/**
	* 根据用户查询系统菜单列表
	* 
	* @param menu 菜单信息
	* @param userId 用户ID
	* @return 菜单列表
 */
public List<SysMenu> selectMenuList(SysMenu menu, Long userId);

将查询菜单构建成树形结构

/**
 * 构建前端所需要下拉树结构
  * 
  * @param menus 菜单列表
  * @return 下拉树结构列表
  */
 public List<TreeSelect> buildMenuTreeSelect(List<SysMenu> menus);

(三)service.impl

判断是否是管理员用户,如果是管理员用户,查询全部的菜单。不是管理员就根据 userId 查询

/**
 * 查询系统菜单列表
  * 
  * @param menu 菜单信息
  * @return 菜单列表
 */
 @Override
 public List<SysMenu> selectMenuList(SysMenu menu, Long userId)
 {
     List<SysMenu> menuList = null;
     // 管理员显示所有菜单信息
     if (SysUser.isAdmin(userId))
     {
         menuList = menuMapper.selectMenuList(menu);
     }
     else
     {
         menu.getParams().put("userId", userId);# 将userId 添加到 SysMenu menu 参数中 注意实体中的SysMenu
         menuList = menuMapper.selectMenuListByUserId(menu);
     }
     return menuList;
 }

构建树形结构的具体实现方法 -递归算法。注意TreeSelect实体类,最终返回的是 TreeSelect 的 List<>对象

/**
 * 构建前端所需要下拉树结构
  * 
  * @param menus 菜单列表
  * @return 下拉树结构列表
  */
 @Override
 public List<TreeSelect> buildMenuTreeSelect(List<SysMenu> menus)
 {
     List<SysMenu> menuTrees = buildMenuTree(menus);
     return menuTrees.stream().map(TreeSelect::new).collect(Collectors.toList());
 }

 /**
* 构建前端所需要树结构
 * 
 * @param menus 菜单列表
 * @return 树结构列表
 */
@Override
public List<SysMenu> buildMenuTree(List<SysMenu> menus)
{
    List<SysMenu> returnList = new ArrayList<SysMenu>();
    List<Long> tempList = menus.stream().map(SysMenu::getMenuId).collect(Collectors.toList());
    for (Iterator<SysMenu> iterator = menus.iterator(); iterator.hasNext();)
    {
        SysMenu menu = (SysMenu) iterator.next();
        // 如果是顶级节点, 遍历该父节点的所有子节点
        if (!tempList.contains(menu.getParentId()))
        {
            recursionFn(menus, menu);
            returnList.add(menu);
        }
    }
    if (returnList.isEmpty())
    {
        returnList = menus;
    }
    return returnList;
}

/**
 * 递归列表
  * 
  * @param list
  * @param t
  */
 private void recursionFn(List<SysMenu> list, SysMenu t)
 {
     // 得到子节点列表
     List<SysMenu> childList = getChildList(list, t);
     t.setChildren(childList);
     for (SysMenu tChild : childList)
     {
         if (hasChild(list, tChild))
         {
             recursionFn(list, tChild);
         }
     }
 }

/**
* 得到子节点列表
  */
 private List<SysMenu> getChildList(List<SysMenu> list, SysMenu t)
 {
     List<SysMenu> tlist = new ArrayList<SysMenu>();
     Iterator<SysMenu> it = list.iterator();
     while (it.hasNext())
     {
         SysMenu n = (SysMenu) it.next();
         if (n.getParentId().longValue() == t.getMenuId().longValue())
         {
             tlist.add(n);
         }
     }
     return tlist;
 }

/**
 * 判断是否有子节点
 */
private boolean hasChild(List<SysMenu> list, SysMenu t)
{
    return getChildList(list, t).size() > 0;
}


(四)mapper

/**
 * 根据用户查询系统菜单列表
  * 
  * @param menu 菜单信息
  * @return 菜单列表
  */
 public List<SysMenu> selectMenuListByUserId(SysMenu menu);

(五)mapper.xml

<select id="selectMenuListByUserId" parameterType="SysMenu" resultMap="SysMenuResult">
		select distinct m.menu_id, m.parent_id, m.menu_name, m.path, m.component, m.`query`, m.visible, m.status, ifnull(m.perms,'') as perms, m.is_frame, m.is_cache, m.menu_type, m.icon, m.order_num, m.create_time
		from sys_menu m
		left join sys_role_menu rm on m.menu_id = rm.menu_id
		left join sys_user_role ur on rm.role_id = ur.role_id
		left join sys_role ro on ur.role_id = ro.role_id
		where ur.user_id = #{params.userId}
		<if test="menuName != null and menuName != ''">
            AND m.menu_name like concat('%', #{menuName}, '%')
		</if>
		<if test="visible != null and visible != ''">
            AND m.visible = #{visible}
		</if>
		<if test="status != null and status != ''">
            AND m.status = #{status}
		</if>
		order by m.parent_id, m.order_num
	</select>

(六)TreeSelect实体类

重点理解无参构造函数中的 子节点树形设置 this.children = menu.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList());

/**
 * Treeselect树结构实体类
 * 
 * @author ruoyi
 */
public class TreeSelect implements Serializable
{
    private static final long serialVersionUID = 1L;

    /** 节点ID */
    private Long id;

    /** 节点名称 */
    private String label;

    /** 子节点 */
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private List<TreeSelect> children;

    public TreeSelect()
    {

    }

    public TreeSelect(SysDept dept)
    {
        this.id = dept.getDeptId();
        this.label = dept.getDeptName();
        this.children = dept.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList());
    }

    public TreeSelect(SysMenu menu)
    {
        this.id = menu.getMenuId();
        this.label = menu.getMenuName();
        this.children = menu.getChildren().stream().map(TreeSelect::new).collect(Collectors.toList());
    }

    public Long getId()
    {
        return id;
    }

    public void setId(Long id)
    {
        this.id = id;
    }

    public String getLabel()
    {
        return label;
    }

    public void setLabel(String label)
    {
        this.label = label;
    }

    public List<TreeSelect> getChildren()
    {
        return children;
    }

    public void setChildren(List<TreeSelect> children)
    {
        this.children = children;
    }
}

三、使用Hutool实现树形结构

(一)引入依赖包

<dependency>
   <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.7.20</version>
</dependency>

(二)controller

@GetMapping("/queryMenuTreeByHuTool")
public Result queryMenuTreeByHuTool() {
     return Result.success(sysMenuService.queryMenuTreeByHuTool());
 }

(三)service

List<Tree<String>> queryMenuTreeByHuTool();

(四)service.impl

获取到所有的菜单列表后,通过HuTool工具构建树形结构

 @Override
public List<Tree<String>> queryMenuTreeByHuTool() {
     //获取所有菜单列表
     List<SysMenu> sysMenuList = baseMapper.selectList(new QueryWrapper<>());

     // hutool工具构建树形结构对象
     TreeNodeConfig treeNodeConfig = new TreeNodeConfig();

     treeNodeConfig.setWeightKey("orderNum"); //权重排序字段
     treeNodeConfig.setIdKey("menuId");      //数结构主键ID
     treeNodeConfig.setNameKey("menuName");   // 数据名称
     treeNodeConfig.setParentIdKey("parentId"); // 父节点Key
     treeNodeConfig.setChildrenKey("children");  //子节点

     List<Tree<String>> treeNodes = TreeUtil.build(sysMenuList,"0",treeNodeConfig,(treeNode, tree) ->{
         tree.setId(treeNode.getMenuId().toString());
         tree.setParentId(treeNode.getParentId().toString());
         tree.setWeight(treeNode.getOrderNum());
         tree.setName(treeNode.getMenuName());
         //扩展属性
         tree.putExtra("path",treeNode.getPath());
         tree.putExtra("component",treeNode.getComponent());
     });

     return treeNodes;
 }

(五)以下SQL是关于菜单分配效果回显问题

此SQL与构建树形结构无关

 SELECT m.menu_id, m.parent_id
    FROM sys_menu m
    WHERE 1 =1
    AND EXISTS (
            SELECT 0 FROM sys_template_menu stm WHERE stm.menu_id = m.menu_id AND stm.template_id = #{templateId,jdbcType=VARCHAR}
			)
    AND m.menu_id NOT IN
            (SELECT m.parent_id FROM sys_menu m INNER JOIN  sys_template_menu stm ON m.menu_id = stm.menu_id AND stm.template_id = #{templateId})
    ORDER BY
    m.parent_id,
    m.order_num

总结

无论是若依框架的树形结构构造,还是使用Hutool工具包,首先是获取所有菜单列表,然后将List列表转换成树形结构,然后构造成TreeSelect类型

Logo

快速构建 Web 应用程序

更多推荐