1. 从零开始:理解树形菜单与若依的代码生成器
树形菜单,说白了就是那种一层套一层的菜单结构,就像电脑里的文件夹,点开一个,里面可能还有好几个子文件夹。在后台管理系统里,这种结构太常见了,比如部门管理、菜单管理、分类管理,几乎无处不在。以前做这种功能,前后端都得写不少重复的递归代码,一个不小心,性能问题或者无限循环的Bug就找上门了,调试起来特别头疼。
我第一次用若依框架的时候,就被它的代码生成器给惊艳到了。这玩意儿简直就是“懒人”开发者的福音。它的核心思路是,你只需要关心数据库表怎么设计,剩下的增删改查、前后端页面、甚至是树形结构的展示逻辑,它都能帮你一键生成。这背后,其实是若依把那些通用的、重复的CRUD(增删改查)逻辑给抽象和模板化了。你建好表,它就能根据表的字段和注释,自动推测出前端需要什么样的表单组件,列表该展示哪些字段,以及这个数据是不是一个树形结构。
那么,怎么告诉代码生成器:“嘿,我这张表是树形结构的,你得按树的方式给我生成代码”呢?关键就在表的设计上。一张标准的树形表,通常会有三个核心字段:一个唯一标识(比如 id),一个父级标识(比如 parent_id),还有一个用于排序的字段(比如 order_num)。parent_id 为 0 或者 NULL 的记录,通常就代表它是根节点。代码生成器会识别这种表结构,并在生成代码时,自动应用树形数据的查询和处理逻辑。接下来,我就带你一步步走通这个流程,并分享几个我踩过坑才总结出来的优化技巧。
2. 实战第一步:设计表结构与导入
万事开头难,但这一步走扎实了,后面就一马平川。我们假设要做一个“产品分类”的功能,分类自然是树形的。
2.1 设计一张标准的树形表
打开你的数据库工具,比如 MySQL,执行下面这条建表语句。我强烈建议你把字段注释写清楚,因为若依的代码生成器会读取这些注释,作为生成页面上字段标签的依据,这能省去你后期手动修改前端文字的工作。
CREATE TABLE `prod_category` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类ID',
`parent_id` bigint(20) DEFAULT 0 COMMENT '父分类ID(0代表根节点)',
`category_name` varchar(100) NOT NULL COMMENT '分类名称',
`order_num` int(4) DEFAULT 0 COMMENT '显示顺序',
`status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='产品分类表';
这张表里,parent_id 和 order_num 是树形结构的灵魂。parent_id 指向父节点的 id,order_num 决定了兄弟节点之间的展示顺序。后面那些 create_time、update_by 是若依框架默认的审计字段,有了它们,生成的代码会自动带上操作日志和权限过滤的功能,非常方便。
2.2 在若依系统中导入数据表
表建好后,登录若依后台,找到 系统工具 -> 代码生成 菜单。点击“导入”按钮,在数据库表列表里,你应该能看到刚刚创建的 prod_category 表,勾选它,然后确定。
导入成功后,这张表就会出现在代码生成的管理列表里。这时候,别急着生成代码,我们先得进行一项关键配置:编辑生成信息。点击列表操作栏里的“编辑”按钮,你会进入一个信息非常丰富的配置页面。
3. 核心配置:编辑生成信息与前端递归逻辑
点击编辑后,系统会做两件事:一是根据 table_id 查询 gen_table 和 gen_table_column 这两张元数据表,把表的基本信息和所有字段信息都回显出来;二是通过独立的接口,拉取系统中所有的字典数据和菜单树数据,供你为字段配置字典关联或上级菜单选择。
3.1 配置基本信息与字段属性
在这个编辑页面,你需要重点关注几个地方:
- 基本信息:这里可以设置生成模块的名称(如
product)、业务名(如category)、实体类名称等。这些名字会直接决定生成出来的Java包路径、类名和前端路由,所以最好遵循项目规范。 - 字段信息:这是配置的重中之重。列表里会展示表的所有字段。你需要为每个字段设置其在增删改查界面中的角色。
- 是否插入/编辑:这个字段在新增和修改表单里要不要出现?
- 是否列表显示:这个字段要不要在数据表格里展示?
- 查询方式:如果这个字段需要支持搜索,是等于查询(
=)、模糊查询(LIKE)还是其他?对于分类名称,我们通常会选择“模糊查询”。 - 显示类型:前端用什么组件来渲染这个字段?对于
parent_id,必须将其显示类型设置为“父级菜单”或“树形选择”(不同版本若依名称可能略有差异)。对于status这种状态字段,则设置为“下拉框”,并关联对应的字典类型(如sys_normal_disable)。 - 字典类型:如上所述,为状态、类型等字段绑定一个字典,这样前端就会显示“正常/停用”这样的可读文本,而不是干巴巴的“0”和“1”。
特别注意 parent_id 字段:把它配置为“父级菜单”后,代码生成器就会明白这个实体具有层级结构。在生成的新增和编辑表单里,它会自动变成一个树形下拉选择框,让你可以选择已有的分类作为父级。
3.2 提交与后端更新逻辑
配置完成后,点击提交。后端会先校验你输入的信息,比如必填项是否填了,格式对不对。校验通过后,它会将你的所有配置更新回 gen_table 和 gen_table_column 这两张元数据表。这意味着,你的所有生成规则都被持久化保存了,下次再编辑或者生成,都会基于这次的最新配置。
3.3 理解前端递归组合数据
这是树形菜单展示的关键,也是若依做得非常巧妙的地方。原始文章里提到“树形数据的组合也是在前端做的”,这句话点明了若依在处理树形数据时采用的策略。
当我们点击生成代码,并最终运行项目,访问分类管理页面时,前端会调用一个查询列表的接口。这个接口返回的数据,通常是一个扁平的列表,包含了所有分类,每条数据都有 id 和 parent_id。类似下面这样:
[
{"id": 1, "parentId": 0, "categoryName": "电子产品", "orderNum": 1},
{"id": 2, "parentId": 1, "categoryName": "手机", "orderNum": 1},
{"id": 3, "parentId": 1, "categoryName": "电脑", "orderNum": 2},
{"id": 4, "parentId": 2, "categoryName": "智能手机", "orderNum": 1}
]
如果后端直接组装成树形结构再返回,对于深层级或大数据量的情况,后端处理压力和网络传输量都会增加。若依采用了更高效的方式:后端只提供扁平数据,由前端通过递归算法,在浏览器内存中将其组装成一棵完整的树。
这样做的好处很明显:
- 减轻后端压力:后端只需要简单的查询,逻辑单一。
- 传输数据量小:扁平列表比嵌套的JSON树结构体积更小。
- 前端处理灵活:前端可以根据UI组件的需求(比如Element UI的
el-tree),自由地转换数据结构。
生成的Vue代码中,会包含一个名为 handleTree 或类似功能的方法,它就是干这个事情的。它遍历这个扁平数组,根据 id 和 parent_id 的关系,将子节点对象放到其父节点的 children 数组里,最终形成一个嵌套的树形对象,供给树形表格或下拉选择器使用。
4. 生成代码与调试:让菜单跑起来
配置妥当后,回到代码生成列表,勾选 prod_category,点击“生成代码”。系统会打包生成一个ZIP文件,里面包含了从Controller、Service、Mapper、Entity到Vue组件的所有代码。
4.1 后端代码集成
解压ZIP包,将Java文件放到项目对应的包路径下(通常生成器会给出提示路径)。你会发现,生成的Controller里已经包含了对树形结构查询的支持。关键在查询方法上,它可能直接调用了若依框架提供的 TreeBuild 工具类,或者返回扁平列表让前端处理。你需要检查一下生成的 ProdCategoryMapper.xml 文件,里面的查询SQL大概是这样的:
<select id="selectCategoryList" resultType="ProdCategory">
SELECT id, parent_id, category_name, order_num, status, create_time
FROM prod_category
<where>
<!-- 这里会有一些动态查询条件,比如根据分类名称模糊查询 -->
<if test="categoryName != null and categoryName != ''">
AND category_name like concat('%', #{categoryName}, '%')
</if>
<if test="status != null and status != ''">
AND status = #{status}
</if>
</where>
ORDER BY parent_id, order_num
注意最后的 ORDER BY parent_id, order_num,这个排序至关重要。它保证了查询出来的扁平列表,父节点一定出现在其子节点之前,这为前端的高效递归组装提供了便利。
4.2 前端代码集成与调试
将Vue文件放到前端项目的对应目录(如 views/product/category)。通常生成的是一个 index.vue 组件。集成后,你需要为这个菜单配置路由和权限。在若依的路由管理里,添加一个新菜单,指向这个组件。
现在,启动你的前后端项目,访问刚添加的“产品分类”菜单。你应该能看到一个树形表格,顶部有新增、修改、删除等按钮。尝试新增一个根分类(父级ID选“顶级”),再新增几个子分类。页面应该能正确展示出树形层级关系。
调试小技巧:如果树没有显示出来,首先打开浏览器的开发者工具(F12),切换到“网络(Network)”标签页,查看查询列表的接口返回的数据是不是正确的扁平结构。然后,检查前端Vue组件中,用于转换树形数据的方法(如 handleTree)是否被正确调用,以及它生成的树形数据是否符合你所用的UI组件(如 el-table 的 tree-props)的要求。
5. 性能优化与进阶技巧
代码生成器给了我们一个完美的起点,但真实项目往往有更复杂的需求。下面分享几个我实战中总结的优化点。
5.1 应对大数据量的树形查询
当分类数据成千上万时,一次性查询所有数据并让前端组装,可能会导致页面卡顿甚至浏览器崩溃。这里有几个优化方向:
- 后端懒加载:这是最常用的优化手段。修改查询接口,让它支持按父节点ID查询子节点。初始只加载根节点,当用户点击展开某个节点时,再动态去请求该节点的子节点数据。这需要修改生成的代码,将原来的全量查询接口改造成支持
parentId参数的查询,并且前端树组件要配置懒加载模式。 - 虚拟滚动:如果必须展示全量树,可以考虑使用支持虚拟滚动的树组件。它只渲染可视区域内的节点,极大提升性能。
- 数据缓存:利用Vuex或Pinia对已加载的树节点数据进行缓存,避免重复请求。
5.2 增强生成的树形功能
生成的代码可能只满足了基本的展示和增删改查。我们通常还需要:
- 节点拖拽排序:允许用户通过拖拽来调整分类顺序。这需要在前端使用支持拖拽的树组件(如
el-tree开启draggable),并在拖拽结束时,向后端发送一个更新节点parent_id和order_num的请求。 - 面包屑导航:在编辑某个深层级分类时,在页面顶部显示从根节点到当前节点的路径。这需要你写一个方法,根据当前节点的
id,递归向上查询其所有父节点,这个功能通常需要后端提供一个“查询父节点链”的接口。 - 路径字段冗余:对于一些需要频繁根据节点查询所有子孙节点的场景(如权限过滤),可以在表里增加一个
ancestors字段(祖先路径),存储从根节点到当前节点的ID路径,用分隔符连接,如0:1:2。这样,查询某个节点的所有子节点,用WHERE ancestors LIKE ‘0:1:2:%’就能快速搞定,这是一种“空间换时间”的优化。当然,这需要在新增和修改节点时,额外维护这个字段。
5.3 代码生成器的定制化
若依的代码生成器模板是可以自定义的!如果你发现生成的代码风格或某些固定逻辑不符合你们团队的习惯,不要手动去改每一个生成的文件,那会是一场维护噩梦。正确的做法是去修改代码生成器的Velocity模板文件。这些模板文件位于后端项目的 resources/vm 目录下,比如 java/controller.java.vm 控制着Controller的生成逻辑。你可以根据团队规范,调整生成的类注释、方法命名、日志记录格式等。一次修改,终身受益。
6. 常见问题排查与解决思路
在实际使用中,你可能会遇到一些坑。这里列举几个典型的:
-
问题:树形结构显示混乱,节点层级错乱。
- 排查:首先检查数据库数据,确认
parent_id是否正确指向了已存在的id,避免出现循环引用(比如A的父节点是B,B的父节点又是A)。然后,检查后端查询SQL的排序,必须是ORDER BY parent_id, order_num,确保父节点在前。 - 解决:清理错误数据,修正排序逻辑。
- 排查:首先检查数据库数据,确认
-
问题:前端页面空白,控制台报JS错误。
- 排查:打开浏览器控制台,看是否有“Cannot read property ‘xxx’ of undefined”这类错误。这通常是前端
handleTree方法在处理数据时,因为数据格式不符合预期而报错。 - 解决:确认后端接口返回的数据格式是否为约定的扁平数组,检查每个对象是否都有
id和parent_id字段。可以在handleTree方法开始处打印一下入参,看看数据到底长什么样。
- 排查:打开浏览器控制台,看是否有“Cannot read property ‘xxx’ of undefined”这类错误。这通常是前端
-
问题:编辑时,父级选择树里出现了自己,导致可以选择自己作为父节点,形成死循环。
- 排查:这是前端树形下拉组件没有过滤掉当前编辑节点及其所有子孙节点导致的。
- 解决:在打开编辑弹窗、加载树形下拉数据时,需要将当前节点的
id传给树组件,并在组件内部实现一个过滤函数,递归排除掉以该节点为根节点的整棵子树。这需要你修改生成的Vue组件,在获取树数据后,手动处理一下数据源。
-
问题:权限控制如何与树形菜单结合? 比如,只允许用户管理其所在部门及子部门下的分类。
- 解决:这需要在后端查询时加入数据权限过滤。若依框架本身提供了基于部门的数据权限注解
@DataScope。你可以在生成的Service方法上添加此注解,并确保你的树形表中有与部门关联的字段(如dept_id)。这样,查询会自动拼接上数据权限的SQL条件,实现数据的自动过滤。对于树形结构,你需要确保过滤后,前端组装的树仍然是完整的(即如果父节点无权限,但某个子节点有权限,这个子节点需要能挂载到一个可见的父节点下),这可能需要对权限过滤逻辑做一些特殊处理。
- 解决:这需要在后端查询时加入数据权限过滤。若依框架本身提供了基于部门的数据权限注解
最后我想说,若依的代码生成器是一个强大的生产力工具,它把我们从重复的底层CRUD代码中解放出来。但它生成的代码是“通用版”的,理解其树形菜单的实现原理——特别是前后端职责分离(后端提供扁平数据,前端递归组装)——至关重要。这样,当遇到复杂业务场景时,你才能知道从哪里入手进行定制和优化,而不是对着生成的代码束手无策。我自己的经验是,先放心使用生成器快速搭建功能原型,然后再根据实际需求,像精雕细琢一样去优化那些特定的业务逻辑和性能瓶颈,这样的开发节奏才是最舒服、最高效的。
1930



被折叠的 条评论
为什么被折叠?



