本教學專為 PHP 初學者設計,逐步建立一個功能完整的留言板系統,包含:資料庫設計、留言列表顯示、新增留言、編輯留言、刪除留言、分頁、基本安全防護。程式碼採用程序式寫法,易於理解與修改。
| 檔案 | 功能 | 對應操作 |
|---|---|---|
| common.php | 資料庫連線共用檔 | PDO 連線、工具函數 |
| index.php | 留言列表首頁 | READ (分頁、倒序) |
| add.php | 新增留言表單/處理 | CREATE |
| edit.php | 編輯留言表單/處理 | UPDATE |
| delete.php | 刪除留言處理 | DELETE |
| style.css | 樣式表 | 基本排版美化 |
CREATE DATABASE `guestbook` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE `guestbook`; CREATE TABLE `gb_content` ( `id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL COMMENT '留言者姓名', `email` VARCHAR(100) DEFAULT NULL COMMENT 'Email (可選)', `content` TEXT NOT NULL COMMENT '留言內容', `ip` VARCHAR(45) DEFAULT NULL COMMENT '留言者 IP', `user_agent` VARCHAR(255) DEFAULT NULL COMMENT '瀏覽器資訊', `insert_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '留言時間', `update_time` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '最後編輯時間', `is_deleted` TINYINT(1) NOT NULL DEFAULT '0' COMMENT '軟刪除標記', PRIMARY KEY (`id`), KEY `idx_insert_time` (`insert_time`), KEY `idx_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='留言板內容表';
PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES " . DB_CHARSET, ]; try { $pdo = new PDO($dsn, DB_USER, DB_PASS, $options); } catch (PDOException $e) { // 正式環境請記錄日誌而非直接輸出 die("資料庫連線失敗: " . $e->getMessage()); } } return $pdo; } // 安全輸出函數 function h($str) { return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); } // 重導向函數 function redirect($url, $msg = '') { if ($msg) { session_start(); $_SESSION['flash_msg'] = $msg; } header("Location: $url"); exit; } // 取得閃存訊息 function getFlash() { session_start(); $msg = $_SESSION['flash_msg'] ?? ''; unset($_SESSION['flash_msg']); return $msg; } // 簡易 CSRF Token function csrfToken() { session_start(); if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf_token']; } function verifyCsrf($token) { session_start(); return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token); }
query("SELECT COUNT(*) FROM gb_content WHERE is_deleted = 0"); $total = $stmt->fetchColumn(); $totalPages = ceil($total / $perPage); // 取得當頁資料 $stmt = $pdo->prepare(" SELECT id, username, content, insert_time FROM gb_content WHERE is_deleted = 0 ORDER BY insert_time DESC LIMIT :offset, :limit "); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->bindValue(':limit', $perPage, PDO::PARAM_INT); $stmt->execute(); $messages = $stmt->fetchAll(); $flash = getFlash(); ?>留言板系統
50) { $errors[] = '姓名長度需 1-50 字元'; } if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) { $errors[] = 'Email 格式不正確'; } if (mb_strlen($content) < 1 || mb_strlen($content) > 2000) { $errors[] = '留言內容長度需 1-2000 字元'; } if (empty($errors)) { $stmt = $pdo->prepare(" INSERT INTO gb_content (username, email, content, ip, user_agent) VALUES (?, ?, ?, ?, ?) "); $stmt->execute([$username, $email, $content, $ip, $ua]); redirect('index.php', '留言發表成功!'); } } ?>發表留言
prepare("SELECT * FROM gb_content WHERE id = ? AND is_deleted = 0"); $stmt->execute([$id]); $msg = $stmt->fetch(); if (!$msg) redirect('index.php', '留言不存在或已刪除'); if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!verifyCsrf($_POST['csrf_token'] ?? '')) die('CSRF 驗證失敗'); $username = trim($_POST['username'] ?? ''); $email = trim($_POST['email'] ?? ''); $content = trim($_POST['content'] ?? ''); $errors = []; if (mb_strlen($username) < 1 || mb_strlen($username) > 50) $errors[] = '姓名長度需 1-50 字元'; if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'Email 格式不正確'; if (mb_strlen($content) < 1 || mb_strlen($content) > 2000) $errors[] = '留言內容長度需 1-2000 字元'; if (empty($errors)) { $stmt = $pdo->prepare("UPDATE gb_content SET username=?, email=?, content=? WHERE id=?"); $stmt->execute([$username, $email, $content, $id]); redirect('index.php', '留言更新成功!'); } } ?>編輯留言
prepare("SELECT username, content FROM gb_content WHERE id = ? AND is_deleted = 0"); $stmt->execute([$id]); $msg = $stmt->fetch(); if (!$msg) redirect('index.php', '留言不存在'); // 顯示確認頁面 echo "確認刪除 "; echo ""; exit; } // POST 執行軟刪除 $stmt = $pdo->prepare("UPDATE gb_content SET is_deleted = 1 WHERE id = ?"); $stmt->execute([$id]); if ($stmt->rowCount() > 0) { redirect('index.php', '留言已刪除'); } else { redirect('index.php', '留言不存在或已刪除'); }確認刪除
"; echo "確定要刪除以下留言嗎?此操作可透過資料庫手動復原。
"; echo ""; echo "
/* style.css */ * { box-sizing: border-box; } body { font-family: 'Microsoft JhengHei', sans-serif; line-height: 1.6; color: #333; background: #f5f5f5; margin: 0; padding: 20px; } .container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } h1 { margin-top: 0; color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; } .btn { display: inline-block; padding: 8px 16px; border-radius: 4px; text-decoration: none; font-size: 14px; cursor: pointer; border: none; margin-right: 5px; } .btn.primary { background: #3498db; color: white; } .btn.danger { background: #e74c3c; color: white; } .btn:hover { opacity: 0.9; } .form-group { margin-bottom: 20px; } label { display: block; margin-bottom: 5px; font-weight: bold; } .required { color: #e74c3c; } input[type=text], input[type=email], textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-family: inherit; font-size: 14px; } textarea { resize: vertical; } small { color: #999; font-size: 12px; } .message { border: 1px solid #eee; border-radius: 4px; padding: 20px; margin-bottom: 20px; background: #fafafa; } .message-header { margin-bottom: 10px; color: #666; font-size: 14px; } .message-header strong { color: #2c3e50; font-size: 16px; margin-right: 15px; } .message-content { white-space: pre-wrap; word-wrap: break-word; } .message-actions a { margin-right: 15px; color: #3498db; text-decoration: none; } .message-actions a:hover { text-decoration: underline; } .pagination { text-align: center; margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; } .pagination a { padding: 8px 16px; background: #3498db; color: white; border-radius: 4px; text-decoration: none; margin: 0 5px; } .pagination .page-info { color: #999; margin: 0 15px; } .alert { padding: 15px; border-radius: 4px; margin-bottom: 20px; } .alert.success { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; } .alert.error { background: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; } .empty { text-align: center; color: #999; padding: 40px; }
htmlspecialchars() 防止 XSSis_deleted) 而非硬刪除