本文目录导读:

我来详细介绍在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缓存方案。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播