本文目录导读:

为 PHP 项目实现内容点赞统计,通常需要结合数据库来持久化存储数据,并辅以 AJAX 实现无刷新交互,为了防刷和用户体验,还需要加入用户身份验证和防重复点赞机制。
以下是实现该功能的标准流程和代码示例。
核心设计思路
- 数据结构:需要两张核心表(
post或article表用于存总数,like表用于存明细)。 - 防重复:记录“谁”(用户IP 或 用户ID)对“什么内容”点赞过。
- 状态切换:通常设计为“点赞/取消点赞”(Toggle)。
- 前端交互:使用 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">♡</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 = '❤'; // 实心心形
} else {
// 切换为未点赞状态
that.classList.remove('liked');
heartSpan.innerHTML = '♡'; // 空心心形
}
} 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 ? '❤' : '♡') . '</span>';
echo '<span class="count">' . $likeCount . '</span> 赞';
echo '</button>';
性能与防刷优化建议
-
频率限制(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();
-
使用缓存:
- 对于显示点赞数,可以使用 Memcached 或 Redis 缓存
like_count,定时同步到 MySQL。 - 使用
Redis SADD存储用户ID集合来判断是否点赞,速度极快。
- 对于显示点赞数,可以使用 Memcached 或 Redis 缓存
-
异步处理:
对于高并发场景,点赞请求可以写入消息队列(如 RabbitMQ),然后由后台进程批量更新数据库。
-
安全:
- 校验
target_id是否真实存在,防止恶意刷无效ID。 - 使用 CSRF Token 防止跨站请求伪造(如果涉及登录态)。
- 校验
点赞统计的核心:
- 数据库设计:
details(谁在哪点赞)+sum(总数,可冗余)。 - 并发控制:
UNIQUE KEY+原子 UPDATE。 - 交互:
JavaScript Fetch+PHP 接口+Toggle 逻辑。 - 状态持久化:页面加载时从后端获取当前用户的点赞状态。