后台提供模块列表接口,这个模块是支持子层级的,所以大概结构是这样:
[
{
id: 1,
label: '默认',
children: [
{
id: 4,
label: '二级子模块1',
children: [
{
id: 9,
label: '三级子模块1'
},
{
id: 10,
label: '三级子模块2'
}
]
}
]
},
{
id: 2,
label: '一级子模块2',
children: [
{
id: 5,
label: '二级子模块 1'
},
{
id: 6,
label: '二级子模块 2'
}
]
}
]
通常来说,可以写递归代码来找出子层级的数据,然后再进行封装返回出来,比较麻烦。
后来发现 HutoolUtil 中有个工具类 TreeUtil 可以完成我需求,非常便捷,本次就使用它来实现。
官方教程:https://hutool.cn/docs/#/core/语言特性/树结构/树结构工具-TreeUtil
一、引用 HutoolUtil
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.22</version> </dependency>
二、建表
给模块建一张新表api_module:
CREATE TABLE `api_module` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id', `projectId` bigint NOT NULL COMMENT '该节点所属项目id', `name` varchar(64) COLLATE utf8mb4_general_ci NOT NULL COMMENT '节点名称', `parentId` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '父节点id', `level` int DEFAULT '1' COMMENT '节点层级', `createTime` datetime NOT NULL DEFAULT '1900-01-01 00:00:00' COMMENT '创建时间', `updateTime` datetime NOT NULL DEFAULT '1900-01-01 00:00:00' COMMENT '更新时间', `pos` double DEFAULT NULL COMMENT '节点顺序位置', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='模块表';
重要字段:
projectId:与项目进行关联parentId:该节点的父节点,一级目录的父节点我会设置为 0 。level:该节点对应层级,从 1 开始。pos:表示该节点在父节点下的位置顺序。
三、后端接口实现
1. Controller 层
新建 ApiModuleController 类,添加一个处理器方法 getNodeByProjectId,通过项目 ID 查询出下面的所有模块。
package com.pingguo.bloomtest.controller;
import com.pingguo.bloomtest.common.Result;
import com.pingguo.bloomtest.service.ApiModuleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("module")
public class ApiModuleController {
@Autowired
ApiModuleService apiModuleService;
@GetMapping("/list/{projectId}")
public Result getNodeByProjectId(@PathVariable Long projectId) {
return Result.success(apiModuleService.getNodeTreeByProjectId(projectId));
}
}
2. DAO层
dao 层自然也要有。
package com.pingguo.bloomtest.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pingguo.bloomtest.pojo.ApiModule;
import org.springframework.stereotype.Repository;
@Repository
public interface ApiModuleDAO extends BaseMapper<ApiModule> {
}
3. Service 层
实现 getNodeTreeByProjectId 方法。
public List<Tree<String>> getNodeTreeByProjectId(Long projectId) {
this.getDefaultNode(projectId);
// 根据 projectId 查询所有节点
QueryWrapper<ApiModule> wrapperApiModule = new QueryWrapper<>();
List<ApiModule> apiModules = apiModuleDAO.selectList(wrapperApiModule.eq("projectId", projectId));
// 配置
TreeNodeConfig treeNodeConfig = new TreeNodeConfig();
// 自定义属性名 ,即返回列表里对象的字段名
treeNodeConfig.setIdKey("id");
treeNodeConfig.setWeightKey("pos");
treeNodeConfig.setParentIdKey("parentId");
treeNodeConfig.setChildrenKey("children");
// 最大递归深度
// treeNodeConfig.setDeep(5);
treeNodeConfig.setNameKey("name");
//转换器
List<Tree<String>> treeNodes = TreeUtil.build(apiModules, "0", treeNodeConfig,
(treeNode, tree) -> {
tree.setId(treeNode.getId().toString());
tree.setParentId(treeNode.getParentId().toString());
tree.setWeight(treeNode.getPos());
tree.setName(treeNode.getName());
// 扩展属性 ...
tree.putExtra("projectId", treeNode.getProjectId());
tree.putExtra("level", treeNode.getLevel());
tree.putExtra("label", treeNode.getName());
tree.putExtra("createTime", treeNode.getCreateTime());
tree.putExtra("updateTime", treeNode.getUpdateTime());
});
return treeNodes;
}
这里开头有个方法 getDefaultNode,在这里面会判断当前项目下是否有默认模块,没有则添加默认模块。
private void getDefaultNode(Long projectId) {
QueryWrapper<ApiModule> wrapperApiModule = new QueryWrapper<>();
wrapperApiModule.eq("projectId", projectId)
.eq("pos", 1.0);
// 判断当前项目下是否有默认模块,没有则添加默认模块
if (apiModuleDAO.selectCount(wrapperApiModule) == 0) {
ApiModule apiModule = new ApiModule();
apiModule.setName("默认");
apiModule.setPos(1.0);
apiModule.setLevel(1);
apiModule.setParentId(0L);
apiModule.setCreateTime(new Date());
apiModule.setUpdateTime(new Date());
apiModule.setProjectId(projectId);
apiModuleDAO.insert(apiModule);
}
}
然后通过 项目id 把项目下所有的数据查询出来:

