后台提供模块列表接口,这个模块是支持子层级的,所以大概结构是这样:
[ { 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标题
首页 --首页 用户 --黑名单 --身份认证 --学历认证 --用户列表 礼品 --礼品列表