修复多人游戏排行榜问题

This commit is contained in:
咕谷酱
2025-08-22 13:52:28 +08:00
parent 6136b9fed3
commit b300ce9b09
13 changed files with 1008 additions and 324 deletions

View File

@@ -218,6 +218,10 @@ This email was sent automatically, please do not reply.
# 生成新的验证码
code = EmailVerificationService.generate_verification_code()
# 解析用户代理字符串
from app.utils import parse_user_agent
parsed_user_agent = parse_user_agent(user_agent, max_length=255)
# 创建验证记录
verification = EmailVerification(
user_id=user_id,
@@ -225,7 +229,7 @@ This email was sent automatically, please do not reply.
verification_code=code,
expires_at=datetime.now(UTC) + timedelta(minutes=10), # 10分钟过期
ip_address=ip_address,
user_agent=user_agent
user_agent=parsed_user_agent # 使用解析后的用户代理
)
db.add(verification)
@@ -388,13 +392,18 @@ class LoginSessionService:
is_new_location: bool = False
) -> LoginSession:
"""创建登录会话"""
from app.utils import parse_user_agent
# 解析用户代理字符串,提取关键信息
parsed_user_agent = parse_user_agent(user_agent, max_length=255)
session_token = EmailVerificationService.generate_session_token()
session = LoginSession(
user_id=user_id,
session_token=session_token,
ip_address=ip_address,
user_agent=user_agent,
user_agent=parsed_user_agent, # 使用解析后的用户代理
country_code=country_code,
is_new_location=is_new_location,
expires_at=datetime.now(UTC) + timedelta(hours=24), # 24小时过期

View File

@@ -20,6 +20,7 @@ async def maintain_playing_users_online_status():
定期刷新正在游玩用户的metadata在线标记
确保他们在游玩过程中显示为在线状态。
但不会恢复已经退出的用户的在线状态。
"""
redis_sync = get_redis_message()
redis_async = get_redis()
@@ -31,17 +32,25 @@ async def maintain_playing_users_online_status():
if not playing_users:
return
logger.debug(f"Maintaining online status for {len(playing_users)} playing users")
logger.debug(f"Checking online status for {len(playing_users)} playing users")
# 为每个游玩用户刷新metadata在线标记
# 仅为当前有效连接的用户刷新在线状态
updated_count = 0
for user_id in playing_users:
user_id_str = user_id.decode() if isinstance(user_id, bytes) else str(user_id)
metadata_key = f"metadata:online:{user_id_str}"
# 设置或刷新metadata在线标记过期时间为1小时
await redis_async.set(metadata_key, "playing", ex=3600)
# 重要:首先检查用户是否已经有在线标记,只有存在才刷新
if await redis_async.exists(metadata_key):
# 只更新已经在线的用户的状态,不恢复已退出的用户
await redis_async.set(metadata_key, "playing", ex=3600)
updated_count += 1
else:
# 如果用户已退出(没有在线标记),则从游玩用户中移除
await _redis_exec(redis_sync.srem, REDIS_PLAYING_USERS_KEY, user_id)
logger.debug(f"Removed user {user_id_str} from playing users as they are offline")
logger.debug(f"Updated metadata online status for {len(playing_users)} playing users")
logger.debug(f"Updated metadata online status for {updated_count} playing users")
except Exception as e:
logger.error(f"Error maintaining playing users online status: {e}")

View File

@@ -1,196 +0,0 @@
"""
实时在线状态清理服务
此模块提供实时的在线状态清理功能,确保用户在断开连接后立即从在线列表中移除。
"""
from __future__ import annotations
import asyncio
import json
from datetime import datetime, timedelta
from app.dependencies.database import get_redis, get_redis_message
from app.log import logger
from app.router.v2.stats import (
REDIS_ONLINE_USERS_KEY,
REDIS_PLAYING_USERS_KEY,
_redis_exec,
)
class RealtimeOnlineCleanup:
"""实时在线状态清理器"""
def __init__(self):
self._running = False
self._task = None
async def start(self):
"""启动实时清理服务"""
if self._running:
return
self._running = True
self._task = asyncio.create_task(self._realtime_cleanup_loop())
logger.info("[RealtimeOnlineCleanup] Started realtime online cleanup service")
async def stop(self):
"""停止实时清理服务"""
if not self._running:
return
self._running = False
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
logger.info("[RealtimeOnlineCleanup] Stopped realtime online cleanup service")
async def _realtime_cleanup_loop(self):
"""实时清理循环 - 每30秒检查一次"""
while self._running:
try:
# 执行快速清理
cleaned_count = await self._quick_cleanup_stale_users()
if cleaned_count > 0:
logger.debug(f"[RealtimeOnlineCleanup] Quick cleanup: removed {cleaned_count} stale users")
# 等待30秒
await asyncio.sleep(30)
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"[RealtimeOnlineCleanup] Error in cleanup loop: {e}")
# 出错时等待30秒再重试
await asyncio.sleep(30)
async def _quick_cleanup_stale_users(self) -> int:
"""快速清理过期用户,返回清理数量"""
redis_sync = get_redis_message()
redis_async = get_redis()
total_cleaned = 0
try:
# 获取所有在线用户
online_users = await _redis_exec(redis_sync.smembers, REDIS_ONLINE_USERS_KEY)
# 快速检查只检查metadata键是否存在
stale_users = []
for user_id in online_users:
user_id_str = user_id.decode() if isinstance(user_id, bytes) else str(user_id)
metadata_key = f"metadata:online:{user_id_str}"
# 如果metadata标记不存在立即标记为过期
if not await redis_async.exists(metadata_key):
stale_users.append(user_id_str)
# 立即清理过期用户
if stale_users:
# 从在线用户集合中移除
await _redis_exec(redis_sync.srem, REDIS_ONLINE_USERS_KEY, *stale_users)
# 同时从游玩用户集合中移除(如果存在)
await _redis_exec(redis_sync.srem, REDIS_PLAYING_USERS_KEY, *stale_users)
total_cleaned = len(stale_users)
logger.info(f"[RealtimeOnlineCleanup] Immediately cleaned {total_cleaned} stale users")
except Exception as e:
logger.error(f"[RealtimeOnlineCleanup] Error in quick cleanup: {e}")
return total_cleaned
async def verify_user_online_status(self, user_id: int) -> bool:
"""实时验证用户在线状态"""
try:
redis = get_redis()
metadata_key = f"metadata:online:{user_id}"
# 检查metadata键是否存在
exists = await redis.exists(metadata_key)
# 如果不存在,从在线集合中移除该用户
if not exists:
redis_sync = get_redis_message()
await _redis_exec(redis_sync.srem, REDIS_ONLINE_USERS_KEY, str(user_id))
await _redis_exec(redis_sync.srem, REDIS_PLAYING_USERS_KEY, str(user_id))
logger.debug(f"[RealtimeOnlineCleanup] Verified user {user_id} is offline, removed from sets")
return bool(exists)
except Exception as e:
logger.error(f"[RealtimeOnlineCleanup] Error verifying user {user_id} status: {e}")
return False
async def force_refresh_online_list(self):
"""强制刷新整个在线用户列表"""
try:
redis_sync = get_redis_message()
redis_async = get_redis()
# 获取所有在线用户
online_users = await _redis_exec(redis_sync.smembers, REDIS_ONLINE_USERS_KEY)
playing_users = await _redis_exec(redis_sync.smembers, REDIS_PLAYING_USERS_KEY)
# 验证每个用户的在线状态
valid_online_users = []
valid_playing_users = []
for user_id in online_users:
user_id_str = user_id.decode() if isinstance(user_id, bytes) else str(user_id)
metadata_key = f"metadata:online:{user_id_str}"
if await redis_async.exists(metadata_key):
valid_online_users.append(user_id_str)
for user_id in playing_users:
user_id_str = user_id.decode() if isinstance(user_id, bytes) else str(user_id)
metadata_key = f"metadata:online:{user_id_str}"
if await redis_async.exists(metadata_key):
valid_playing_users.append(user_id_str)
# 重建在线用户集合
if online_users:
await _redis_exec(redis_sync.delete, REDIS_ONLINE_USERS_KEY)
if valid_online_users:
await _redis_exec(redis_sync.sadd, REDIS_ONLINE_USERS_KEY, *valid_online_users)
# 设置过期时间
await redis_async.expire(REDIS_ONLINE_USERS_KEY, 3 * 3600)
# 重建游玩用户集合
if playing_users:
await _redis_exec(redis_sync.delete, REDIS_PLAYING_USERS_KEY)
if valid_playing_users:
await _redis_exec(redis_sync.sadd, REDIS_PLAYING_USERS_KEY, *valid_playing_users)
# 设置过期时间
await redis_async.expire(REDIS_PLAYING_USERS_KEY, 3 * 3600)
cleaned_online = len(online_users) - len(valid_online_users)
cleaned_playing = len(playing_users) - len(valid_playing_users)
if cleaned_online > 0 or cleaned_playing > 0:
logger.info(f"[RealtimeOnlineCleanup] Force refresh: removed {cleaned_online} stale online users, {cleaned_playing} stale playing users")
return cleaned_online + cleaned_playing
except Exception as e:
logger.error(f"[RealtimeOnlineCleanup] Error in force refresh: {e}")
return 0
# 全局实例
realtime_cleanup = RealtimeOnlineCleanup()
def start_realtime_cleanup():
"""启动实时清理服务"""
asyncio.create_task(realtime_cleanup.start())
def stop_realtime_cleanup():
"""停止实时清理服务"""
asyncio.create_task(realtime_cleanup.stop())

View File

@@ -11,14 +11,13 @@ from app.router.v2.stats import (
)
async def cleanup_stale_online_users() -> tuple[int, int, int]:
"""清理过期的在线和游玩用户,返回清理的用户数(online_cleaned, playing_cleaned, metadata_cleaned)"""
async def cleanup_stale_online_users() -> tuple[int, int]:
"""清理过期的在线和游玩用户,返回清理的用户数"""
redis_sync = get_redis_message()
redis_async = get_redis()
online_cleaned = 0
playing_cleaned = 0
metadata_cleaned = 0
try:
# 获取所有在线用户
@@ -72,63 +71,10 @@ async def cleanup_stale_online_users() -> tuple[int, int, int]:
playing_cleaned = len(stale_playing_users)
logger.info(f"Cleaned {playing_cleaned} stale playing users")
# 新增清理过期的metadata在线标记
# 这个步骤用于清理那些由于异常断开连接而没有被正常清理的metadata键
metadata_cleaned = 0
try:
# 查找所有metadata:online:*键
metadata_keys = []
cursor = 0
pattern = "metadata:online:*"
# 使用SCAN命令遍历所有匹配的键
while True:
cursor, keys = await redis_async.scan(cursor=cursor, match=pattern, count=100)
metadata_keys.extend(keys)
if cursor == 0:
break
# 检查这些键是否对应有效的在线用户
orphaned_metadata_keys = []
for key in metadata_keys:
if isinstance(key, bytes):
key_str = key.decode()
else:
key_str = key
# 从键名中提取用户ID
user_id = key_str.replace("metadata:online:", "")
# 检查用户是否在在线用户集合中
is_in_online_set = await _redis_exec(redis_sync.sismember, REDIS_ONLINE_USERS_KEY, user_id)
is_in_playing_set = await _redis_exec(redis_sync.sismember, REDIS_PLAYING_USERS_KEY, user_id)
# 如果用户既不在在线集合也不在游玩集合中检查TTL
if not is_in_online_set and not is_in_playing_set:
# 检查键的TTL
ttl = await redis_async.ttl(key_str)
# TTL < 0 表示键没有过期时间或已过期
# 我们只清理那些明确过期或没有设置TTL的键
if ttl < 0:
# 再次确认键确实存在且没有对应的活跃连接
key_value = await redis_async.get(key_str)
if key_value:
# 键存在但用户不在任何集合中且没有有效TTL可以安全删除
orphaned_metadata_keys.append(key_str)
# 清理孤立的metadata键
if orphaned_metadata_keys:
await redis_async.delete(*orphaned_metadata_keys)
metadata_cleaned = len(orphaned_metadata_keys)
logger.info(f"Cleaned {metadata_cleaned} orphaned metadata:online keys")
except Exception as e:
logger.error(f"Error cleaning orphaned metadata keys: {e}")
except Exception as e:
logger.error(f"Error cleaning stale users: {e}")
return online_cleaned, playing_cleaned, metadata_cleaned
return online_cleaned, playing_cleaned
async def refresh_redis_key_expiry() -> None:

View File

@@ -134,10 +134,10 @@ class StatsScheduler:
"""清理循环 - 每10分钟清理一次过期用户"""
# 启动时立即执行一次清理
try:
online_cleaned, playing_cleaned, metadata_cleaned = await cleanup_stale_online_users()
if online_cleaned > 0 or playing_cleaned > 0 or metadata_cleaned > 0:
online_cleaned, playing_cleaned = await cleanup_stale_online_users()
if online_cleaned > 0 or playing_cleaned > 0:
logger.info(
f"Initial cleanup: removed {online_cleaned} stale online users, {playing_cleaned} stale playing users, {metadata_cleaned} orphaned metadata keys"
f"Initial cleanup: removed {online_cleaned} stale online users, {playing_cleaned} stale playing users"
)
await refresh_redis_key_expiry()
@@ -153,10 +153,10 @@ class StatsScheduler:
try:
# 清理过期用户
online_cleaned, playing_cleaned, metadata_cleaned = await cleanup_stale_online_users()
if online_cleaned > 0 or playing_cleaned > 0 or metadata_cleaned > 0:
online_cleaned, playing_cleaned = await cleanup_stale_online_users()
if online_cleaned > 0 or playing_cleaned > 0:
logger.info(
f"Cleanup: removed {online_cleaned} stale online users, {playing_cleaned} stale playing users, {metadata_cleaned} orphaned metadata keys"
f"Cleanup: removed {online_cleaned} stale online users, {playing_cleaned} stale playing users"
)
# 刷新Redis key过期时间