分類管理是 CMS、電商系統的核心功能。本文展示如何在 ThinkPHP 中高效讀取二級/多級分類,構建樹形結構,並在模板中優雅渲染。
CREATE TABLE `category` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`pid` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '父級ID,0為頂級',
`name` VARCHAR(50) NOT NULL,
`sort` INT UNSIGNED NOT NULL DEFAULT 0,
`status` TINYINT UNSIGNED NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
KEY `idx_pid_status` (`pid`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
<?php
// 控制器
public function index() {
// 1. 獲取所有一級分類
$topCates = Db::name('category')
->where(['pid' => 0, 'status' => 1])
->order('sort ASC, id ASC')
->select();
if ($topCates) {
// 2. 獲取所有二級分類(一次查詢避免 N+1)
$pids = array_column($topCates, 'id');
$childCates = Db::name('category')
->where(['pid' => ['IN', $pids], 'status' => 1])
->order('sort ASC, id ASC')
->select();
// 3. 按 pid 分組
$childGroup = [];
foreach ($childCates as $cate) {
$childGroup[$cate['pid']][] = $cate;
}
// 4. 掛載到一級分類
foreach ($topCates as &$cate) {
$cate['children'] = $childGroup[$cate['id']] ?? [];
}
}
$this->assign('categories', $topCates);
return $this->fetch();
}
?>
<?php
// 工具類或模型方法
function buildTree(array $data, int $pid = 0, string $pk = 'id', string $pidKey = 'pid'): array {
$tree = [];
foreach ($data as $item) {
if ($item[$pidKey] == $pid) {
$children = buildTree($data, $item[$pk], $pk, $pidKey);
if ($children) $item['children'] = $children;
$tree[] = $item;
}
}
return $tree;
}
// 控制器使用
public function tree() {
$allCates = Db::name('category')
->where('status', 1)
->order('sort ASC, id ASC')
->select()
->toArray(); // 轉純數組提高遞歸性能
$tree = buildTree($allCates);
$this->assign('categories', $tree);
return $this->fetch();
}
?>
<?php
// 僅獲取某分類下的所有子孫 ID(如刪除分類連帶子分類)
public function getDescendantIds(int $cateId): array {
$sql = <<column('id');
}
?>
<!-- 二級分類渲染 -->
<ul class="cate-list">
{volist name="categories" id="cate"}
<li>
<a href="{:url('list', ['cid' => $cate.id])}">{$cate.name}</a>
{if !empty($cate.children)}
<ul class="sub-cate">
{volist name="cate.children" id="sub"}
<li><a href="{:url('list', ['cid' => $sub.id])}">{$sub.name}</a></li>
{/volist}
</ul>
{/if}
</li>
{/volist}
</ul>
<!-- 定義遞歸模板文件 widget/cate_tree.html -->
<ul>
{volist name="categories" id="cate"}
<li>
<a href="{:url('list', ['cid' => $cate.id])}">{$cate.name}</a>
{if !empty($cate.children)}
{include file="widget/cate_tree" categories="$cate.children" /}
{/if}
</li>
{/volist}
</ul>
<!-- 主模板調用 -->
{include file="widget/cate_tree" categories="$categories" /}
<?php
// API 返回樹形 JSON
public function apiTree() {
$all = Db::name('category')->where('status', 1)->select()->toArray();
$tree = buildTree($all);
return json($tree);
}
?>
<!-- 前端 Vue + Element UI TreeSelect -->
<el-tree-select
v-model="form.cate_id"
:data="cateTree"
:props="{label: 'name', value: 'id', children: 'children'}"
placeholder="請選擇分類"
@node-click="handleNodeClick"
></el-tree-select>
<script>
export default {
data() { return { cateTree: [], form: { cate_id: '' } }; },
mounted() {
fetch('/category/apiTree').then(r => r.json()).then(data => this.cateTree = data);
}
}
</script>
<?php
public function getCachedTree() {
return cache('category_tree', function() {
$all = Db::name('category')->where('status', 1)->order('sort, id')->select()->toArray();
return buildTree($all);
}, 3600); // 緩存 1 小時
}
// 後台更新分類後清除緩存
public function clearCateCache() {
cache('category_tree', null);
return json(['code' => 1]);
}
?>
| 方案 | 適用場景 | 查詢次數 | 代碼複雜度 | 擴展性 |
|---|---|---|---|---|
| 兩次查詢組裝 | 固定二級、數據量小 | 2 | 低 | 僅二級 |
| PHP 遞歸構建 | 無限級、數據量 < 5000 | 1 | 中 | 全支援 |
| MySQL CTE 遞歸 | MySQL 8.0+、需單獨查子樹 | 1 | 中 | 全支援 |
| 緩存樹 + 異步更新 | 高併發讀取、分類變動少 | 0 (命中緩存) | 高 | 全支援 |
建議:中小項目用 PHP 遞歸 + 緩存,大型項目考慮 Closure Table(閉包表) 或 物化路徑 結構實現高效祖先/後代查詢。