本文目录导读:

我来详细介绍在PHP项目中实现资讯点赞功能的几种方案,从简单到完善逐步展开。
基础功能设计
数据库表结构
-- 资讯文章表
CREATE TABLE `articles` (
`id` int(11) NOT NULL AUTO_INCREMENT, varchar(200) NOT NULL,
`content` text,
`like_count` int(11) DEFAULT '0',
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 点赞记录表
CREATE TABLE `article_likes` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`article_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
`created_at` timestamp DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_article_user` (`article_id`, `user_id`),
KEY `idx_article_id` (`article_id`),
KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
PHP后端代码
<?php
class LikeService {
private $db;
public function __construct($db) {
$this->db = $db;
}
/**
* 点赞/取消点赞
*/
public function toggleLike($articleId, $userId) {
try {
$this->db->beginTransaction();
// 检查是否已点赞
$sql = "SELECT id FROM article_likes
WHERE article_id = ? AND user_id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$articleId, $userId]);
$existing = $stmt->fetch();
if ($existing) {
// 已点赞,取消点赞
$sql = "DELETE FROM article_likes WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$existing['id']]);
// 减少点赞数
$sql = "UPDATE articles SET like_count = like_count - 1
WHERE id = ? AND like_count > 0";
$stmt = $this->db->prepare($sql);
$stmt->execute([$articleId]);
$liked = false;
} else {
// 未点赞,添加点赞
$sql = "INSERT INTO article_likes (article_id, user_id) VALUES (?, ?)";
$stmt = $this->db->prepare($sql);
$stmt->execute([$articleId, $userId]);
// 增加点赞数
$sql = "UPDATE articles SET like_count = like_count + 1 WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$articleId]);
$liked = true;
}
// 获取最新点赞数
$sql = "SELECT like_count FROM articles WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$articleId]);
$likeCount = $stmt->fetchColumn();
$this->db->commit();
return [
'success' => true,
'liked' => $liked,
'like_count' => $likeCount
];
} catch (Exception $e) {
$this->db->rollBack();
return [
'success' => false,
'message' => '操作失败'
];
}
}
/**
* 获取用户点赞状态
*/
public function getUserLikeStatus($articleId, $userId) {
$sql = "SELECT id FROM article_likes
WHERE article_id = ? AND user_id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$articleId, $userId]);
return $stmt->fetch() ? true : false;
}
}
AJAX前端实现
// 点赞功能
class LikeManager {
constructor() {
this.init();
}
init() {
document.querySelectorAll('.like-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
this.handleLike(e.target);
});
});
}
async handleLike(button) {
const articleId = button.dataset.articleId;
const userId = button.dataset.userId;
try {
button.disabled = true;
const response = await fetch('/api/like/toggle', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': this.getCsrfToken()
},
body: JSON.stringify({
article_id: articleId,
user_id: userId
})
});
const result = await response.json();
if (result.success) {
// 更新UI
this.updateUI(button, result);
}
} catch (error) {
console.error('点赞失败:', error);
alert('操作失败,请重试');
} finally {
button.disabled = false;
}
}
updateUI(button, data) {
// 更新点赞状态
button.classList.toggle('liked', data.liked);
// 更新点赞数
const countElement = button.querySelector('.like-count');
if (countElement) {
countElement.textContent = data.like_count;
}
// 更新按钮文本
const textElement = button.querySelector('.like-text');
if (textElement) {
textElement.textContent = data.liked ? '已赞' : '点赞';
}
}
getCsrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.content || '';
}
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
new LikeManager();
});
性能优化方案
Redis缓存实现
<?php
class LikeServiceWithCache {
private $redis;
private $db;
private $cacheKey = 'article:like:';
public function __construct($redis, $db) {
$this->redis = $redis;
$this->db = $db;
}
/**
* 异步点赞(Redis + 定时写入DB)
*/
public function asyncLike($articleId, $userId) {
$likeKey = $this->cacheKey . $articleId;
$userKey = $likeKey . ':users';
// 使用Redis Set存储已点赞用户
$isLiked = $this->redis->sIsMember($userKey, $userId);
if ($isLiked) {
// 取消点赞
$this->redis->sRem($userKey, $userId);
$this->redis->decr($likeKey);
} else {
// 点赞
$this->redis->sAdd($userKey, $userId);
$this->redis->incr($likeKey);
}
// 记录同步任务
$this->addSyncTask($articleId, $userId, !$isLiked);
return [
'liked' => !$isLiked,
'like_count' => $this->redis->get($likeKey)
];
}
/**
* 批量同步到数据库
*/
public function syncToDatabase() {
// 获取待同步的点赞数据
$tasks = $this->redis->lRange('like:sync:queue', 0, -1);
foreach ($tasks as $task) {
$data = json_decode($task, true);
try {
$this->db->beginTransaction();
if ($data['action'] === 'add') {
$sql = "INSERT INTO article_likes (article_id, user_id)
VALUES (?, ?)
ON DUPLICATE KEY UPDATE created_at = NOW()";
} else {
$sql = "DELETE FROM article_likes
WHERE article_id = ? AND user_id = ?";
}
$stmt = $this->db->prepare($sql);
$stmt->execute([$data['article_id'], $data['user_id']]);
// 更新文章点赞计数
$sql = "UPDATE articles SET like_count = (
SELECT COUNT(*) FROM article_likes WHERE article_id = ?
) WHERE id = ?";
$stmt = $this->db->prepare($sql);
$stmt->execute([$data['article_id'], $data['article_id']]);
$this->db->commit();
// 移除已完成的任务
$this->redis->lPop('like:sync:queue');
} catch (Exception $e) {
$this->db->rollBack();
// 记录失败日志
error_log("Like sync failed: " . $e->getMessage());
}
}
}
private function addSyncTask($articleId, $userId, $isLike) {
$task = json_encode([
'article_id' => $articleId,
'user_id' => $userId,
'action' => $isLike ? 'add' : 'remove',
'timestamp' => time()
]);
$this->redis->rPush('like:sync:queue', $task);
}
}
防重复点击处理
// 防重复点击
class LikeButton {
constructor(button) {
this.button = button;
this.isProcessing = false;
this.debounceTimer = null;
}
handleClick() {
if (this.isProcessing) return;
// 防抖处理,300ms内只处理一次
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => {
this.isProcessing = true;
this.sendLikeRequest();
}, 300);
}
async sendLikeRequest() {
try {
// 发送请求
const response = await fetch(this.button.dataset.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
article_id: this.button.dataset.articleId
})
});
const result = await response.json();
this.updateState(result);
} catch (error) {
console.error('点赞失败:', error);
// 恢复状态
this.button.classList.remove('liked');
} finally {
this.isProcessing = false;
}
}
}
SQL优化
-- 创建联合索引
CREATE INDEX idx_like_status ON article_likes (article_id, user_id);
-- 使用EXISTS优化查询
SELECT EXISTS(
SELECT 1 FROM article_likes
WHERE article_id = ? AND user_id = ?
) AS is_liked;
-- 批量更新点赞数
UPDATE articles a
SET a.like_count = (
SELECT COUNT(*) FROM article_likes l
WHERE l.article_id = a.id
)
WHERE a.id IN (1, 2, 3); -- 指定需要更新的文章
安全性考虑
CSRF防护
<?php
// 生成CSRF Token
session_start();
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// 验证CSRF Token
function validateCsrfToken($token) {
if (!isset($_SESSION['csrf_token']) || $token !== $_SESSION['csrf_token']) {
http_response_code(403);
die('CSRF token validation failed');
}
return true;
}
请求频率限制
<?php
class RateLimiter {
private $redis;
private $limit = 10; // 每分钟最多10次
private $window = 60; // 时间窗口(秒)
public function __construct($redis) {
$this->redis = $redis;
}
public function checkLimit($userId, $action = 'like') {
$key = "rate_limit:{$action}:{$userId}";
$count = $this->redis->incr($key);
if ($count === 1) {
$this->redis->expire($key, $this->window);
}
return $count <= $this->limit;
}
}
输入验证
<?php
// 参数验证
function validateLikeRequest($articleId, $userId) {
// 验证ID是否为正整数
if (!filter_var($articleId, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]])) {
throw new InvalidArgumentException('无效的文章ID');
}
if (!filter_var($userId, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]])) {
throw new InvalidArgumentException('无效的用户ID');
}
// 检查文章是否存在
$article = getArticle($articleId);
if (!$article) {
throw new Exception('文章不存在');
}
return true;
}
完整示例:点赞API接口
<?php
// /api/like/toggle.php
header('Content-Type: application/json');
require_once 'config.php';
require_once 'LikeService.php';
// 验证登录状态
session_start();
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['error' => '请先登录']);
exit;
}
// 验证CSRF Token
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
try {
// 验证输入
$articleId = filter_var($input['article_id'], FILTER_VALIDATE_INT);
$userId = $_SESSION['user_id'];
if (!$articleId) {
throw new Exception('参数错误');
}
// 频率限制检查
$rateLimiter = new RateLimiter($redis);
if (!$rateLimiter->checkLimit($userId)) {
throw new Exception('操作太频繁,请稍后再试');
}
// 执行点赞操作
$likeService = new LikeService($db);
$result = $likeService->toggleLike($articleId, $userId);
echo json_encode($result);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => $e->getMessage()
]);
}
}
前端UI示例
<!-- 点赞按钮HTML -->
<button class="like-btn <?= $isLiked ? 'liked' : '' ?>"
data-article-id="<?= $article['id'] ?>"
data-user-id="<?= $_SESSION['user_id'] ?>">
<span class="like-icon">
<svg viewBox="0 0 24 24" width="16" height="16">
<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.09
C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5
c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/>
</svg>
</span>
<span class="like-text"><?= $isLiked ? '已赞' : '点赞' ?></span>
<span class="like-count"><?= $article['like_count'] ?></span>
</button>
<style>
.like-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 20px;
background: #fff;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
}
.like-btn:hover {
background: #f0f0f0;
}
.like-btn.liked {
background: #ff6b6b;
color: #fff;
border-color: #ff6b6b;
}
.like-btn.liked .like-icon svg {
fill: #fff;
}
.like-icon svg {
fill: #666;
transition: fill 0.3s ease;
}
.like-btn .like-count {
font-weight: 600;
}
/* 点赞动画 */
.like-btn.liked .like-icon {
animation: likeAnim 0.3s ease;
}
@keyframes likeAnim {
0% { transform: scale(1); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
</style>
这个实现方案涵盖了:
- 基础功能:点赞/取消点赞
- 性能优化:Redis缓存、异步同步
- 安全性:CSRF防护、频率限制、输入验证
- 用户体验:防抖处理、动画效果
根据项目规模选择合适的实现方式,小型项目直接用数据库即可,高并发场景建议使用Redis缓存方案。