PHP项目怎样实现内容点赞统计?

wen PHP项目 13

本文目录导读:

PHP项目怎样实现内容点赞统计?

  1. 核心设计思路
  2. 第一步:数据库设计 (MySQL)
  3. 第二步:后端 PHP 实现(核心逻辑)
  4. 第三步:前端 HTML + JavaScript 实现
  5. 第四步:显示初始状态(重要)
  6. 性能与防刷优化建议

为 PHP 项目实现内容点赞统计,通常需要结合数据库来持久化存储数据,并辅以 AJAX 实现无刷新交互,为了防刷和用户体验,还需要加入用户身份验证防重复点赞机制。

以下是实现该功能的标准流程和代码示例。

核心设计思路

  1. 数据结构:需要两张核心表(postarticle 表用于存总数,like 表用于存明细)。
  2. 防重复:记录“谁”(用户IP 或 用户ID)对“什么内容”点赞过。
  3. 状态切换:通常设计为“点赞/取消点赞”(Toggle)。
  4. 前端交互:使用 JavaScript (Fetch/AJAX) 异步请求 PHP 接口。

第一步:数据库设计 (MySQL)

方案 A:单表存总数 + 明细表(推荐,防止并发问题)

表1:posts (文章/内容表)

CREATE TABLE `posts` (
  `id` INT PRIMARY KEY AUTO_INCREMENT, VARCHAR(255) NOT NULL,
  `content` TEXT,
  `like_count` INT DEFAULT 0  -- 冗余字段:点赞总数,避免每次都COUNT
);

表2:likes (点赞记录表)