接下来使用 TreeUtil 来完成树结构处理。
首先,创建一个配置类 TreeNodeConfig 对象,在这个对象里设置属性,对应的就是返回出来的字段名。

还可以设置最大递归深度,也可以不设。我测试之后就注释掉了,先不加限制。
最后就是构建树结构 treeNodes,完成处理后返回给 controller 层。

因为我要返回的还有其他的字段,可以使用tree.putExtra来添加要返回的其他字段,比如:
tree.putExtra("projectId", treeNode.getProjectId());
第一个参数是定义的字段名称,第二个参数就是使用这个结点的 get 方法获取对应的属性值。
最后返回到上层的是List<Tree<String>>类型,可以直接塞到统一结果里去返回。

四、测试一下
1. 测试结构数据
测试一下接口,先手动网表里插入了对应结构的数据。

请求接口,传入 projectId 为 3。
{
"code": 20000,
"message": "成功",
"data": [
{
"id": "9",
"parentId": "0",
"pos": 1.0,
"name": "默认",
"projectId": 3,
"level": 1,
"label": "默认",
"createTime": "2021-09-29 10:50:00",
"updateTime": "2021-09-29 10:50:00",
"children": [
{
"id": "14",
"parentId": "9",
"pos": 1.0,
"name": "默认-2",
"projectId": 3,
"level": 2,
"label": "默认-2",
"createTime": "1900-01-01 08:00:00",
"updateTime": "1900-01-01 08:00:00"
},
{
"id": "10",
"parentId": "9",
"pos": 1.0,
"name": "默认-1",
"projectId": 3,
"level": 2,
"label": "默认-1",
"createTime": "2021-10-01 08:00:00",
"updateTime": "1900-01-01 08:00:00",
"children": [
{
"id": "11",
"parentId": "10",
"pos": 1.0,
"name": "默认-1-1",
"projectId": 3,
"level": 3,
"label": "默认-1-1",
"createTime": "1900-01-01 08:00:00",
"updateTime": "1900-01-01 08:00:00",
"children": [
{
"id": "12",
"parentId": "11",
"pos": 1.0,
"name": "默认-1-1-1",
"projectId": 3,
"level": 4,
"label": "默认-1-1-1",
"createTime": "1900-01-01 08:00:00",
"updateTime": "1900-01-01 08:00:00",
"children": [
{
"id": "13",
"parentId": "12",
"pos": 1.0,
"name": "默认-1-1-1-1",
"projectId": 3,
"level": 5,
"label": "默认-1-1-1-1",
"createTime": "1900-01-01 08:00:00",
"updateTime": "1900-01-01 08:00:00"
}
]
}
]
}
]
}
]
}
]
}
结果正确。
2. 测试新增默认
传入一个 projectId 为 4 ,localhost:8080/bloomtest/module/list/4:
{
"code": 20000,
"message": "成功",
"data": [
{
"id": "15",
"parentId": "0",
"pos": 1.0,
"name": "默认",
"projectId": 4,
"level": 1,
"label": "默认",
"createTime": "2021-10-01 12:25:54",
"updateTime": "2021-10-01 12:25:54"
}
]
}
返回正确。

落库正常。
摘自:https://www.cnblogs.com/pingguo-softwaretesting/p/15341673.html
【实际项目中的一些代码片段】
1、数据表:

