應用場景
- 組織機構樹 (部門/崗位/人員)
- 菜單權限樹 (多級路由)
- 分類目錄樹 (電商 SKU 分類、CMS 欄目)
- 地區行政區劃 (省/市/區/街道)
數據表結構設計
CREATE TABLE sys_dept (
id VARCHAR(64) PRIMARY KEY COMMENT '主鍵',
parent_id VARCHAR(64) COMMENT '父節點ID,頂級為 0 或 NULL',
name VARCHAR(100) COMMENT '節點名稱',
sort INT DEFAULT 0 COMMENT '同級排序',
status TINYINT DEFAULT 1 COMMENT '1啟用 0禁用'
);
通用 TreeNode 實體 (支援任意業務數據泛型)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TreeNode<T> {
private String id; // 節點唯一標識
private String parentId; // 父節點ID
private String text; // 顯示文本
private String state = "open"; // easyui/zTree 狀態: open/closed
private boolean checked = false; // 是否勾選
private List<Map<String, Object>> attributes; // 擴展屬性
private List<TreeNode<T>> children = new ArrayList<>(); // 子節點
private boolean isParent = false; // 是否父節點
private boolean isChildren = false; // 是否有子節點
private T data; // 攜帶原始業務對象 (可選)
}
O(n) 時間複雜度構建算法 (HashMap 索引 + 單次遍歷)
public class TreeBuilder {
public static <T> List<TreeNode<T>> build(List<TreeNode<T>> nodes, String rootPid) {
if (nodes == null || nodes.isEmpty()) return Collections.emptyList();
// 1. 建立 ID -> Node 映射 (O(n))
Map<String, TreeNode<T>> nodeMap = new HashMap<>(nodes.size() * 2);
nodes.forEach(node -> nodeMap.put(node.getId(), node));
// 2. 掛載子節點 (O(n))
List<TreeNode<T>> roots = new ArrayList<>();
for (TreeNode<T> node : nodes) {
String pid = node.getParentId();
if (rootPid.equals(pid) || pid == null || pid.isEmpty()) {
roots.add(node); // 頂級節點
} else {
TreeNode<T> parent = nodeMap.get(pid);
if (parent != null) {
parent.getChildren().add(node);
parent.setParent(true);
node.setChildren(true); // 標記父子關係
} else {
// 父節點不存在 (臟數據),視為根節點或拋異常
roots.add(node);
}
}
}
// 3. 遞歸排序子節點 (可選)
roots.forEach(TreeBuilder::sortChildrenRecursively);
return roots;
}
private static <T> void sortChildrenRecursively(TreeNode<T> node) {
node.getChildren().sort(Comparator.comparingInt(n -> n.getData() != null ? ((Sortable) n.getData()).getSort() : 0));
node.getChildren().forEach(TreeBuilder::sortChildrenRecursively);
}
}
Service 層組裝調用
@Service
public class DeptServiceImpl implements DeptService {
@Autowired private DeptMapper deptMapper;
@Override
public List<TreeNode<Dept> tree() {
// 1. 查詢所有啟用部門
List<Dept> depts = deptMapper.selectEnabled(); // SELECT * FROM sys_dept WHERE status=1
// 2. 轉換為 TreeNode
List<TreeNode<Dept>> nodes = depts.stream().map(d -> {
TreeNode<Dept> node = new TreeNode<>();
node.setId(d.getId());
node.setParentId(d.getParentId());
node.setText(d.getName());
node.setData(d); // 攜帶原始對象,前端可能需要額外字段
return node;
}).collect(Collectors.toList());
// 3. 構建樹
return TreeBuilder.build(nodes, "0"); // 假設頂級 parent_id = '0'
}
}
Controller 返回 JSON (FastJSON / Jackson 均可)
@RestController
@RequestMapping("/api/dept")
public class DeptController {
@Autowired private DeptService deptService;
@GetMapping("/tree")
public List<TreeNode<Dept>> tree() {
return deptService.tree(); // 直接返回 List,框架自動序列化為 JSON 數組
}
}
前端渲染示例 (zTree / Ant Design Tree / Element Tree)
// zTree 配置
var setting = {
data: { simpleData: { enable: true, idKey: "id", pIdKey: "parentId", rootPId: "0" } },
callback: { onClick: function(e, treeId, treeNode) { console.log(treeNode.id); } }
};
$.fn.zTree.init($("#tree"), setting, treeData); // treeData 為後端返回的 List
輸出 JSON 結構示例
[
{
"id": "1",
"parentId": "0",
"text": "技術部",
"state": "open",
"checked": false,
"isParent": true,
"children": [
{
"id": "11",
"parentId": "1",
"text": "Java 組",
"isParent": false,
"children": []
}
]
},
{
"id": "2",
"parentId": "0",
"text": "產品部",
"isParent": false,
"children": []
}
]
進階優化與避坑
- 大數據量分頁加載:前端按需加載 (lazy load),後端提供
/tree/children?parentId=xxx 接口。
- 循環引用檢測:構建前用 DFS 檢測是否存在環 (父子關係形成環),防止死循環/棧溢出。
- 權限過濾:構建樹時根據用戶角色過濾不可見節點,或前端按權限控制顯示。
- 序列化深度限制:Jackson 配置
MapperFeature.MAX_NESTING_DEPTH 防止極深樹導致 StackOverflowError。
- 數據庫遞歸查詢:MySQL 8.0+ 支持
WITH RECURSIVE CTE 單條 SQL 查詢全樹,避免多次 round-trip。