萬盛學電腦網

您现在的位置: 萬盛學電腦網 >> 程式設計 >  >> 網路程式設計 >> PHP 程式設計

PHP 簡易留言板實作(三):刪除功能、AJAX 增強與部署檢查清單

完成留言板最後核心功能:管理員刪除、前台 AJAX 即時互動,並提供生產環境部署完整檢查清單。

管理員刪除 admin/delete.php

<?php
// admin/delete.php
require_once __DIR__ . '/../includes/common.php';
require_once __DIR__ . '/../includes/admin_auth.php';

$pdo = getPDO();

$id = (int)($_POST['id'] ?? $_GET['id'] ?? 0);
if ($id <= 0) {
    // AJAX 請求回傳 JSON
    if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
        header('Content-Type: application/json');
        echo json_encode(['success' => false, 'message' => '無效 ID']);
        exit;
    }
    flash('無效 ID', 'danger');
    redirect('index.php');
}

// CSRF 驗證(支援 Header 與表單)
$csrf_token = $_POST['csrf'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? '';
if (!csrf_check($csrf_token)) {
    if (!empty($_SERVER['HTTP_X_REQUESTED_WITH'])) {
        header('Content-Type: application/json');
        http_response_code(403);
        echo json_encode(['success' => false, 'message' => 'CSRF 驗證失敗']);
        exit;
    }
    flash('CSRF 驗證失敗', 'danger');
    redirect('index.php');
}

try {
    $stmt = $pdo->prepare('DELETE FROM `messages` WHERE `id` = ?');
    $stmt->execute([$id]);
    
    if ($stmt->rowCount() > 0) {
        $msg = '留言已刪除';
        $success = true;
    } else {
        $msg = '找不到該留言';
        $success = false;
    }
} catch (PDOException $e) {
    $msg = '資料庫錯誤:' . $e->getMessage();
    $success = false;
}

// AJAX 回應
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
    header('Content-Type: application/json');
    echo json_encode(['success' => $success, 'message' => $msg]);
    exit;
}

// 一般表單提交
flash($msg, $success ? 'success' : 'danger');
redirect('index.php');

前台 AJAX 增強

引入 JavaScript 模組 public/assets/js/app.js

// public/assets/js/app.js
const API_BASE = '';

/** 通用 AJAX 請求 */
async function apiRequest(url, options = {}) {
    const defaults = {
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.content || '',
            'X-Requested-With': 'XMLHttpRequest',
        },
        credentials: 'same-origin',
    };
    
    const res = await fetch(url, { ...defaults, ...options });
    const contentType = res.headers.get('content-type') || '';
    
    if (contentType.includes('application/json')) {
        return res.json();
    }
    return res.text();
}

/** 刪除留言(管理員) */
async function deleteMessage(id, buttonEl) {
    if (!confirm('確定刪除此留言?')) return;
    
    buttonEl.disabled = true;
    buttonEl.textContent = '刪除中...';
    
    try {
        const result = await apiRequest('admin/delete.php', {
            method: 'POST',
            body: new URLSearchParams({ id, csrf: getCsrfToken() }),
        });
        
        if (result.success) {
            // 移除 DOM 元素
            const card = buttonEl.closest('.message-card') || buttonEl.closest('tr');
            card?.remove();
            showToast('刪除成功', 'success');
        } else {
            showToast(result.message || '刪除失敗', 'danger');
        }
    } catch (e) {
        showToast('網路錯誤:' + e.message, 'danger');
    } finally {
        buttonEl.disabled = false;
        buttonEl.textContent = '刪除';
    }
}

/** 取得 CSRF Token */
function getCsrfToken() {
    return document.querySelector('meta[name="csrf-token"]')?.content 
        || document.querySelector('input[name="csrf"]')?.value 
        || '';
}

/** Toast 通知 */
function showToast(message, type = 'info') {
    const container = getOrCreateToastContainer();
    const toast = document.createElement('div');
    toast.className = `toast align-items-center text-white bg-${type} border-0`;
    toast.setAttribute('role', 'alert');
    toast.innerHTML = `
        
${message}
`; container.appendChild(toast); const bsToast = new bootstrap.Toast(toast, { delay: 3000 }); bsToast.show(); toast.addEventListener('hidden.bs.toast', () => toast.remove()); } function getOrCreateToastContainer() { let container = document.getElementById('toast-container'); if (!container) { container = document.createElement('div'); container.id = 'toast-container'; container.className = 'toast-container position-fixed bottom-0 end-0 p-3'; document.body.appendChild(container); } return container; } // 綁定刪除按鈕(委派事件) document.addEventListener('click', (e) => { const btn = e.target.closest('[data-action="delete"]'); if (btn) { e.preventDefault(); const id = btn.dataset.id; if (id) deleteMessage(id, btn); } }); // 表單 AJAX 提交(新增留言、回覆) document.querySelectorAll('form[data-ajax="true"]').forEach(form => { form.addEventListener('submit', async (e) => { e.preventDefault(); const submitBtn = form.querySelector('button[type="submit"]'); const originalText = submitBtn.textContent; submitBtn.disabled = true; submitBtn.textContent = '送出中...'; try { const formData = new FormData(form); formData.append('csrf', getCsrfToken()); const result = await apiRequest(form.action, { method: 'POST', body: formData, headers: { 'Content-Type': 'multipart/form-data' }, // 讓瀏覽器自動設定 boundary }); if (result.success) { showToast(result.message || '操作成功', 'success'); form.reset(); // 留言列表頁:重新載入或插入新留言 if (form.dataset.reload === 'true') { setTimeout(() => location.reload(), 500); } } else { showToast(result.message || '操作失敗', 'danger'); } } catch (err) { showToast('錯誤:' + err.message, 'danger'); } finally { submitBtn.disabled = false; submitBtn.textContent = originalText; } }); });

