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

jQuery + Ajax + PHP「讚/喜歡」評分系統:防重複、IP+Cookie 雙重驗證、Redis 計數器、即時更新與動畫效果

     編輯:PHP 綜合知識

建立高併發、防刷單、體驗流暢的「讚」系統:前端 jQuery 動畫回饋、Ajax 非同步請求、後端 PHP + Redis 原子計數、IP+Cookie 雙重去重、速率限制、CSRF 保護、數據一致性最終一致同步至 MySQL。

一、資料庫與 Redis 結構

-- MySQL 永久儲存
CREATE TABLE posts (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    title VARCHAR(200) NOT NULL,
    likes_count INT UNSIGNED DEFAULT 0,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

CREATE TABLE post_likes (
    id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    post_id BIGINT UNSIGNED NOT NULL,
    ip_hash CHAR(64) NOT NULL, -- SHA256(ip + secret)
    cookie_id CHAR(32) NOT NULL, -- 隨機 16 bytes hex
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uniq_like (post_id, ip_hash, cookie_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
// Redis 結構 (熱資料)
/*
like:count:{post_id}          -> STRING (計數器)
like:ips:{post_id}            -> SET (IP hash 集合)
like:cookies:{post_id}        -> SET (Cookie ID 集合)
like:lock:{post_id}           -> 鎖 (防止並發競態)
*/

二、前端頁面 (含 CSRF、動畫)

<article class="post" data-post-id="123">
  <h2>文章標題</h2>
  <div class="like-btn-wrap">
    <button class="like-btn" type="button" aria-pressed="false" aria-label="喜歡">
      <svg class="heart-icon" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
      <span class="count">0</span>
    &button>
  </div>
</article>

<script>
$(function() {
    $('.like-btn').on('click', function(e) {
        const $btn = $(this);
        const postId = $btn.closest('.post').data('post-id');
        if ($btn.hasClass('loading') || $btn.attr('aria-pressed') === 'true') return;
        
        $btn.addClass('loading').prop('disabled', true);
        $.ajax({
            url: '/api/like',
            method: 'POST',
            data: { post_id: postId, csrf_token: $('meta[name="csrf-token"]').attr('content') },
            dataType: 'json',
            success: function(res) {
                if (res.success) {
                    $btn.attr('aria-pressed', 'true').addClass('liked');
                    $btn.find('.count').text(res.count);
                    // 心跳動畫
                    $btn.find('.heart-icon').addClass('beat');
                    setTimeout(() => $btn.find('.heart-icon').removeClass('beat'), 600);
                } else {
                    alert(res.message || '操作失敗');
                }
            },
            error: function() { alert('網路錯誤,請重試'); },
            complete: function() { $btn.removeClass('loading').prop('disabled', false); }
        });
    });
});
</script>

<style>
.like-btn { display:inline-flex; align-items:center; gap:4px; background:#fff; border:1px solid #dee2e6; border-radius:999px; padding:6px 12px; cursor:pointer; transition:all .2s; }
.like-btn:hover { border-color:#0d6efd; color:#0d6efd; }
.like-btn.liked { background:#fff0f0; border-color:#dc3545; color:#dc3545; }
.like-btn .heart-icon { width:18px; height:18px; fill:none; stroke:currentColor; stroke-width:2; transition:transform .2s; }
.like-btn .heart-icon.beat { animation: beat .6s ease; }
@keyframes beat { 0%,100% { transform:scale(1); } 50% { transform:scale(1.3); } }
.like-btn.loading { opacity:.6; cursor:wait; }
</style>

三、後端 API (PHP + Redis 原子操作)

<?php
declare(strict_types=1);
// api/like.php
require __DIR__.'/../vendor/autoload.php';
use Predis\Client as Redis;

session_start();
header('Content-Type: application/json; charset=utf-8');

$redis = new Redis(['host'=>'127.0.0.1','port'=>6379]);
$pdo = new PDO('mysql:host=localhost;dbname=blog;charset=utf8mb4', 'user', 'pass', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

$postId = (int)($_POST['post_id'] ?? 0);
$csrf = $_POST['csrf_token'] ?? '';
if ($postId <= 0 || !hash_equals($_SESSION['csrf_token'] ?? '', $csrf)) {
    echo json_encode(['success'=>false, 'message'=>'無效請求']);
    exit;
}

// 速率限制:每 IP 每分鐘最多 30 次
$ipHash = hash('sha256', $_SERVER['REMOTE_ADDR'] . 'SALT_KEY');
$rateKey = "ratelike:ip:{$ipHash}";
$rate = $redis->incr($rateKey);
if ($rate === 1) $redis->expire($rateKey, 60);
if ($rate > 30) {
    echo json_encode(['success'=>false, 'message'=>'操作太頻繁,請稍後']);
    exit;
}

// Cookie ID (若無則發新)
$cookieId = $_COOKIE['like_id'] ?? bin2hex(random_bytes(16));
setcookie('like_id', $cookieId, time()+86400*365, '/', '', true, true); // Secure, HttpOnly

// Redis 分散式鎖 (防止同一用戶並發請求重複計數)
$lockKey = "like:lock:{$postId}:{$ipHash}:{$cookieId}";
$lock = $redis->set($lockKey, '1', 'NX', 'EX', 10);
if (!$lock) {
    echo json_encode(['success'=>false, 'message'=>'請勿重複點擊']);
    exit;
}

try {
    // 檢查是否已讚 (IP + Cookie 雙重)
    $ipKey = "like:ips:{$postId}";
    $cookieKey = "like:cookies:{$postId}";
    $already = $redis->sismember($ipKey, $ipHash) && $redis->sismember($cookieKey, $cookieId);
    
    if ($already) {
        // 已讚,返回當前計數
        $count = (int)$redis->get("like:count:{$postId}");
        echo json_encode(['success'=>true, 'count'=>$count, 'message'=>'已讚過']);
    } else {
        // 原子遞增
        $count = $redis->incr("like:count:{$postId}");
        $redis->sadd($ipKey, $ipHash);
        $redis->sadd($cookieKey, $cookieId);
        $redis->expire($ipKey, 86400*30);
        $redis->expire($cookieKey, 86400*30);
        
        // 非同步同步至 MySQL (可用佇列)
        // 這裡簡單演示同步寫入
        $pdo->prepare('UPDATE posts SET likes_count = ? WHERE id = ?')->execute([$count, $postId]);
        $pdo->prepare('INSERT IGNORE INTO post_likes (post_id, ip_hash, cookie_id) VALUES (?, ?, ?)')
            ->execute([$postId, $ipHash, $cookieId]);
        
        echo json_encode(['success'=>true, 'count'=>$count]);
    }
} finally {
    $redis->del($lockKey);
}
?>

四、資料一致性:定時同步 Redis -> MySQL

<?php
// cron/sync_likes.php (每分鐘跑一次)
$redis = new Redis(['host'=>'127.0.0.1']);
$pdo = new PDO('mysql:host=localhost;dbname=blog;charset=utf8mb4', 'user', 'pass');

$keys = $redis->keys('like:count:*');
foreach ($keys as $key) {
    $postId = (int)str_replace('like:count:', '', $key);
    $count = (int)$redis->get($key);
    $pdo->prepare('UPDATE posts SET likes_count = ? WHERE id = ?')->execute([$count, $postId]);
}
// 也可用 Redis Keyspace Notifications 觸發即時同步
?>

五、擴展:取消讚、顯示已讚狀態、多類型互動

  • 取消讚:前端送 `action=unlike`,後端 `DECR` + `SREM`,同步更新。
  • 頁面載入時顯示狀態:後端渲染時檢查 Redis Set 成員資格,或前端 Ajax 批次查詢 `/api/like/status?ids=1,2,3`。
  • 多類型:Key 改為 `like:{type}:{id}`,支援文章、評論、影片等。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved