首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

社區論壇 PHP & MySQL 實戰(二):工具函式庫、郵件通知與 Cookie 狀態管理

     編輯:PHP 綜合知識

前言

論壇系統的基礎建設模組,雖不直接面向使用者,卻是整個系統穩定運行的基石。本文整理了早期 BBS 專案中分散在各處的工具函式,現代化重構為可複用的服務類,涵蓋:時間格式化、回文計數、分頁導航、展開狀態 Cookie、郵件驗證、瀏覽數遞增、訂閱管理、郵件通知發送、推薦給好友等核心功能。

核心工具函式

1. 時間格式化 — 使用 DateTime 物件

function formatDateTime(string $datetime, string $format = 'm/d H:i'): string
{
    try {
        return (new DateTime($datetime))->format($format);
    } catch (Exception $e) {
        error_log("Invalid datetime: {$datetime}");
        return 'Invalid Date';
    }
}

2. 回文計數 — 避免 N+1 查詢

function getReplyCountMap(PDO $pdo, int $aid): array
{
    $stmt = $pdo->prepare("SELECT sid, COUNT(*) AS cnt FROM bbs_docs WHERE aid = :aid GROUP BY sid");
    $stmt->execute([':aid' => $aid]);
    return $stmt->fetchAll(PDO::FETCH_KEY_PAIR); // [sid => count]
}

// 單一查詢版 (若無批次需求)
function getReplyCount(PDO $pdo, int $sid): int
{
    $stmt = $pdo->prepare("SELECT COUNT(*) FROM bbs_docs WHERE sid = :sid");
    $stmt->execute([':sid' => $sid]);
    return (int)$stmt->fetchColumn();
}

3. 分頁導航物件化渲染

class Paginator
{
    public function __construct(
        private int $totalItems,
        private int $perPage = 20,
        private int $currentPage = 1,
        private string $baseUrl = '',
        private array $queryParams = []
    ) {
        $this->totalPages = (int)ceil($this->totalItems / $this->perPage);
        $this->currentPage = max(1, min($this->currentPage, max(1, $this->totalPages)));
    }

    public function render(): string
    {
        if ($this->totalPages <= 1) return '';

        $html = '<nav class="pagination" aria-label="分頁導航"><ul>';
        // 上一頁
        if ($this->currentPage > 1) {
            $html .= $this->link($this->currentPage - 1, '« 上一頁');
        }
        // 頁碼 (可加入省略號邏輯)
        for ($i = 1; $i <= $this->totalPages; $i++) {
            $html .= ($i === $this->currentPage)
                ? '<li class="active" aria-current="page"><span>' . $i . '</span></li>'
                : $this->link($i, (string)$i);
        }
        // 下一頁
        if ($this->currentPage < $this->totalPages) {
            $html .= $this->link($this->currentPage + 1, '下一頁 »');
        }
        $html .= '</ul></nav>';
        return $html;
    }

    private function link(int $page, string $text): string
    {
        $params = array_merge($this->queryParams, ['pgno' => $page]);
        $url = $this->baseUrl . '?' . http_build_query($params);
        return '<li><a href="' . htmlspecialchars($url) . '">' . $text . '</a></li>';
    }
}

4. 展開/收合狀態 — Cookie 管理

class ThreadExpandState
{
    private const COOKIE_NAME = 'BBS_OPEN_LIST';
    private const EXPIRE = 31536000; // 1 年

    public static function get(): bool
    {
        return isset($_COOKIE[self::COOKIE_NAME]) && $_COOKIE[self::COOKIE_NAME] === '1';
    }

    public static function set(bool $open, string $path = '/'): void
    {
        $value = $open ? '1' : '0';
        // Secure=true 僅限 HTTPS 環境;SameSite=Lax 平衡安全與體驗
        setcookie(self::COOKIE_NAME, $value, [
            'expires'  => time() + self::EXPIRE,
            'path'     => $path,
            'domain'   => '', // 當前域名
            'secure'   => isset($_SERVER['HTTPS']),
            'httponly' => true, // 防 XSS 竊取
            'samesite' => 'Lax',
        ]);
    }

    public static function toggle(string $redirectUrl): void
    {
        self::set(!self::get());
        header("Location: {$redirectUrl}");
        exit;
    }
}

郵件相關核心功能

1. 郵箱格式驗證 — 使用內建 filter_var

function isValidEmail(string $email): bool
{
    return (bool)filter_var($email, FILTER_VALIDATE_EMAIL);
}

2. 瀏覽數遞增 — 原子操作

function incrementViewCount(PDO $pdo, int $id, bool $isSubject): void
{
    $table = $isSubject ? 'bbs_subj' : 'bbs_docs';
    // 單一 SQL 原子遞增,避免競態條件
    $pdo->prepare("UPDATE {$table} SET views = views + 1 WHERE id = :id")
        ->execute([':id' => $id]);
}

3. 訂閱管理 (bbs_sml) — 訂閱/退訂

function toggleSubscription(PDO $pdo, int $sid, string $email, bool $subscribe): array
{
    if (!isValidEmail($email)) {
        return ['success' => false, 'msg' => 'Email 格式錯誤'];
    }

    $checkStmt = $pdo->prepare("SELECT 1 FROM bbs_sml WHERE sid = :sid AND email = :email");
    $checkStmt->execute([':sid' => $sid, ':email' => $email]);
    $exists = (bool)$checkStmt->fetchColumn();

    if ($subscribe) {
        if ($exists) return ['success' => false, 'msg' => '已訂閱過'];
        $pdo->prepare("INSERT INTO bbs_sml (sid, email) VALUES (:sid, :email)")
            ->execute([':sid' => $sid, ':email' => $email]);
        return ['success' => true, 'msg' => '訂閱成功'];
    } else {
        if (!$exists) return ['success' => false, 'msg' => '未訂閱過'];
        $pdo->prepare("DELETE FROM bbs_sml WHERE sid = :sid AND email = :email")
            ->execute([':sid' => $sid, ':email' => $email]);
        return ['success' => true, 'msg' => '退訂成功'];
    }
}

