本文目录导读:

这是一个很好的问题!在PHP项目中实现用户自定义评分(比如用户可以对文章、商品进行1-5星评分),通常涉及前端展示、后端逻辑、数据库存储三个部分。
下面是一个从基础到进阶的完整实现方案,包含代码示例和安全考虑。
核心流程
- 用户前端:点击星星,发送Ajax请求。
- 后端接收:验证用户登录、防止重复评分、存储数据。
- 数据库更新:更新评分表,并同步更新主表的平均分/评分人数。
- 前端反馈:显示更新后的分数和用户评分状态。
第一步:数据库设计
你需要两张主要的表(或者一张表加缓存字段)。
评分记录表 (ratings)
存储每个用户对每个目标的评分。
CREATE TABLE `ratings` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '用户ID', `target_id` int(11) NOT NULL COMMENT '被评分对象ID ( article_id, product_id)', `target_type` varchar(50) NOT NULL DEFAULT 'article' COMMENT '评分对象类型', `score` tinyint(1) NOT NULL COMMENT '评分值 1-5', `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `unique_rating` (`user_id`, `target_id`, `target_type`), -- 关键:防止同一用户多次评分 KEY `target_score` (`target_id`, `target_type`, `score`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
被评分对象表(articles)
在主表中增加冗余字段,避免每次查询都去ratings表做SUM/COUNT,提高性能。
ALTER TABLE articles ADD COLUMN `avg_score` decimal(2,1) NOT NULL DEFAULT '0.0' COMMENT '平均分', ADD COLUMN `score_count` int(11) NOT NULL DEFAULT '0' COMMENT '评分人数';
第二步:后端PHP逻辑 (API接口)
这里假设你有一个用户认证系统,$_SESSION['user_id'] 代表当前登录用户。
文件:rate.php
<?php
session_start();
header('Content-Type: application/json');
// 1. 权限检查
if (!isset($_SESSION['user_id'])) {
echo json_encode(['success' => false, 'message' => '请先登录']);
exit;
}
$userId = (int)$_SESSION['user_id'];
$targetId = (int)$_POST['target_id'];
$targetType = $_POST['target_type'] ?? 'article'; // 白名单校验
$score = (int)$_POST['score'];
// 2. 输入校验
if ($score < 1 || $score > 5) {
echo json_encode(['success' => false, 'message' => '评分值必须在1-5之间']);
exit;
}
$allowedTypes = ['article', 'product', 'video']; // 白名单
if (!in_array($targetType, $allowedTypes)) {
echo json_encode(['success' => false, 'message' => '无效的评分类型']);
exit;
}
// 3. 数据库操作 (PDO示例)
try {
$pdo = new PDO('mysql:host=localhost;dbname=yourdb;charset=utf8mb4', 'username', 'password');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->beginTransaction();
// 3.1 插入或更新评分 (使用 ON DUPLICATE KEY UPDATE 实现“修改评分”)
$sql = "INSERT INTO ratings (user_id, target_id, target_type, score)
VALUES (:user_id, :target_id, :target_type, :score)
ON DUPLICATE KEY UPDATE score = VALUES(score)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':user_id' => $userId,
':target_id' => $targetId,
':target_type' => $targetType,
':score' => $score
]);
// 3.2 更新主表的平均分和评分人数
// 计算此对象的平均分
$avgSql = "SELECT AVG(score) as avg_score, COUNT(*) as score_count
FROM ratings
WHERE target_id = :target_id AND target_type = :target_type";
$avgStmt = $pdo->prepare($avgSql);
$avgStmt->execute([':target_id' => $targetId, ':target_type' => $targetType]);
$result = $avgStmt->fetch(PDO::FETCH_ASSOC);
$avgScore = round($result['avg_score'], 1);
$count = (int)$result['score_count'];
// 动态更新主表 (根据目标类型选择不同表)
$updateSql = "UPDATE articles
SET avg_score = :avg_score, score_count = :score_count
WHERE id = :target_id";
$updateStmt = $pdo->prepare($updateSql);
$updateStmt->execute([
':avg_score' => $avgScore,
':score_count' => $count,
':target_id' => $targetId
]);
$pdo->commit();
echo json_encode([
'success' => true,
'new_avg' => $avgScore,
'new_count' => $count,
'your_score' => $score
]);
} catch (PDOException $e) {
$pdo->rollBack();
// 实际项目应记录日志,不暴露给用户
echo json_encode(['success' => false, 'message' => '评分失败,请稍后重试']);
}
第三步:前端实现 (HTML + JavaScript)
使用CSS星星和Ajax请求。
HTML结构:
<div class="rating-widget" data-target-id="123" data-target-type="article">
<!-- 显示当前平均分 -->
<div class="rating-average">
当前评分:<span class="avg-score">0.0</span> (<span class="score-count">0</span>人)
</div>
<!-- 星星区域 -->
<div class="stars">
<span class="star" data-value="1">☆</span>
<span class="star" data-value="2">☆</span>
<span class="star" data-value="3">☆</span>
<span class="star" data-value="4">☆</span>
<span class="star" data-value="5">☆</span>
</div>
<p class="rating-message" style="color:green;"></p>
</div>
CSS样式 (简单示例):
.stars {
cursor: pointer;
font-size: 30px;
color: #ddd;
}
.stars .star.active,
.stars .star:hover,
.stars .star:hover ~ .star {
color: #ffd700; /* 金色 */
}
JavaScript (原生或jQuery示例):
// 假设使用 jQuery
$(document).ready(function() {
$('.star').on('click', function() {
var $this = $(this);
var value = $this.data('value');
var $widget = $this.closest('.rating-widget');
var targetId = $widget.data('target-id');
var targetType = $widget.data('target-type');
var $message = $widget.find('.rating-message');
// 发送Ajax
$.post('rate.php', {
target_id: targetId,
target_type: targetType,
score: value
}, function(response) {
if (response.success) {
// 更新显示
$widget.find('.avg-score').text(response.new_avg);
$widget.find('.score-count').text(response.new_count);
$message.text('评分成功!').fadeIn().delay(2000).fadeOut();
// 高亮用户选择的星星
$widget.find('.star').each(function() {
if ($(this).data('value') <= response.your_score) {
$(this).addClass('active');
} else {
$(this).removeClass('active');
}
});
} else {
$message.text(response.message).css('color', 'red').fadeIn().delay(3000).fadeOut();
}
}, 'json');
});
});
进阶优化与安全考虑
-
防刷评分:
- IP限制:对同一IP在短时间内限制评分次数。
- Token验证:使用CSRF token防止跨站请求伪造。
-
性能优化:
- 缓存平均分:如果访问量大,不要每次都计算
AVG(),可以考虑:- 使用Redis缓存
target_id对应的平均分和人数。 - 或使用MySQL的触发器/事件在写入
ratings表时自动更新主表(上面示例已实现)。
- 使用Redis缓存
- 读写分离:写操作走主库,读操作走从库。
- 缓存平均分:如果访问量大,不要每次都计算
-
用户体验:
- 禁用重复点击:Ajax请求发送期间,禁用星星点击或显示加载状态。
- 回滚机制:如果Ajax失败,显示错误并允许重新点击。
- 显示用户当前评分:页面加载时,查询当前用户是否已经评分,并高亮显示。
-
数据库事务:上面代码使用了事务,确保
ratings表和主表的avg_score一致。 -
防止SQL注入:严格使用参数绑定或预处理语句(上面示例已使用PDO)。
完整示例的数据库查询 (获取页面数据)
当你渲染文章列表或详情页时,需要获取评分数据:
SELECT id, title, avg_score, score_count FROM articles WHERE id = :id;
这个方案实现了:
- 可复用:通过
target_type字段支持多种对象(文章、商品、评论)。 - 防重复:使用唯一索引。
- 高性能:主表保存平均分和人数,避免每次查询都计算。
- 安全:输入校验、白名单、参数绑定、事务。
你可以根据项目规模选择使用简单的文件缓存或Redis来进一步优化。