首页
学习
活动
专区
圈层
工具
发布

用PHP和MySQL实现基于总分的用户位置自动更新

PHP与MySQL实现基于总分的用户位置自动更新

基础概念

基于总分的用户位置自动更新是指在用户积分或总分发生变化时,系统自动重新计算并更新该用户在排行榜中的位置。这种功能常见于游戏排行榜、竞赛系统、学习平台等需要实时反映用户排名的场景。

实现方案

数据库设计

首先需要设计用户表和积分表:

代码语言:txt
复制
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)
);

核心实现代码

1. 更新用户分数并重新计算位置

代码语言:txt
复制
<?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();
?>

2. 优化版本(仅更新受影响用户的位置)

上面的实现会更新所有用户的位置,当用户量很大时可能效率不高。下面是优化版本,只更新分数变化可能影响到的用户:

代码语言:txt
复制
/**
 * 优化版 - 更新用户分数并智能调整位置
 * @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;
    }
}

应用场景

  1. 游戏排行榜:玩家得分变化时实时更新排名
  2. 在线考试系统:根据考试成绩自动排名
  3. 电商平台:根据用户积分或消费金额排名
  4. 社交平台:根据用户活跃度或影响力排名
  5. 竞赛平台:参赛者成绩实时排名

优势

  1. 实时性:用户分数变化后立即反映在排名中
  2. 准确性:使用数据库事务保证数据一致性
  3. 灵活性:可根据不同场景调整排名算法
  4. 可扩展性:支持大量用户的排名计算

可能遇到的问题及解决方案

1. 性能问题(用户量很大时)

原因:每次更新都重新计算所有用户位置会导致性能下降

解决方案

  • 使用优化版本,只更新受影响用户的位置
  • 考虑使用Redis等内存数据库存储排行榜
  • 实现分页查询,避免一次性加载所有用户数据
  • 对于非实时性要求高的场景,可以使用定时任务批量更新位置

2. 排名相同的问题

原因:多个用户分数相同时,如何处理排名

解决方案

  • 使用RANK()函数会产生并列排名(如1,2,2,4)
  • 使用DENSE_RANK()函数会产生连续排名(如1,2,2,3)
  • 使用ROW_NUMBER()函数会产生唯一排名(如1,2,3,4)

示例修改:

代码语言:txt
复制
-- 使用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

3. 并发更新问题

原因:多个用户同时更新分数可能导致排名不一致

解决方案

  • 使用数据库事务(如代码中所示)
  • 在应用层加锁(如Redis分布式锁)
  • 使用乐观锁机制,检查版本号或时间戳

4. 历史排名记录

需求:需要记录用户排名的历史变化

解决方案: 创建排名历史表:

代码语言:txt
复制
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)
);

在更新位置时同时记录历史:

代码语言:txt
复制
// 在更新位置后添加
$historyStmt = $conn->prepare("INSERT INTO position_history (user_id, score, position) VALUES (?, ?, ?)");
$historyStmt->bind_param("iii", $userId, $newScore, $position);
$historyStmt->execute();

扩展功能

1. 分页获取排行榜

代码语言:txt
复制
/**
 * 获取排行榜分页数据
 * @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;
}

2. 获取用户前后几名

代码语言:txt
复制
/**
 * 获取用户周围的排名(前后各$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实现基于总分的用户位置自动更新的完整方案,包括数据库设计、核心实现代码、优化策略以及常见问题的解决方案。根据实际应用场景和性能需求,可以选择合适的实现方式,并可以进一步扩展功能如分页查询、历史记录等。

页面内容是否对你有帮助?
有帮助
没帮助

相关·内容

没有搜到相关的文章

领券