4. 發文後郵件通知 — 非同步佇列化建議

function sendNewPostNotifications(PDO $pdo, int $aid, int $sid, int $rid): void
{
    // 1. 取得內容摘要
    $table = ($sid === $rid) ? 'bbs_subj' : 'bbs_docs';
    $stmt = $pdo->prepare("SELECT poster, banner, body FROM {$table} WHERE id = :rid");
    $stmt->execute([':rid' => $rid]);
    $post = $stmt->fetch();
    if (!$post) return;

    $subject = "{$post['poster']} 發表新回覆";
    $message = "發文者: {$post['poster']}\n標題: {$post['banner']}\n內容: {$post['body']}\n\n";
    $message .= "查看完整內容: https://{$_SERVER['HTTP_HOST']}/view.php?aid={$aid}&sid={$sid}&rid={$rid}\n";

    // 2. 通知主題訂閱者
    $subStmt = $pdo->prepare("SELECT email FROM bbs_sml WHERE sid = :sid");
    $subStmt->execute([':sid' => $sid]);
    $emails = $subStmt->fetchAll(PDO::FETCH_COLUMN);

    // 3. 通知版主 (從 bbs_area 取得)
    $areaStmt = $pdo->prepare("SELECT email FROM bbs_area WHERE id = :aid");
    $areaStmt->execute([':aid' => $aid]);
    $masterEmail = $areaStmt->fetchColumn();
    if ($masterEmail) $emails[] = $masterEmail;

    // 4. 發送 (生產環境請推入佇列,如 Redis/RabbitMQ/Database Queue)
    foreach (array_unique($emails) as $email) {
        // 非同步範例:寫入 mail_queue 表
        $pdo->prepare("INSERT INTO mail_queue (to_email, subject, body, created_at) VALUES (?, ?, ?, NOW())")
            ->execute([$email, $subject, $message]);
        
        // 同步發送 (僅示意,會阻塞請求)
        // mail($email, $subject, $message, "From: BBS System <noreply@example.com>\r\nContent-Type: text/plain; charset=UTF-8");
    }
}

郵件發送最佳實踐:

  • **絕不**在 HTTP 請求中同步呼叫 mail() 或 SMTP,會嚴重拖慢回應且易超時。
  • 寫入 mail_queue 資料表,由獨立 Worker (cron / systemd / Supervisor 執行 PHP CLI) 處理發送、重試、退信記錄。
  • 使用成熟庫:symfony/mailer, phpmailer/phpmailer,支援 TLS、DKIM、SPF、模板引擎。
  • 內容採用 HTML + 純文字 multipart/alternative 格式。

5. 推薦給好友 — 記錄分享日誌

function shareToFriend(PDO $pdo, int $aid, int $sid, int $rid, string $fromEmail, string $toEmail): array
{
    if (!isValidEmail($toEmail)) return ['success' => false, 'msg' => '收件人 Email 格式錯誤'];

    $table = ($sid === $rid) ? 'bbs_subj' : 'bbs_docs';
    $stmt = $pdo->prepare("SELECT banner, body FROM {$table} WHERE id = :rid");
    $stmt->execute([':rid' => $rid]);
    $post = $stmt->fetch();
    if (!$post) return ['success' => false, 'msg' => '內容不存在'];

    $subject = "{$fromEmail} 推薦您閱讀: {$post['banner']}";
    $message = "您的好友 {$fromEmail} 推薦這篇文章:\n\n";
    $message .= "標題: {$post['banner']}\n內容: {$post['body']}\n\n";
    $message .= "連結: https://{$_SERVER['HTTP_HOST']}/view.php?aid={$aid}&sid={$sid}&rid={$rid}\n";

    // 寫入分享日誌 (供統計/防濫用)
    $pdo->prepare("INSERT INTO share_log (from_email, to_email, aid, sid, rid, created_at) VALUES (?, ?, ?, ?, ?, NOW())")
        ->execute([$fromEmail, $toEmail, $aid, $sid, $rid]);

    // 推入郵件佇列
    $pdo->prepare("INSERT INTO mail_queue (to_email, subject, body, created_at) VALUES (?, ?, ?, NOW())")
        ->execute([$toEmail, $subject, $message]);

    return ['success' => true, 'msg' => '推薦郵件已排程發送'];
}

小結

本文將散落在程式碼各處的基礎功能,重構為單一職責、可單元測試、採用現代 PHP 語法 (型別宣告、嚴格模式、DateTime、filter_var、PDO 參數綁定)的服務類與函式。關鍵架構決策:

  • 郵件非同步化: 寫入佇列表,Worker 處理,徹底解耦 HTTP 請求與發送耗時。
  • 原子操作: 瀏覽數遞增使用單一 SQL,避免競態。
  • Cookie 安全屬性: HttpOnly、Secure、SameSite=Lax。
  • 輸入驗證: 善用 filter_var、(int) 強制轉型。

這些基礎建設的現代化,能大幅降低維護成本、提升系統穩定度與安全性。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved