基于復選框的多商品批量刪除功能(PHP + HTML 教程))
前言批量刪除聽起來是列表頁加一列復選框、后面接個循環(huán)的小功能但它是最容易出事故的列表操作之一。常見癥狀有四種勾了十條只刪掉一條明明只勾了幾條數(shù)據(jù)庫里一串無關記錄被刪了勾選數(shù)量一多隨機地少刪幾條而且沒有任何報錯以及刷新頁面導致重復提交。這些癥狀分別對應四個不同的根因沒把復選框提交成數(shù)組、把 id 直接拼進了 SQL、表單字段數(shù)超過max_input_vars被 PHP 靜默截斷、沒有用 POST/Redirect/Get。它們都不難解決但每一個都必須在寫第一行代碼時就想到。本文會用 PHP HTML 寫一個完整可運行的批量刪除前端復選框、全選、CSRF 令牌后端用 PDO 預處理占位符、校驗每一個 id、軟刪除并做 PRG 跳轉。示例用 SQLite 內(nèi)存庫跑換成 MySQL 只需改 DSN 和那句datetime(now)其余邏輯完全一致最低 PHP 8.0。一、前端復選框必須提交成數(shù)組HTML 表單里同名的多個控件只有寫成nameids[]才會被 PHP 解析成數(shù)組$_POST[ids]。寫成nameids時PHP 只會保留最后一個值——這就是勾了十條只刪一條的最常見原因。同時要記住兩點一是刪除動作必須走 POST不能用a hrefdelete.php?id1因為瀏覽器預取、爬蟲、甚至聊天軟件生成鏈接預覽都可能觸發(fā) GET二是要帶 CSRF 令牌否則任何第三方頁面都能誘導已登錄用戶發(fā)出刪除請求。form methodpost actionbatch-delete.php idbatch-form input typehidden namecsrf value__CSRF__ table thead tr thinput typecheckbox idcheck-all aria-label全選/th th商品/th th價格/th /tr /thead tbody tr tdinput typecheckbox nameids[] value1 classrow-check/td td機械鍵盤/td td399.00/td /tr tr tdinput typecheckbox nameids[] value2 classrow-check/td td無線鼠標/td td129.00/td /tr tr tdinput typecheckbox nameids[] value3 classrow-check/td td27 寸顯示器/td td1299.00/td /tr /tbody /table button typesubmit idsubmit-btn批量刪除/button /form script const all document.getElementById(check-all); const rows document.querySelectorAll(.row-check); all.addEventListener(change, () { rows.forEach((box) { box.checked all.checked; }); }); document.getElementById(batch-form).addEventListener(submit, (event) { if (!document.querySelector(.row-check:checked)) { event.preventDefault(); alert(請至少選擇一件商品); } }); /script前端校驗只是體驗優(yōu)化服務端永遠要再校驗一次。二、后端為什么必須用占位符而不是拼接WHERE id IN (1,2,3)里的IN列表長度是可變的很多人圖省事直接implode(,, $_POST[ids])這就是注入的入口而且很容易順手把整個條件拼成WHERE id IN (1,2,3)——在 MySQL 里字符串隱式轉換后只匹配到第一行于是出現(xiàn)只刪一條的詭異現(xiàn)象。正確做法是把每個 id 嚴格校驗成整數(shù)再按數(shù)量生成等量的?占位符。-- 軟刪除保留數(shù)據(jù)只打時間戳 UPDATE products SET deleted_at datetime(now) WHERE deleted_at IS NULL AND shop_id ? AND id IN (?, ?, ?)注意還有shop_id ?這個歸屬條件。批量刪除最常見的越權事故就是只按 id 刪而沒有校驗這條記錄是否屬于當前用戶或當前租戶。三、完整可運行示例下面這份代碼可以直接存成batch-delete.php用php -S 127.0.0.1:8000跑起來。它用 SQLite 內(nèi)存庫自建表把處理邏輯和頁面放在同一個文件里生產(chǎn)環(huán)境建議拆開。?php // 最低版本PHP 8.0 declare(strict_types1); session_start(); function db(): PDO { static $pdo null; if ($pdo instanceof PDO) { return $pdo; } $pdo new PDO(sqlite::memory:, null, null, [ PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES false, ]); $pdo-exec(CREATE TABLE products ( id INTEGER PRIMARY KEY, shop_id INTEGER NOT NULL, title TEXT NOT NULL, deleted_at TEXT NULL )); $insert $pdo-prepare(INSERT INTO products (id, shop_id, title) VALUES (?, 7, ?)); foreach ([[1, 機械鍵盤], [2, 無線鼠標], [3, 27 寸顯示器]] as [$id, $title]) { $insert-execute([$id, $title]); } return $pdo; } function csrfToken(): string { if (empty($_SESSION[csrf])) { $_SESSION[csrf] bin2hex(random_bytes(16)); } return $_SESSION[csrf]; } function assertCsrf(?string $token): void { $expected $_SESSION[csrf] ?? ; if ($expected || !is_string($token) || !hash_equals($expected, $token)) { http_response_code(419); exit(CSRF 校驗失敗請刷新頁面重試); } } /** * param arraymixed $rawIds * return int 實際刪除的行數(shù) */ function deleteProducts(PDO $pdo, array $rawIds, int $shopId): int { $ids []; foreach ($rawIds as $raw) { // 嚴格校驗必須是不含前導零的純十進制整數(shù) if (!is_string($raw) || !preg_match(/^[1-9][0-9]{0,9}$/, $raw)) { throw new InvalidArgumentException(商品 id 非法); } $ids[] (int) $raw; } $ids array_values(array_unique($ids)); if ($ids []) { return 0; } if (count($ids) 500) { throw new InvalidArgumentException(單次最多刪除 500 件商品); } $placeholders implode(,, array_fill(0, count($ids), ?)); $sql UPDATE products SET deleted_at datetime(now) WHERE deleted_at IS NULL AND shop_id ? AND id IN ({$placeholders}); $stmt $pdo-prepare($sql); // 參數(shù)順序歸屬條件在前id 列表在后 $stmt-execute(array_merge([$shopId], $ids)); return $stmt-rowCount(); } // ---- 請求處理 ---- $flash $_SESSION[flash] ?? null; unset($_SESSION[flash]); if (($_SERVER[REQUEST_METHOD] ?? GET) POST) { assertCsrf($_POST[csrf] ?? null); $raw $_POST[ids] ?? []; if (!is_array($raw)) { $raw []; } try { $deleted deleteProducts(db(), $raw, 7); $_SESSION[flash] 已刪除 {$deleted} 件商品; } catch (InvalidArgumentException $e) { $_SESSION[flash] 操作失敗 . $e-getMessage(); } // PRG處理完立刻重定向防止刷新重復提交 header(Location: . $_SERVER[PHP_SELF], true, 303); exit; } // ---- 渲染 ---- $csrf csrfToken(); $products db()-query(SELECT id, title FROM products WHERE deleted_at IS NULL ORDER BY id) -fetchAll(); $csrfEscaped htmlspecialchars($csrf, ENT_QUOTES, UTF-8); if ($flash ! null) { echo p . htmlspecialchars($flash, ENT_QUOTES, UTF-8) . /p; } echo form methodpost; echo input typehidden namecsrf value . $csrfEscaped . ; foreach ($products as $p) { printf( labelinput typecheckbox nameids[] value%d %s/labelbr, $p[id], htmlspecialchars($p[title], ENT_QUOTES, UTF-8) ); } echo button typesubmit批量刪除/button/form;把上面渲染部分換成前面給出的完整 HTML 表單就是一個可以直接投用的頁面。要點是每一次刪除都經(jīng)過preg_match校驗、array_unique去重、占位符綁定、并對shop_id做了歸屬校驗。常見坑點1. 復選框沒寫成數(shù)組名?input typecheckbox nameids value1重復出現(xiàn)——PHP 只保留最后一個值癥狀是永遠只刪一條。 ? 用nameids[]后端再用is_array()兜底$_POST[ids]也可能因為別的原因變成標量。2. 直接把 id 拼進 SQL?$sql DELETE FROM products WHERE id IN ( . implode(,, $_POST[ids]) . );——既可注入又會因為值被當成一個字符串而變成IN (1,2,3)只匹配第一行。 ? 全部轉成整數(shù) array_fill(0, count($ids), ?)生成占位符按順序綁參。3. 忽略max_input_vars導致的靜默截斷? 商品列表每行有多個表單字段勾選幾百行后提交——PHP 的max_input_vars默認是 1000超出部分不會報錯只是悄悄丟掉表現(xiàn)為隨機少刪幾條而且$_POST里根本找不到原因。 ? 大表單改成fetch()提交 JSON讀php://input不受max_input_vars限制或顯式調(diào)高該 ini 值。4. 用 GET 做刪除?a href/delete.php?ids[]1刪除/a——瀏覽器預取、爬蟲、鏈接預覽都會替你點一遍。 ? 只接受 POST并在服務端校驗$_SERVER[REQUEST_METHOD]。5. 只按 id 刪除不做歸屬校驗?DELETE FROM products WHERE id IN (...)——用戶改一下表單里的 value 就能刪別人的數(shù)據(jù)。 ? 條件里帶上user_id/shop_id并檢查rowCount()是否與預期相符。6. 用rowCount()判斷是否刪除成功? 軟刪除場景下重復提交記錄已是deleted_at非空UPDATE影響 0 行代碼卻報刪除失敗其實數(shù)據(jù)早就是刪掉的。 ?rowCount()只表示本次影響的行數(shù)要區(qū)分沒刪到和本來就已刪除先SELECT查一次狀態(tài)或在 SQL 的WHERE里保留deleted_at IS NULL并接受 0 行的語義。7. 刪完直接輸出頁面? 處理完就地渲染用戶按 F5 會彈出重新提交表單并在同意后又刪一次。 ? 用 PRGPost/Redirect/Get處理完立即303重定向到列表頁提示語放 session 里閃現(xiàn)。8. 忘了事務與外鍵? 刪除商品主表時關聯(lián)的庫存、圖片記錄留在庫里界面查不到、后臺統(tǒng)計又多出來。 ? 用事務包住多條寫操作外鍵設ON DELETE CASCADE或按同樣的邏輯一并軟刪除??偨Y環(huán)節(jié)關鍵做法不加會怎樣表單控件nameids[]只提交最后一個值只刪一條CSRF隱藏字段 hash_equals()第三方頁面可誘導刪除id 校驗正則嚴格匹配十進制整數(shù)注入、或誤刪無關記錄SQL?占位符 array_fill()注入IN (1,2,3)只匹配首行歸屬校驗AND shop_id ?越權刪除他人數(shù)據(jù)大批量控制在max_input_vars內(nèi)或改用 JSON靜默截斷隨機少刪收尾303重定向PRG刷新重復提交批量刪除的正確姿勢其實是一條很短的鏈路數(shù)組名 - CSRF - 嚴格校驗 - 占位符 - 歸屬條件 - 重定向。每一步都對應一類真實事故缺任何一環(huán)都可能在生產(chǎn)環(huán)境上以偶發(fā)、難以復現(xiàn)的形式出現(xiàn)。把這些當成模板固化下來比每次臨場發(fā)揮可靠得多。