本文目录导读:

构建“部门树”(即组织架构树)是后端开发中非常常见的需求,比如在考勤系统、OA系统、权限管理系统中都会用到。
本质上,这是一个将扁平的列表数据,转换为具有层级结构的树形数据的过程。
以下是几种常见的构建方案,从数据库设计到前端处理,分场景讲解。
第一步:数据库表设计
这是基础,通常有两种主流设计方式:
邻接表模式
最常用、最直观,每一条记录只记录自己的直接父级。
CREATE TABLE department (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
parent_id BIGINT DEFAULT NULL, -- 父级ID,顶级部门为NULL或0
sort_order INT DEFAULT 0, -- 排序字段
-- 其他字段...
FOREIGN KEY (parent_id) REFERENCES department(id)
);
示例数据: | id | name | parent_id | sort_order | |----|------|-----------|------------| | 1 | 总公司 | NULL | 1 | | 2 | 技术部 | 1 | 1 | | 3 | 产品部 | 1 | 2 | | 4 | 前端组 | 2 | 1 | | 5 | 后端组 | 2 | 2 | | 6 | 设计组 | 3 | 1 |
第二步:后端构建树的算法
假设你已经从数据库查询出了所有部门列表(List<Department>),现在需要将其转换为树结构。
递归构建(简单直观,适合数据量小 / < 几百条)
核心思想:找到根节点,然后递归为每个节点寻找子节点。
Java 伪代码:
// 1. 查询所有部门
List<Department> allDepts = departmentMapper.selectList(null);
// 2. 构建树
public List<DepartmentTree> buildTree(List<Department> allDepts) {
List<DepartmentTree> trees = new ArrayList<>();
// 用于快速查找的Map
Map<Long, DepartmentTree> dtoMap = allDepts.stream()
.collect(Collectors.toMap(Department::getId, DepartmentTree::new));
for (Department dept : allDepts) {
DepartmentTree node = dtoMap.get(dept.getId());
if (dept.getParentId() == null) {
// 根节点
trees.add(node);
} else {
// 非根节点,找到父节点并添加
DepartmentTree parent = dtoMap.get(dept.getParentId());
if (parent != null) {
parent.getChildren().add(node);
}
}
}
// 可选:对每个节点的子节点进行排序
sortChildren(trees);
return trees;
}
优点:逻辑简单,只需要一次数据库查询。 缺点:如果部门层级非常深(比如超过100层),递归可能导致栈溢出。
两次遍历 + Map 映射(推荐,最高效)
核心思想:第一次遍历把所有节点放入Map,第二次遍历建立父子关系。
Python 伪代码:
def build_tree(dept_list):
"""
dept_list: list of dict, 每个dict包含 id, name, parent_id
return: list of root nodes
"""
node_map = {}
roots = []
# 第一次遍历:创建所有节点,并以id为key存入map
for dept in dept_list:
node = {
'id': dept['id'],
'name': dept['name'],
'children': []
}
node_map[dept['id']] = node
# 第二次遍历:建立父子关系
for dept in dept_list:
node = node_map[dept['id']]
parent_id = dept.get('parent_id')
if parent_id is None or parent_id == 0:
roots.append(node)
else:
parent_node = node_map.get(parent_id)
if parent_node:
parent_node['children'].append(node)
# 排序(可选)
def sort_tree(nodes):
for node in nodes:
if node['children']:
node['children'].sort(key=lambda x: x.get('sort_order', 0))
sort_tree(node['children'])
sort_tree(roots)
return roots
优点:时间复杂度 O(n),空间换时间,非常快,适合任何深度。
第三步:前端如何处理?
如果你只是需要前端来拼树(比如从后端拿到的数据是平铺的),可以使用JavaScript的reduce或Map来实现,思路与后端完全一致:
function buildTree(flatList) {
const map = {};
const tree = [];
// 1. 建立映射
flatList.forEach(item => {
map[item.id] = { ...item, children: [] };
});
// 2. 构建树
flatList.forEach(item => {
const node = map[item.id];
if (item.parentId === null || item.parentId === 0) {
tree.push(node);
} else {
const parent = map[item.parentId];
if (parent) {
parent.children.push(node);
}
}
});
// 3. 排序(可选)
const sortChildren = (nodes) => {
nodes.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
nodes.forEach(node => {
if (node.children) sortChildren(node.children);
});
};
sortChildren(tree);
return tree;
}
特殊情况与优化技巧
-
大数据量优化:
- 如果全量数据超过几千条,一次性加载到内存构建可能会慢,建议在数据库层使用递归CTE(PostgreSQL/MySQL 8.0+)。
- MySQL 8.0+ 递归CTE示例:
WITH RECURSIVE dept_tree AS ( SELECT id, name, parent_id, 1 AS level FROM department WHERE parent_id IS NULL UNION ALL SELECT d.id, d.name, d.parent_id, dt.level + 1 FROM department d INNER JOIN dept_tree dt ON d.parent_id = dt.id ) SELECT * FROM dept_tree;这种方式直接在数据库层面拿到带层级的平铺数据,然后后端用Map组装即可。
-
树结构校验:
- 防止出现循环依赖:A的父级是B,B的父级是C,C的父级是A,简单办法:构建时记录已访问过的节点ID,如果某节点又是自己的祖先,则报错或断开链路。
-
懒加载(按需加载):
- 如果树非常深(比如10层以上)且节点很多,建议不要一次加载全部,可以只加载顶层,用户点击展开时,再通过接口加载对应子节点。
-
路径字段:
- 为了快速查询某个部门下的所有子部门,可以增加一个
path字段(如1/2/4/表示总公司->技术部->前端组),这样用LIKE '1/2/%'就能查到所有子孙部门,不用递归。
- 为了快速查询某个部门下的所有子部门,可以增加一个
推荐方案
- 中小规模(<2000条):使用 两次遍历 + Map 的方式,代码简单,效率高。
- 大规模:优先使用数据库 递归CTE 或 路径字段 + 后端Map组装。
- 前端使用:如果数据已经是平铺的,直接用JS的Map法在前端组装,可以减轻后端压力(适用于小树)。
如果你用的是Java,推荐使用Stream API配合Collectors.toMap来做;如果是JavaScript,注意使用Map代替Object来存储节点映射,性能会更好。