From 90c11f60f9ba313acbfd76de66f3b4022bc8b270 Mon Sep 17 00:00:00 2001 From: lishuyan <1206770390@qq.com> Date: Sun, 20 Jul 2025 12:57:09 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(core)=EF=BC=9A=E2=9C=A8=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20=E6=89=A9=E5=B1=95=20hutool=20TreeUtil=20=E5=B0=81?= =?UTF-8?q?=E8=A3=85=E6=A0=91=E6=9E=84=E5=BB=BA=E7=9A=84=20TreeBuildUtils?= =?UTF-8?q?=20=E5=B7=A5=E5=85=B7=E7=B1=BB=EF=BC=8C=E5=85=B6=E4=B8=AD?= =?UTF-8?q?=E5=8C=85=E6=8B=AC=E6=89=A9=E5=B1=95=E7=9A=84=EF=BC=88=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E6=A0=91=E5=BD=A2=E7=BB=93=E6=9E=84=E3=80=81=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E5=A4=9A=E6=A0=B9=E8=8A=82=E7=82=B9=E7=9A=84=E6=A0=91?= =?UTF-8?q?=E7=BB=93=E6=9E=84=EF=BC=88=E6=94=AF=E6=8C=81=E5=A4=9A=E4=B8=AA?= =?UTF-8?q?=E9=A1=B6=E7=BA=A7=E8=8A=82=E7=82=B9=EF=BC=89=EF=BC=89=E7=AD=89?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../starter/core/util/TreeBuildUtils.java | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 continew-starter-core/src/main/java/top/continew/starter/core/util/TreeBuildUtils.java diff --git a/continew-starter-core/src/main/java/top/continew/starter/core/util/TreeBuildUtils.java b/continew-starter-core/src/main/java/top/continew/starter/core/util/TreeBuildUtils.java new file mode 100644 index 00000000..67a10a46 --- /dev/null +++ b/continew-starter-core/src/main/java/top/continew/starter/core/util/TreeBuildUtils.java @@ -0,0 +1,161 @@ +package top.continew.starter.core.util; + +import cn.hutool.core.collection.CollUtil; +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.lang.tree.parser.NodeParser; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 扩展 hutool TreeUtil 封装树构建 + * + * @author Lion Li + * @author lishuyan + */ +public class TreeBuildUtils extends TreeUtil { + + private TreeBuildUtils() { + } + + /** + * 构建树形结构 + * + * @param 输入节点的类型 + * @param 节点ID的类型 + * @param list 节点列表,其中包含了要构建树形结构的所有节点 + * @param nodeParser 解析器,用于将输入节点转换为树节点 + * @return 构建好的树形结构列表 + */ + public static List> build(List list, NodeParser nodeParser) { + if (CollUtil.isEmpty(list)) { + return CollUtil.newArrayList(); + } + K k = ReflectUtil.invoke(list.get(0), CharSequenceUtil.genGetter("parentId")); + return TreeUtil.build(list, k, TreeNodeConfig.DEFAULT_CONFIG, nodeParser); + } + + /** + * 构建树形结构 + * + * @param 输入节点的类型 + * @param 节点ID的类型 + * @param parentId 顶级节点 + * @param list 节点列表,其中包含了要构建树形结构的所有节点 + * @param nodeParser 解析器,用于将输入节点转换为树节点 + * @return 构建好的树形结构列表 + */ + public static List> build(List list, K parentId, NodeParser nodeParser) { + if (CollUtil.isEmpty(list)) { + return CollUtil.newArrayList(); + } + return TreeUtil.build(list, parentId, TreeNodeConfig.DEFAULT_CONFIG, nodeParser); + } + + /** + * 构建树形结构 + * + * @param 输入节点的类型 + * @param 节点ID的类型 + * @param list 节点列表,其中包含了要构建树形结构的所有节点 + * @param parentId 顶级节点 + * @param treeNodeConfig 树节点配置 + * @param nodeParser 解析器,用于将输入节点转换为树节点 + * @return 构建好的树形结构列表 + */ + public static List> build(List list, K parentId, TreeNodeConfig treeNodeConfig, NodeParser nodeParser) { + if (CollUtil.isEmpty(list)) { + return CollUtil.newArrayList(); + } + return TreeUtil.build(list, parentId, treeNodeConfig, nodeParser); + } + + /** + * 构建多根节点的树结构(支持多个顶级节点) + * + * @param list 原始数据列表 + * @param getId 获取节点 ID 的方法引用,例如:node -> node.getId() + * @param getParentId 获取节点父级 ID 的方法引用,例如:node -> node.getParentId() + * @param parser 树节点属性映射器,用于将原始节点 T 转为 Tree 节点 + * @param 原始数据类型(如实体类、DTO 等) + * @param 节点 ID 类型(如 Long、String) + * @return 构建完成的树形结构(可能包含多个顶级根节点) + */ + public static List> buildMultiRoot(List list, Function getId, Function getParentId, NodeParser parser) { + if (CollUtil.isEmpty(list)) { + return CollUtil.newArrayList(); + } + Set rootParentIds = CollUtils.mapToSet(list, getParentId); + rootParentIds.removeAll(CollUtils.mapToSet(list, getId)); + // 构建每一个根 parentId 下的树,并合并成最终结果列表 + return rootParentIds.stream() + .flatMap(rootParentId -> TreeUtil.build(list, rootParentId, parser).stream()) + .collect(Collectors.toList()); + } + + /** + * 构建多根节点的树结构(支持多个顶级节点) + * + * @param 原始数据类型(如实体类、DTO 等) + * @param 节点 ID 类型(如 Long、String) + * @param list 原始数据列表 + * @param getId 获取节点 ID 的方法引用,例如:node -> node.getId() + * @param getParentId 获取节点父级 ID 的方法引用,例如:node -> node.getParentId() + * @param treeNodeConfig 树节点配置 + * @param parser 树节点属性映射器,用于将原始节点 T 转为 Tree 节点 + * @return 构建完成的树形结构(可能包含多个顶级根节点) + */ + public static List> buildMultiRoot(List list, Function getId, Function getParentId, + TreeNodeConfig treeNodeConfig, NodeParser parser) { + if (CollUtil.isEmpty(list)) { + return CollUtil.newArrayList(); + } + Set rootParentIds = CollUtils.mapToSet(list, getParentId); + rootParentIds.removeAll(CollUtils.mapToSet(list, getId)); + // 构建每一个根 parentId 下的树,并合并成最终结果列表 + return rootParentIds.stream() + .flatMap(rootParentId -> TreeUtil.build(list, rootParentId, treeNodeConfig, parser).stream()) + .collect(Collectors.toList()); + } + + /** + * 获取节点列表中所有节点的叶子节点 + * + * @param 节点ID的类型 + * @param nodes 节点列表 + * @return 包含所有叶子节点的列表 + */ + public static List> getLeafNodes(List> nodes) { + if (CollUtil.isEmpty(nodes)) { + return CollUtil.newArrayList(); + } + return nodes.stream() + .flatMap(TreeBuildUtils::extractLeafNodes) + .collect(Collectors.toList()); + } + + /** + * 获取指定节点下的所有叶子节点 + * + * @param 节点ID的类型 + * @param node 要查找叶子节点的根节点 + * @return 包含所有叶子节点的列表 + */ + private static Stream> extractLeafNodes(Tree node) { + if (!node.hasChild()) { + return Stream.of(node); + } else { + // 递归调用,获取所有子节点的叶子节点 + return node.getChildren().stream() + .flatMap(TreeBuildUtils::extractLeafNodes); + } + } + +} From 586322a180f2bce9faf1acbacb65ec09df921815 Mon Sep 17 00:00:00 2001 From: lishuyan <1206770390@qq.com> Date: Sun, 20 Jul 2025 14:11:05 +0800 Subject: [PATCH 2/2] =?UTF-8?q?refactor(crud)=EF=BC=9A=E2=99=BB=EF=B8=8F?= =?UTF-8?q?=20=E9=87=8D=E6=9E=84=E6=A0=91=E5=BD=A2=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E6=9E=84=E5=BB=BA=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=BA=86=E4=BD=BF=E7=94=A8=E6=9F=A5=E8=AF=A2=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E7=BC=BA=E5=A4=B1=E6=A0=B9=E8=8A=82=E7=82=B9=E6=97=B6?= =?UTF-8?q?=EF=BC=8C=E6=A0=91=E8=8A=82=E7=82=B9=E4=B8=8D=E4=B8=A2=E5=A4=B1?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除了冗余的代码和不必要的注释 - 使用三元运算符简化了 treeNodeConfig 的赋值 - 新增 createMethodReference 方法,通过反射创建方法引用 - 使用 TreeBuildUtils.buildMultiRoot 方法替代 TreeUtil.build,支持多根节点树的构建 --- .../extension/crud/service/CrudService.java | 4 -- .../crud/service/CrudServiceImpl.java | 52 +++++++++++++------ 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/continew-starter-extension/continew-starter-extension-crud/continew-starter-extension-crud-core/src/main/java/top/continew/starter/extension/crud/service/CrudService.java b/continew-starter-extension/continew-starter-extension-crud/continew-starter-extension-crud-core/src/main/java/top/continew/starter/extension/crud/service/CrudService.java index 4616c9af..b42dbed5 100644 --- a/continew-starter-extension/continew-starter-extension-crud/continew-starter-extension-crud-core/src/main/java/top/continew/starter/extension/crud/service/CrudService.java +++ b/continew-starter-extension/continew-starter-extension-crud/continew-starter-extension-crud-core/src/main/java/top/continew/starter/extension/crud/service/CrudService.java @@ -61,10 +61,6 @@ public interface CrudService { /** * 查询树列表 - *

- * 虽然提供了查询条件,但不建议使用,容易因缺失根节点导致树节点丢失。 - * 建议在前端进行查询过滤,如需使用建议重写方法。 - *

* * @param query 查询条件 * @param sortQuery 排序查询条件 diff --git a/continew-starter-extension/continew-starter-extension-crud/continew-starter-extension-crud-mp/src/main/java/top/continew/starter/extension/crud/service/CrudServiceImpl.java b/continew-starter-extension/continew-starter-extension-crud/continew-starter-extension-crud-mp/src/main/java/top/continew/starter/extension/crud/service/CrudServiceImpl.java index d88783bd..ae2309cd 100644 --- a/continew-starter-extension/continew-starter-extension-crud/continew-starter-extension-crud-mp/src/main/java/top/continew/starter/extension/crud/service/CrudServiceImpl.java +++ b/continew-starter-extension/continew-starter-extension-crud/continew-starter-extension-crud-mp/src/main/java/top/continew/starter/extension/crud/service/CrudServiceImpl.java @@ -22,7 +22,6 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.convert.Convert; 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.map.MapUtil; import cn.hutool.core.text.CharSequenceUtil; import cn.hutool.core.util.ReflectUtil; @@ -36,9 +35,13 @@ import org.springframework.transaction.annotation.Transactional; import top.continew.starter.core.constant.StringConstants; import top.continew.starter.core.util.ClassUtils; import top.continew.starter.core.util.ReflectUtils; +import top.continew.starter.core.util.TreeBuildUtils; +import top.continew.starter.core.util.validation.CheckUtils; +import top.continew.starter.core.util.validation.ValidationUtils; import top.continew.starter.data.mapper.BaseMapper; import top.continew.starter.data.service.impl.ServiceImpl; import top.continew.starter.data.util.QueryWrapperHelper; +import top.continew.starter.excel.util.ExcelUtils; import top.continew.starter.extension.crud.annotation.DictModel; import top.continew.starter.extension.crud.annotation.TreeField; import top.continew.starter.extension.crud.autoconfigure.CrudProperties; @@ -48,12 +51,13 @@ import top.continew.starter.extension.crud.model.query.PageQuery; import top.continew.starter.extension.crud.model.query.SortQuery; import top.continew.starter.extension.crud.model.resp.LabelValueResp; import top.continew.starter.extension.crud.model.resp.PageResp; -import top.continew.starter.excel.util.ExcelUtils; -import top.continew.starter.core.util.validation.CheckUtils; -import top.continew.starter.core.util.validation.ValidationUtils; +import java.lang.invoke.MethodHandleProxies; +import java.lang.invoke.MethodHandles; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.*; +import java.util.function.Function; /** * CRUD 业务实现基类 @@ -100,20 +104,16 @@ public abstract class CrudServiceImpl, T extends BaseIdD CrudProperties crudProperties = SpringUtil.getBean(CrudProperties.class); CrudTreeProperties treeProperties = crudProperties.getTree(); TreeField treeField = listClass.getDeclaredAnnotation(TreeField.class); - TreeNodeConfig treeNodeConfig; - Long rootId; // 简单树(下拉列表)使用全局配置结构,复杂树(表格)使用局部配置 - if (isSimple) { - treeNodeConfig = treeProperties.genTreeNodeConfig(); - rootId = treeProperties.getRootId(); - } else { - treeNodeConfig = treeProperties.genTreeNodeConfig(treeField); - rootId = treeField.rootId(); - } + TreeNodeConfig treeNodeConfig = isSimple ? treeProperties.genTreeNodeConfig() : treeProperties.genTreeNodeConfig(treeField); + String valueGetter = CharSequenceUtil.genGetter(treeField.value()); + String parentIdKeyGetter = CharSequenceUtil.genGetter(treeField.parentIdKey()); + Function getId = createMethodReference(listClass, valueGetter); + Function getParentId = createMethodReference(listClass, parentIdKeyGetter); // 构建树 - return TreeUtil.build(list, rootId, treeNodeConfig, (node, tree) -> { - tree.setId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.value()))); - tree.setParentId(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.parentIdKey()))); + return TreeBuildUtils.buildMultiRoot(list, getId, getParentId, treeNodeConfig, (node, tree) -> { + tree.setId(ReflectUtil.invoke(node, valueGetter)); + tree.setParentId(ReflectUtil.invoke(node, parentIdKeyGetter)); tree.setName(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.nameKey()))); tree.setWeight(ReflectUtil.invoke(node, CharSequenceUtil.genGetter(treeField.weightKey()))); // 如果构建简单树结构,则不包含扩展字段 @@ -127,6 +127,26 @@ public abstract class CrudServiceImpl, T extends BaseIdD }); } + /** + * 通过反射创建方法引用 + * + * @param clazz 实体类类型 + * @param methodName 方法名 + * @param 实体类类型 + * @param 返回值类型 + * @return Function 方法引用 + */ + @SuppressWarnings("unchecked") + public static Function createMethodReference(Class clazz, String methodName) { + try { + Method method = clazz.getDeclaredMethod(methodName); + method.setAccessible(true); + return MethodHandleProxies.asInterfaceInstance(Function.class, MethodHandles.lookup().unreflect(method)); + } catch (Exception e) { + throw new RuntimeException("Failed to create method reference for " + methodName, e); + } + } + @Override public D get(Long id) { T entity = super.getById(id, false);