頁面引入調整

<!-- 在所有頁面 head 結束前加入 -->
<meta name="csrf-token" content="<?= csrf_token() ?>">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/app.js" defer></script>

<!-- 表單加上 data-ajax -->
<form method="post" data-ajax="true" data-reload="true">...</form>

<!-- 刪除按鈕加上 data 屬性 -->
<a href="#" class="btn btn-sm btn-danger" data-action="delete" data-id="<?= $msg['id'] ?>">刪除</a>

部署上線檢查清單

類別檢查項目狀態
安全性PHP 版本 ≥ 8.1(安全支援中)☐
關閉 display_errors、開啟 log_errors☐
Session Cookie:Secure、HttpOnly、SameSite=Lax☐
資料庫帳號最小權限、強密碼、非 root☐
上傳目錄禁用 PHP 執行(.htaccess / nginx location)☐
啟用 HTTPS、HSTS、CSP 標頭☐
效能OPcache 啟用、JIT 開啟(PHP 8+)☐
Composer 優化自動載入 composer dump-autoload -o☐
靜態資源 CDN、瀏覽器快取標頭☐
資料庫索引優化、慢查詢日誌分析☐
考慮 Redis 快取熱門留言、Session☐
維運錯誤日誌集中收集(Loki/ELK/Sentry)☐
資料庫備份策略(每日全備 + Binlog 增備)☐
健康檢查端點 /healthz☐
部署腳本自動化(GitHub Actions / GitLab CI / Deployer)☐
程式碼品質PHPStan Level 5+ 靜態分析通過☐
單元/功能測試覆蓋率 ≥ 80%☐
PSR-12 程式碼風格統一☐
相依套件無已知漏洞 composer audit☐
法規遵循隱私權政策、Cookie 同意條款☐
個資加密儲存、存取紀錄☐
定期滲透測試、漏洞掃描☐

Nginx + PHP-FPM 生產配置片段

# /etc/nginx/sites-available/guestbook
server {
    listen 80;
    server_name guestbook.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name guestbook.example.com;
    
    root /var/www/guestbook/public;
    index index.php;
    
    ssl_certificate /etc/letsencrypt/live/guestbook.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/guestbook.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    
    # 安全標頭
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header Referrer-Policy "strict-origin-when-cross-origin";
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' cdn.jsdelivr.net; img-src 'self' data:; font-src 'self' cdn.jsdelivr.net;";
    
    # 禁止上傳目錄執行 PHP
    location /uploads/ {
        location ~ \.php$ { deny all; }
    }
    
    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }
    
    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_hide_header X-Powered-By;
    }
    
    location ~ /\.(?!well-known) {
        deny all;
    }
}

專案完整結構

guestbook/
├── public/
│   ├── index.php           ✅ 留言列表、分頁
│   ├── add_message.php     ✅ 新增留言表單
│   ├── assets/
│   │   └── js/app.js       ✅ AJAX 模組、Toast、刪除/表單增強
│   └── .htaccess           # Apache 重寫規則
├── admin/
│   ├── login.php           ✅ 登入
│   ├── logout.php          ✅ 登出
│   ├── index.php           ✅ 管理列表、分頁
│   ├── reply.php           ✅ 回覆編輯
│   └── delete.php          ✅ 刪除(支援 AJAX/一般)
├── includes/
│   ├── common.php          ✅ 共用函數、DB、Session、CSRF
│   ├── db.php              ✅ PDO Singleton
│   └── admin_auth.php      ✅ 管理員中介層
├── config.php              ✅ 設定檔
├── composer.json
└── README.md

總結與延伸

本系列實作涵蓋:

  1. 資料庫設計與 PDO 封裝
  2. Session/CSRF/XSS/SQL Injection 核心防護
  3. CRUD 完整流程(建立、讀取、更新、刪除)
  4. 分頁、搜尋、閃存訊息、權限控制
  5. AJAX 非同步體驗優化
  6. 生產環境部署檢查清單與 Nginx 配置

後續可擴展方向:

  • 使用者註冊/登入、個人留言管理
  • 留言巢狀回覆(無限層級)
  • 圖片上傳、表情符號、Markdown 支援
  • 垃圾留言過濾(Akismet、關鍵字、機器學習)
  • 通知系統(Email、WebPush、WebSocket 即時)
  • API 版本化、OpenAPI 文件、第三方整合
  • 遷移至 Laravel/Symfony 享受完整生態系

從簡易留言板出發,掌握 PHP Web 開發核心技能,為進階框架學習打下堅實基礎。

copyright © 萬盛學電腦網 all rights reserved