核心邏輯
- 取得目標分類 ID
- 遞歸收集所有後代分類 ID
- 組合成
IN (...) 單次刪除
刪除入口
<?php
$act = isset($_GET['act']) ? trim($_GET['act']) : '';
if ($act === 'del') {
$sort_id = isset($_GET['id']) ? intval($_GET['id']) : 0;
if ($sort_id === 0) exit('Invalid ID');
$ids = $sort_id . getChildrenIds($sort_id); // 字串 "1,2,3,4"
$sql = "DELETE FROM `article_sort` WHERE `sort_id` IN ($ids)";
$res = $db->exec($sql); // PDO::exec 返回影響行數
if ($res !== false) {
alert('刪除成功', 'sort_list.php');
} else {
alert('刪除失敗', 'sort_list.php');
}
exit;
}
?>
遞歸收集子分類 ID
<?php
/**
* 遞歸取得指定分類下所有後代 ID 字串 (如 ",2,3,5")
*
* @param int $sort_id 父分類 ID
* @return string 以逗號開頭的 ID 字串
*/
function getChildrenIds($sort_id)
{
global $db;
$ids = '';
$sql = "SELECT `sort_id` FROM `" . $db->table('article_sort') . "` WHERE `parent_id` = ?";
$stmt = $db->prepare($sql);
$stmt->execute([$sort_id]);
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$ids .= ',' . $row['sort_id'];
$ids .= getChildrenIds($row['sort_id']);
}
return $ids;
}
?>
效能與安全優化
| 問題 | 解決方案 |
| N+1 查詢 | 改用 Closure Table 或 遞歸 CTE (MySQL 8.0+) 單次查詢取得完整子樹 |
| SQL 注入 | ID 已 intval,IN 子句拼接安全;或改用 WHERE sort_id IN (?,?,...) 參數綁定 |
| 超大 ID 字串超過 SQL 長度限制 | 分批刪除或使用臨時表 |
| 誤刪保護 | 刪除前檢查文章關聯、權限驗證、軟刪除 (deleted_at) |
MySQL 8.0 遞歸 CTE 範例
WITH RECURSIVE `cte` AS (
SELECT `sort_id` FROM `article_sort` WHERE `sort_id` = ?
UNION ALL
SELECT a.`sort_id` FROM `article_sort` a
JOIN `cte` c ON a.`parent_id` = c.`sort_id`
)
DELETE FROM `article_sort` WHERE `sort_id` IN (SELECT `sort_id` FROM `cte`);