Java小技能:多级菜单排序并返回树结构菜单列表

215 阅读4分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

引言

需求: 菜单(服务商角色配置权限管理)、文章分类、MCC类目、区域信息。

框架: SpringBoot+MybatisPlus 对数据表中的菜单进行排序并返回树形Json格式的菜单列表

实现思路:先获取全部菜单,然后再对菜单进行装配,生成树形结构。

先获取一级菜单,再递归获取子节点

{
  "data": [
    {
      "id": "1595742481192857601",
      "createTime": "2022-11-24 19:33:50",
      "createId": "1",
      "updateId": "1",
      "updateTime": "2022-11-24 19:33:50",
      "code": "1",
      "name": "string",
      "parentId": null,
      "url": "string",
      "isExpand": "0",
      "isShow": "0",
      "type": "1",
      "typeText": "菜单",
      "sortNum": "0",
      "tagsType": "PT",
      "tagsTypeText": "平台",
      "remark": "string",
      "children": []
    },
    {
      "id": "1595742537635606529",
      "createTime": "2022-11-24 19:34:03",
      "createId": "1",
      "updateId": "1",
      "updateTime": "2022-11-24 19:34:03",
      "code": "2",
      "name": "string",
      "parentId": null,
      "url": "string",
      "isExpand": "0",
      "isShow": "0",
      "type": "1",
      "typeText": "菜单",
      "sortNum": "0",
      "tagsType": "PT",
      "tagsTypeText": "平台",
      "remark": "string",
      "children": []
    },
    {
      "id": "1595742587770122242",
      "createTime": "2022-11-24 19:34:15",
      "createId": "1",
      "updateId": "1",
      "updateTime": "2022-11-24 19:34:15",
      "code": "3",
      "name": "string",
      "parentId": null,
      "url": "string",
      "isExpand": "0",
      "isShow": "0",
      "type": "1",
      "typeText": "菜单",
      "sortNum": "0",
      "tagsType": "PT",
      "tagsTypeText": "平台",
      "remark": "string",
      "children": [
        {
          "id": "1595742715377627138",
          "createTime": "2022-11-24 19:34:45",
          "createId": "1",
          "updateId": "1",
          "updateTime": "2022-11-24 19:34:45",
          "code": "4",
          "name": "string",
          "parentId": "1595742587770122242",
          "url": "string",
          "isExpand": "0",
          "isShow": "0",
          "type": "1",
          "typeText": "菜单",
          "sortNum": "0",
          "tagsType": "PT",
          "tagsTypeText": "平台",
          "remark": "string",
          "children": []
        },
        {
          "id": "1595742770520141826",
          "createTime": "2022-11-24 19:34:59",
          "createId": "1",
          "updateId": "1",
          "updateTime": "2022-11-24 19:34:59",
          "code": "5",
          "name": "string",
          "parentId": "1595742587770122242",
          "url": "string",
          "isExpand": "0",
          "isShow": "0",
          "type": "1",
          "typeText": "菜单",
          "sortNum": "0",
          "tagsType": "PT",
          "tagsTypeText": "平台",
          "remark": "string",
          "children": [
            {
              "id": "1595742832834916354",
              "createTime": "2022-11-24 19:35:13",
              "createId": "1",
              "updateId": "1",
              "updateTime": "2022-11-24 19:35:13",
              "code": "6",
              "name": "string",
              "parentId": "1595742770520141826",
              "url": "string",
              "isExpand": "0",
              "isShow": "0",
              "type": "1",
              "typeText": "菜单",
              "sortNum": "0",
              "tagsType": "PT",
              "tagsTypeText": "平台",
              "remark": "string",
              "children": []
            },
            {
              "id": "1595742854490107906",
              "createTime": "2022-11-24 19:35:19",
              "createId": "1",
              "updateId": "1",
              "updateTime": "2022-11-24 19:35:19",
              "code": "7",
              "name": "string",
              "parentId": "1595742770520141826",
              "url": "string",
              "isExpand": "0",
              "isShow": "0",
              "type": "1",
              "typeText": "菜单",
              "sortNum": "0",
              "tagsType": "PT",
              "tagsTypeText": "平台",
              "remark": "string",
              "children": []
            }
          ]
        }
      ]
    }
  ]

I 生成树形结构菜单列表

1.1 获取全部菜单

    /**
     * 先获取全部菜单,然后再对菜单进行装配,生成树形结构
     */

        List<TSysMenu> list = tSysMenuMapper.selectList(queryWrapper);

        List<SysMenuDto> listDto = getSortMenus(list);

1.2 获取一级菜单,递归获取子节点。

    @Override
    public List<SysMenuDto> getSortMenus(List<TSysMenu> sourceList) throws Exception {
        if (sourceList.size() < 1) {
            return null;
        }
        List<SysMenuDto> dtos = sourceList.stream().map(ele -> {
            SysMenuDto dto = new SysMenuDto();
            BeanUtils.copyProperties(ele, dto);
            return dto;
        }).collect(Collectors.toList());
        // 获取第一层SysMenuDto: 筛选parentId为空的   或者0的情况: item.getParentId().equals("0")
        //SortNum 0表示最前面
        return dtos.stream()
                .filter(item -> item.getParentId() == null)
                .map(item -> item.setChildren(getChild(item.getId(), dtos)))
                .sorted(Comparator.comparingInt(menu -> (menu.getSortNum() == null ? 0 : menu.getSortNum())))
                .collect(Collectors.toList());
    }

    /**
     * 递归设置节点
     *
     * @param id
     * @param allMenu
     * @return
     */
    private List<SysMenuDto> getChild(Long parentId, List<SysMenuDto> allMenu) {
        //item.getParentId().equals(id) 会出现空指针的情况
        return allMenu.stream()
                .filter(item -> {
                    if (parentId == null) {
                        return item.getParentId() == null;
                    } else {
                        return item.getParentId() != null && item.getParentId().equals(parentId);//集合filter过滤Integer数值为空问题解决方案:使用equal取代==判断。
                    }
                })
                .map(item -> item.setChildren(getChild(item.getId(), allMenu)))
                .sorted(Comparator.comparingInt(menu -> (menu.getSortNum() == null ? 0 : menu.getSortNum())))
                .collect(Collectors.toList());
    }

1.3 实体

  • 单表查询不用添加事务注解 @Transactional
  • 新生成实体时,加一下fill,时间和用户ID自动填充。
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_sys_menu")
@ApiModel(value="TSysMenu对象", description="系统菜单表")
public class SysMenu extends Model<SysMenu> {
    @ApiModelProperty(value = "主键id")
    @TableId("id")
    private Long id;
        @ApiModelProperty(value = "菜单父级id")
    @TableField("parent_id")
    private Long parentId;
        @ApiModelProperty(value = "排序")
    @TableField("sort_num")
    private Integer sortNum;
        @Override
    protected Serializable pkVal() {
        return this.id;
    }

}

1.4 Dto

@Data
@Accessors(chain = true)//链式访问
public class SysMenuDto extends SysMenu {

    @ApiModelProperty(value = "子菜单")
    private List<SysMenuDto> children;
}

II 对分类进行上移和下移排序

查询最近的兄弟节点进行交换

/*
    对分类进行排序
     */
    public TSysCollegeCategory sortCategory(Long id,Integer type) throws Exception {
        if(id ==null){
            throw CommonException.create(ServerResponse.createByError("id不存在"));
        }
        var current = tSysCollegeCategoryMapper.selectById(id);
        if(current ==null){
            throw CommonException.create(ServerResponse.createByError("分类不存在"));
        }
        LambdaQueryWrapper<TSysCollegeCategory> queryWrapper = new LambdaQueryWrapper<>();
        // 检查ParentId为空的情况。
        if(current.getParentId() == null){
            queryWrapper.isNull(TSysCollegeCategory::getParentId);
        }else{
            queryWrapper.eq(null != current.getParentId(),TSysCollegeCategory::getParentId,current.getParentId());
        }
        if(type ==1){//上移
            queryWrapper.lt(TSysCollegeCategory::getSortNum,current.getSortNum())
                    .orderByAsc(TSysCollegeCategory::getSortNum).last("limit 1");
        }else if(type ==2){//下移
            queryWrapper.gt(TSysCollegeCategory::getSortNum,current.getSortNum())
                    .orderByAsc(TSysCollegeCategory::getSortNum).last("limit 1");
        }else{
            throw CommonException.create(ServerResponse.createByError("type类型错误"));
        }
        var next = tSyCollegeCategoryMapper.selectOne(queryWrapper);
        if(next !=null)
        {
            var sort = current.getSortNum();
            current.setSortNum(next.getSortNum());
            next.setSortNum(sort);
            tSysCollegeCategoryMapper.updateById(current);
            tSysCollegeCategoryMapper.updateById(next);
        }
        return current;
    }

III 常见问题

3.1 no instance(s) of type variable(s) R exist so that void conforms to R,

实体类上标注有 @Accessors(chain = true)//链式访问

                .map(item -> item.setChildren(getChild(item.getId(), dtos)))

3.2 MybatisPlus QueryWrapper的null查询

        if(category.getParentId() == null){
        queryWrapper.isNull("parent_id");
        }else{
            queryWrapper
                    .eq("parent_id", category.getParentId());
        }

3.3 集合filter过滤Integer数值为空问题解决方案

    /**
     * @param parentId 传递的父id  用来过滤用 ,可以为空
     * @return
     * @throws Exception
     */
    private List<SysMenuDto> getChilds(Long parentId, List<SysMenuDto> sourceList) {

        List<SysMenuDto> menus = sourceList.stream()
                .filter(menu -> {
                    if (parentId == null) {
                        return menu.getParentId() == null;
                    } else {
                        return menu.getParentId() != null && menu.getParentId().equals(parentId);
                    }
                }).collect(Collectors.toList());
        //排序 0表示最前面
        return menus;
    }