Java后端如何实现三级菜单

919 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

今天学习了尚硅谷的谷粒商城项目,其中有一个实现三级菜单的功能,我将实现思路记录下来,请大家指教。我采用的持久层框架是mybatis-plus。

1.什么是三级菜单

image.png 如上图所示就是我说的三级菜单。我们获取上图里面的数据呢?

2.三级菜单的数据库设计

我们三级菜单里面的数据都是从数据库里面获取的,所以我们需要设计一张合理的数据库表来存放这些内容。

DROP TABLE IF EXISTS `pms_category`;

CREATE TABLE `pms_category` (
  `cat_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类id',
  `name` char(50) DEFAULT NULL COMMENT '分类名称',
  `parent_cid` bigint(20) DEFAULT NULL COMMENT '父分类id',
  `cat_level` int(11) DEFAULT NULL COMMENT '层级',
  `show_status` tinyint(4) DEFAULT NULL COMMENT '是否显示[0-不显示,1显示]',
  `sort` int(11) DEFAULT NULL COMMENT '排序',
  `icon` char(255) DEFAULT NULL COMMENT '图标地址',
  `product_unit` char(50) DEFAULT NULL COMMENT '计量单位',
  `product_count` int(11) DEFAULT NULL COMMENT '商品数量',
  PRIMARY KEY (`cat_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1433 DEFAULT CHARSET=utf8mb4 COMMENT='商品三级分类';

简单介绍下这张表格。 无论数据是几级分类的数据,都是符合这张表的。我们这张表里面有个parent_cid,如果数据的parent_cid是0,代表这条数据是一级菜单里面的数据。cat_id除了是主键外,它还有另一个作用,如果另一条数据的parent_cid等于本条数据的cat_id,那么说明另一条数据是本条数据的子菜单。sort的作用就是排序。

3.实体类设计

下面的代码是分类数据库表对应的实体类。下面的实体类新添加了一个字段private List children,原因是后面我们会把查询到的数据组成一个树状结果,即一级菜单的数据包含二级菜单的数据,二级菜单的数据包含三级菜单的数据,因为无论你是几级菜单的数据,都可以用到这个实体类,所以就这样写。@TableField(exist = false) 注解加载bean属性上,表示当前属性不是数据库的字段。

/**
 * 商品三级分类
 * 
 * @author wzj
 * @email wzj@gmail.com
 * @date 2022-06-26 11:03:23
 */
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
   private static final long serialVersionUID = 1L;

   /**
    * 分类id
    */
   @TableId
   private Long catId;
   /**
    * 分类名称
    */
   private String name;
   /**
    * 父分类id
    */
   private Long parentCid;
   /**
    * 层级
    */
   private Integer catLevel;
   /**
    * 是否显示[0-不显示,1显示]
    */
   private Integer showStatus;
   /**
    * 排序
    */
   private Integer sort;
   /**
    * 图标地址
    */
   private String icon;
   /**
    * 计量单位
    */
   private String productUnit;
   /**
    * 商品数量
    */
   private Integer productCount;

   @TableField(exist = false)
   private List<CategoryEntity> children;

}

4.业务代码实现

4.1 controller层

这里的代码较为简单,其实就是说明下你要去那里查询数据

@Autowired
private CategoryService categoryService;

/**
 * 查出所有分类及子分类,以树形结构组装起来
 */
@RequestMapping("/list/tree")
public R list(){

    List<CategoryEntity> entities = categoryService.listWithTree();
    return R.ok().put("data", entities);
}

小tips:你可以直接用注入进来的categoryService去调用listWithTree(),这个方法是将数据组成树状结果的方法。问题来了,你这方法不是还没有吗?q(≧▽≦q)我们可以先这样写,接着让idea帮我们创建。

4.2 service层

经过上面的步骤,我们service层接口的代码如下:

/**
 * 商品三级分类
 *
 * @author wzj
 * @email wzj@gmail.com
 * @date 2022-06-26 11:03:23
 */
public interface CategoryService extends IService<CategoryEntity> {

    PageUtils queryPage(Map<String, Object> params);

    List<CategoryEntity> listWithTree();
}

service层接口的实现类,才是实现我们数据组装的地方

@Override
public List<CategoryEntity> listWithTree() {
    //1.查出所有分类
    List<CategoryEntity> entities = baseMapper.selectList(null);

    //2.组装成父子的树形结构
    //2.1 找到所有1级分类
    List<CategoryEntity> level1Menus = entities.stream()
            .filter(categoryEntity ->
                    categoryEntity.getParentCid() == 0)
            .map((menu) ->{
                menu.setChildren(getChildren(menu,entities));
                return menu;
            })
            .sorted((menu1,menu2) ->{
                return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
            })
            .collect(Collectors.toList());
    return level1Menus;
}

//递归查找所有菜单的子菜单
private List<CategoryEntity> getChildren(CategoryEntity root,List<CategoryEntity> all){

    List<CategoryEntity> children = all.stream()
            .filter(categoryEntity -> {
                return categoryEntity.getParentCid() == root.getCatId();
            })
            .map(categoryEntity ->{
                //1.找到子菜单
                categoryEntity.setChildren(getChildren(categoryEntity,all));
                return categoryEntity;
            })
            .sorted((menu1,menu2) ->{
                //2.菜单的排序
                return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
            })
            .collect(Collectors.toList());
    return children;
}

这里我记录下service层实现类代码的实现思路。

1.在listWithTree方法里我们先将所有数据查询出来

2.将查询出来的数据通过stream里的filter方法过滤下,过滤条件是parentCid == 0即一级菜单的数据

3.将过滤出来的数据通过stream里的map方法重新添加到流里

3.1我们在map方法里要将查询到的子菜单封装到当前菜单里面,在返回到流里
3.2所以我们在map里面采用递归的方式,不断查询当前菜单是否有子菜单。为此我们又写了一个方法getChildren来递归查询子菜单。

4.将处理后的数据通过stream里的collect方法重新生成集合并返回

有些朋友看到这里,可能会有如下问题

1.map里的setChildren方法哪里来的?还记得实体类设置的那个字段吗?在文章开头有提到 2.递归的结束条件是什么?确实,写成了stream可读性下降了不少,你可以看下filter,这个就是过滤的条件,如果没有符合过滤条件的子菜单了,那么递归到了这里就结束了。3.为什么排序要这样写menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort()?原因是sort可能是空的。