修复多人游戏排行榜问题
This commit is contained in:
@@ -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小时过期
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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过期时间
|
||||
|
||||
Reference in New Issue
Block a user