CREATE TABLE `likes` (
  `id` INT PRIMARY KEY AUTO_INCREMENT,
  `target_type` VARCHAR(20) NOT NULL,  -- 标识类型:'post', 'comment', 'article'
  `target_id` INT NOT NULL,            -- 对应内容的ID
  `user_id` INT NOT NULL,              -- 对应用户表ID(如果是未登录用户可以用IP/DeviceID)
  `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
   -- 关键:防止同一个人对同一个内容多次点赞
  UNIQUE KEY `uk_user_target` (`target_type`, `target_id`, `user_id`)
);

方案 B:只用一张表(小规模项目)

如果不想维护冗余字段,每次显示时都用 SELECT COUNT(*) FROM likes WHERE target_type='post' AND target_id = ?,优点是简单,缺点是高并发时数据库压力大。


第二步:后端 PHP 实现(核心逻辑)

创建一个 like_api.php 文件处理 AJAX 请求。

<?php
// like_api.php
session_start();
header('Content-Type: application/json');
// 1. 用户登录检测(简化示例,实际项目中请使用完善的认证)
$userId = $_SESSION['user_id'] ?? 0;
if (!$userId) {
    echo json_encode(['code' => 0, 'msg' => '请先登录']);
    exit;
}
// 2. 获取前端参数
$targetId = intval($_POST['target_id'] ?? 0);
$targetType = 'post'; // 固定为文章类型,可根据项目扩展
if ($targetId <= 0) {
    echo json_encode(['code' => 0, 'msg' => '参数错误']);
    exit;
}
// 3. 数据库连接(请替换为自己的配置)
$pdo = new PDO('mysql:host=localhost;dbname=test;charset=utf8mb4', 'root', 'password');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
try {
    // 4. 尝试插入点赞记录 (这一步就包含了防重复逻辑,因为 UNIQUE KEY)
    $stmt = $pdo->prepare(
        "INSERT IGNORE INTO likes (target_type, target_id, user_id, created_at) 
         VALUES (:type, :id, :uid, NOW())"
    );
    $stmt->execute([
        ':type' => $targetType,
        ':id'   => $targetId,
        ':uid'  => $userId
    ]);
    $insertedRows = $stmt->rowCount();
    if ($insertedRows > 0) {
        // 5. 点赞成功,更新帖子总计数 (使用原子操作防止并发)
        $pdo->prepare(
            "UPDATE posts SET like_count = like_count + 1 WHERE id = :id"
        )->execute([':id' => $targetId]);
        $action = 'liked';
    } else {
        // 6. 如果返回0行,说明已经点过赞了,执行取消点赞操作
        $pdo->prepare(
            "DELETE FROM likes WHERE target_type = :type AND target_id = :id AND user_id = :uid"
        )->execute([
            ':type' => $targetType,
            ':id'   => $targetId,
            ':uid'  => $userId
        ]);
        // 更新帖子总计数 (减1)
        $pdo->prepare(
            "UPDATE posts SET like_count = like_count - 1 WHERE id = :id AND like_count > 0"
        )->execute([':id' => $targetId]);
        $action = 'unliked';
    }
    // 7. 获取最新点赞数
    $stmt = $pdo->prepare("SELECT like_count FROM posts WHERE id = :id");
    $stmt->execute([':id' => $targetId]);
    $likeCount = $stmt->fetchColumn();
    // 8. 返回结果给前端
    echo json_encode([
        'code' => 1,
        'action' => $action,
        'like_count' => $likeCount
    ]);
} catch (PDOException $e) {
    // 记录错误日志
    error_log($e->getMessage());
    echo json_encode(['code' => 0, 'msg' => '服务器繁忙,请稍后重试']);
}

关键点解释:

  • INSERT IGNORE:UNIQUE KEY 冲突,不会报错,而是返回影响行数为 0,这是区分“点赞”和“取消点赞”的巧妙方法。
  • UPDATE ... like_count + 1:使用数据库的原子操作,避免在高并发下读取+写入的竞态条件(Race Condition)。
  • AND like_count > 0:防止因为并发错误导致点赞数变成负数。

第三步:前端 HTML + JavaScript 实现

<!DOCTYPE html>
<html>
<head>点赞示例</title>
    <style>
        .like-btn {
            cursor: pointer;
            display: inline-flex;
            align-items: center;
            padding: 8px 16px;
            background: #f0f0f0;
            border: 1px solid #ddd;
            border-radius: 20px;
            transition: all 0.2s;
        }
        .like-btn.liked {
            background: #ffeef0;
            border-color: #ff6b81;
            color: #ff4757;
        }
        .like-btn .heart {
            margin-right: 5px;
        }
    </style>
</head>
<body>
<div class="post" data-post-id="1">
    <h2>这是一篇优秀的文章</h2>
    <p>内容内容内容...</p>
    <!-- 点赞按钮 -->
    <button class="like-btn" data-target-id="1">
        <span class="heart">&#9825;</span> <!-- 空心心形 -->
        <span class="count">0</span> 赞
    </button>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
    // 获取所有点赞按钮(项目中可以改为更具体的选择器)
    document.querySelectorAll('.like-btn').forEach(btn => {
        btn.addEventListener('click', function() {
            const targetId = this.dataset.targetId;
            const countSpan = this.querySelector('.count');
            const heartSpan = this.querySelector('.heart');
            const that = this;
            // 发送AJAX请求
            fetch('like_api.php', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                },
                body: `target_id=${targetId}`
            })
            .then(response => response.json())
            .then(data => {
                if (data.code === 1) {
                    // 更新显示
                    countSpan.textContent = data.like_count;
                    if (data.action === 'liked') {
                        // 切换为高亮状态
                        that.classList.add('liked');
                        heartSpan.innerHTML = '&#10084;'; // 实心心形
                    } else {
                        // 切换为未点赞状态
                        that.classList.remove('liked');
                        heartSpan.innerHTML = '&#9825;'; // 空心心形
                    }
                } else {
                    alert(data.msg); // 显示错误信息(如未登录)
                }
            })
            .catch(error => {
                console.error('点赞失败:', error);
                alert('网络错误,请稍后重试');
            });
        });
    });
});
</script>
</body>
</html>

第四步:显示初始状态(重要)

当页面加载时,你需要告诉前端当前用户是否已经点过赞,以及当前的点赞数。

方法: 修改你的文章模板,在渲染时查询 likes 表。

// 假设在文章列表渲染时
$userId = $_SESSION['user_id'] ?? 0;
$postId = 1; // 循环中的每个文章ID
// 查询点赞数
$stmt = $pdo->prepare("SELECT like_count FROM posts WHERE id = ?");
$stmt->execute([$postId]);
$likeCount = $stmt->fetchColumn();
// 查询当前用户是否点过赞
$stmt = $pdo->prepare("SELECT COUNT(*) FROM likes WHERE target_type='post' AND target_id = ? AND user_id = ?");
$stmt->execute([$postId, $userId]);
$hasLiked = $stmt->fetchColumn() > 0;
// 输出到HTML
echo '<button class="like-btn ' . ($hasLiked ? 'liked' : '') . '" data-target-id="' . $postId . '">';
echo '<span class="heart">' . ($hasLiked ? '&#10084;' : '&#9825;') . '</span>';
echo '<span class="count">' . $likeCount . '</span> 赞';
echo '</button>';

性能与防刷优化建议

  1. 频率限制(Rate Limiting)

    • 在 PHP 端使用 Session 或 Redis 记录用户操作时间戳。
    • 同一用户每秒只能点赞一次。
      // 简单示例
      $lastLikeTime = $_SESSION['last_like_time'] ?? 0;
      if (time() - $lastLikeTime < 1) { // 限制1秒
        echo json_encode(['code' => 0, 'msg' => '操作过于频繁']);
        exit;
      }
      $_SESSION['last_like_time'] = time();
  2. 使用缓存

    • 对于显示点赞数,可以使用 Memcached 或 Redis 缓存 like_count,定时同步到 MySQL。
    • 使用 Redis SADD 存储用户ID集合来判断是否点赞,速度极快。
  3. 异步处理

    对于高并发场景,点赞请求可以写入消息队列(如 RabbitMQ),然后由后台进程批量更新数据库。

  4. 安全

    • 校验 target_id 是否真实存在,防止恶意刷无效ID。
    • 使用 CSRF Token 防止跨站请求伪造(如果涉及登录态)。

点赞统计的核心:

  1. 数据库设计details(谁在哪点赞)+ sum(总数,可冗余)。
  2. 并发控制UNIQUE KEY + 原子 UPDATE
  3. 交互JavaScript Fetch + PHP 接口 + Toggle 逻辑
  4. 状态持久化:页面加载时从后端获取当前用户的点赞状态。

抱歉,评论功能暂时关闭!