2、单元测试代码:
package com.wanma;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.lang.tree.TreeUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.wanma.entity.AuthModule;
import com.wanma.framework_web.helper.JsonHelper;
import com.wanma.service.IAuthModuleService;
import com.wanma.service.ext.AuthService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import java.util.List;
@SpringBootTest
class RunTest {
@Resource
private IAuthModuleService iAuthModuleService;
@Resource
private AuthService authService;
@Test
void test_01() {
List<AuthModule> authModuleList = iAuthModuleService.getAllModuleList();
// 配置
TreeNodeConfig treeNodeConfig = new TreeNodeConfig();
// 自定义属性名,即返回列表里对象的字段名
treeNodeConfig.setIdKey("id");
treeNodeConfig.setNameKey("moduleTitle");
treeNodeConfig.setParentIdKey("parentId");
treeNodeConfig.setWeightKey("orderNum");
treeNodeConfig.setChildrenKey("children");
// 转换器
List<Tree<String>> treeList = TreeUtil.build(authModuleList, "0", treeNodeConfig,
(treeNode, tree) -> {
tree.setId(treeNode.getId().toString());
tree.setName(treeNode.getModuleTitle());
tree.setParentId(treeNode.getParentId().toString());
tree.setWeight(treeNode.getOrderNum());
// 扩展属性 ...
tree.putExtra("moduleIcon", treeNode.getModuleIcon());
tree.putExtra("moduleType", treeNode.getModuleType());
tree.putExtra("authCode", treeNode.getAuthCode());
tree.putExtra("pageUrl", treeNode.getPageUrl());
tree.putExtra("openType", treeNode.getOpenType());
});
// 输出(1):输出json字符串
String json = JsonHelper.objectToString(treeList);
System.out.println(json);
// 输出(2):递归输出Tree标题
int deep = 0;
printTree(treeList, deep);
}
/**
* 递归输出Tree
*/
public void printTree(List<Tree<String>> treeList, int deep) {
if (ObjectUtil.isEmpty(treeList)) {
return;
}
for (Tree<String> tree : treeList) {
String name = StrUtil.repeat("--", deep) + tree.getName();
System.out.println(name);
if (tree.hasChild()) {
printTree(tree.getChildren(), deep + 1);
}
}
}
}
3、输出(1):输出json字符串
[{
"id": "23",
"moduleTitle": "首页",
"parentId": "0",
"orderNum": 1,
"moduleIcon": "home-2-line",
"moduleType": 1,
"authCode": "sys:index",
"pageUrl": "/",
"openType": 1,
"children": [{
"id": "24",
"moduleTitle": "首页",
"parentId": "23",
"orderNum": 1,
"moduleIcon": "index",
"moduleType": 1,
"authCode": "sys:index:detail",
"pageUrl": "index",
"openType": 2
}
]
}, {
"id": "1",
"moduleTitle": "用户",
"parentId": "0",
"orderNum": 2,
"moduleIcon": "user-3-line",
"moduleType": 1,
"authCode": "sys:user",
"pageUrl": "/",
"openType": 1,
"children": [{
"id": "28",
"moduleTitle": "黑名单",
"parentId": "1",
"orderNum": 1,
"moduleIcon": "",
"moduleType": 1,
"authCode": "sys:user:blackList",
"pageUrl": "blackList",
"openType": 2
}, {
"id": "30",
"moduleTitle": "身份认证",
"parentId": "1",
"orderNum": 1,
"moduleIcon": "",
"moduleType": 1,
"authCode": "sys:user:Identity",
"pageUrl": "identity",
"openType": 2
}, {
"id": "31",
"moduleTitle": "学历认证",
"parentId": "1",
"orderNum": 1,
"moduleIcon": "",
"moduleType": 1,
"authCode": "sys:user:degree",
"pageUrl": "degree",
"openType": 2
}, {
"id": "2",
"moduleTitle": "用户列表 ",
"parentId": "1",
"orderNum": 1,
"moduleIcon": "user-list",
"moduleType": 1,
"authCode": "sys:user:list",
"pageUrl": "userList",
"openType": 2
}
]
}, {
"id": "3",
"moduleTitle": "礼品",
"parentId": "0",
"orderNum": 3,
"moduleIcon": "gift-line",
"moduleType": 1,
"authCode": "sys:gift",
"pageUrl": "/",
"openType": 1,
"children": [{
"id": "4",
"moduleTitle": "礼品列表",
"parentId": "3",
"orderNum": 1,
"moduleIcon": "gift-list",
"moduleType": 1,
"authCode": "sys:gift:list",
"pageUrl": "giftList",
"openType": 2
}
]
}
]
4、输出(2):递归输出Tree标题
首页 --首页 用户 --黑名单 --身份认证 --学历认证 --用户列表 礼品 --礼品列表