基于总分的用户位置自动更新是指在用户积分或总分发生变化时,系统自动重新计算并更新该用户在排行榜中的位置。这种功能常见于游戏排行榜、竞赛系统、学习平台等需要实时反映用户排名的场景。
首先需要设计用户表和积分表:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
-- 其他用户信息字段
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE user_scores (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
score INT NOT NULL DEFAULT 0,
position INT DEFAULT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX (score),
INDEX (position)
);
<?php
// 数据库连接配置
$dbHost = 'localhost';
$dbUser = 'username';
$dbPass = 'password';
$dbName = 'database_name';
// 创建数据库连接
$conn = new mysqli($dbHost, $dbUser, $dbPass, $dbName);
if ($conn->connect_error) {
die("连接失败: " . $conn->connect_error);
}
/**
* 更新用户分数并重新计算位置
* @param int $userId 用户ID
* @param int $newScore 新的分数
* @return bool 是否成功
*/
function updateUserScoreAndPosition($userId, $newScore) {
global $conn;
// 开始事务
$conn->begin_transaction();
try {
// 1. 更新用户分数
$stmt = $conn->prepare("UPDATE user_scores SET score = ? WHERE user_id = ?");
$stmt->bind_param("ii", $newScore, $userId);
$stmt->execute();
if ($stmt->affected_rows === 0) {
// 如果没有记录,插入新记录
$stmt = $conn->prepare("INSERT INTO user_scores (user_id, score) VALUES (?, ?)");
$stmt->bind_param("ii", $userId, $newScore);
$stmt->execute();
}
// 2. 重新计算所有用户的位置
$updatePositionQuery = "
UPDATE user_scores us
JOIN (
SELECT id,
RANK() OVER (ORDER BY score DESC) as new_position
FROM user_scores
) ranked ON us.id = ranked.id
SET us.position = ranked.new_position
";
$conn->query($updatePositionQuery);
// 提交事务
$conn->commit();
return true;
} catch (Exception $e) {
// 回滚事务
$conn->rollback();
error_log("更新分数和位置失败: " . $e->getMessage());
return false;
}
}
/**
* 获取用户排名
* @param int $userId 用户ID
* @return int|null 排名,如果用户不存在返回null
*/
function getUserPosition($userId) {
global $conn;
$stmt = $conn->prepare("SELECT position FROM user_scores WHERE user_id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$row = $result->fetch_assoc();
return $row['position'];
}
return null;
}
// 示例使用
$userId = 1;
$newScore = 1500;
if (updateUserScoreAndPosition($userId, $newScore)) {
$position = getUserPosition($userId);
echo "用户 {$userId} 的分数已更新为 {$newScore}, 当前排名: {$position}";
} else {
echo "更新分数失败";
}
$conn->close();
?>
上面的实现会更新所有用户的位置,当用户量很大时可能效率不高。下面是优化版本,只更新分数变化可能影响到的用户:
/**
* 优化版 - 更新用户分数并智能调整位置
* @param int $userId 用户ID
* @param int $newScore 新的分数
* @return bool 是否成功
*/
function optimizedUpdateUserScoreAndPosition($userId, $newScore) {
global $conn;
$conn->begin_transaction();
try {
// 获取旧分数和旧位置
$stmt = $conn->prepare("SELECT score, position FROM user_scores WHERE user_id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
// 新用户,插入记录
$stmt = $conn->prepare("INSERT INTO user_scores (user_id, score) VALUES (?, ?)");
$stmt->bind_param("ii", $userId, $newScore);
$stmt->execute();
// 新用户初始位置需要计算
$recalculateAll = true;
} else {
$row = $result->fetch_assoc();
$oldScore = $row['score'];
$oldPosition = $row['position'];
// 更新分数
$stmt = $conn->prepare("UPDATE user_scores SET score = ? WHERE user_id = ?");
$stmt->bind_param("ii", $newScore, $userId);
$stmt->execute();
// 判断是否需要重新计算位置
$recalculateAll = ($newScore == $oldScore);
}
if ($recalculateAll) {
// 分数变化较大,重新计算所有用户位置
$updatePositionQuery = "
UPDATE user_scores us
JOIN (
SELECT id,
RANK() OVER (ORDER BY score DESC) as new_position
FROM user_scores
) ranked ON us.id = ranked.id
SET us.position = ranked.new_position
";
$conn->query($updatePositionQuery);
} else {
// 分数变化较小,只更新受影响用户的位置
if ($newScore > $oldScore) {
// 分数增加,可能超过前面的用户
$updateQuery = "
UPDATE user_scores us
JOIN (
SELECT id,
RANK() OVER (ORDER BY score DESC) as new_position
FROM user_scores
WHERE score <= ? OR user_id = ?
) ranked ON us.id = ranked.id
SET us.position = ranked.new_position
WHERE us.score <= ? OR us.user_id = ?
";
$stmt = $conn->prepare($updateQuery);
$stmt->bind_param("iiii", $newScore, $userId, $newScore, $userId);
} else {
// 分数减少,可能被后面的用户超过
$updateQuery = "
UPDATE user_scores us
JOIN (
SELECT id,
RANK() OVER (ORDER BY score DESC) as new_position
FROM user_scores
WHERE score >= ? OR user_id = ?
) ranked ON us.id = ranked.id
SET us.position = ranked.new_position
WHERE us.score >= ? OR us.user_id = ?
";
$stmt = $conn->prepare($updateQuery);
$stmt->bind_param("iiii", $newScore, $userId, $newScore, $userId);
}
$stmt->execute();
}
$conn->commit();
return true;
} catch (Exception $e) {
$conn->rollback();
error_log("优化版更新失败: " . $e->getMessage());
return false;
}
}
原因:每次更新都重新计算所有用户位置会导致性能下降
解决方案:
原因:多个用户分数相同时,如何处理排名
解决方案:
RANK()
函数会产生并列排名(如1,2,2,4)DENSE_RANK()
函数会产生连续排名(如1,2,2,3)ROW_NUMBER()
函数会产生唯一排名(如1,2,3,4)示例修改:
-- 使用DENSE_RANK()
UPDATE user_scores us
JOIN (
SELECT id,
DENSE_RANK() OVER (ORDER BY score DESC) as new_position
FROM user_scores
) ranked ON us.id = ranked.id
SET us.position = ranked.new_position
原因:多个用户同时更新分数可能导致排名不一致
解决方案:
需求:需要记录用户排名的历史变化
解决方案: 创建排名历史表:
CREATE TABLE position_history (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
score INT NOT NULL,
position INT NOT NULL,
recorded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
在更新位置时同时记录历史:
// 在更新位置后添加
$historyStmt = $conn->prepare("INSERT INTO position_history (user_id, score, position) VALUES (?, ?, ?)");
$historyStmt->bind_param("iii", $userId, $newScore, $position);
$historyStmt->execute();
/**
* 获取排行榜分页数据
* @param int $page 页码
* @param int $perPage 每页数量
* @return array 排行榜数据
*/
function getLeaderboard($page = 1, $perPage = 10) {
global $conn;
$offset = ($page - 1) * $perPage;
$query = "
SELECT u.username, us.score, us.position
FROM user_scores us
JOIN users u ON us.user_id = u.id
ORDER BY us.position ASC
LIMIT ?, ?
";
$stmt = $conn->prepare($query);
$stmt->bind_param("ii", $offset, $perPage);
$stmt->execute();
$result = $stmt->get_result();
$leaderboard = [];
while ($row = $result->fetch_assoc()) {
$leaderboard[] = $row;
}
return $leaderboard;
}
/**
* 获取用户周围的排名(前后各$range名)
* @param int $userId 用户ID
* @param int $range 前后范围
* @return array 排名数据
*/
function getSurroundingPositions($userId, $range = 2) {
global $conn;
// 获取用户当前位置
$position = getUserPosition($userId);
if ($position === null) return [];
$minPos = max(1, $position - $range);
$maxPos = $position + $range;
$limit = $maxPos - $minPos + 1;
$query = "
SELECT u.username, us.score, us.position
FROM user_scores us
JOIN users u ON us.user_id = u.id
WHERE us.position BETWEEN ? AND ?
ORDER BY us.position ASC
";
$stmt = $conn->prepare($query);
$stmt->bind_param("ii", $minPos, $maxPos);
$stmt->execute();
$result = $stmt->get_result();
$surroundings = [];
while ($row = $result->fetch_assoc()) {
$surroundings[] = $row;
}
return $surroundings;
}
本文介绍了使用PHP和MySQL实现基于总分的用户位置自动更新的完整方案,包括数据库设计、核心实现代码、优化策略以及常见问题的解决方案。根据实际应用场景和性能需求,可以选择合适的实现方式,并可以进一步扩展功能如分页查询、历史记录等。
没有搜到相关的文章