添加在线统计
This commit is contained in:
237
app/router/v2/stats.py
Normal file
237
app/router/v2/stats.py
Normal file
@@ -0,0 +1,237 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
from typing import Any
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from app.dependencies.database import get_redis, get_redis_message
|
||||
from app.log import logger
|
||||
|
||||
from .router import router
|
||||
|
||||
from fastapi import APIRouter
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Redis key constants
|
||||
REDIS_ONLINE_USERS_KEY = "server:online_users"
|
||||
REDIS_PLAYING_USERS_KEY = "server:playing_users"
|
||||
REDIS_REGISTERED_USERS_KEY = "server:registered_users"
|
||||
REDIS_ONLINE_HISTORY_KEY = "server:online_history"
|
||||
|
||||
# 线程池用于同步Redis操作
|
||||
_executor = ThreadPoolExecutor(max_workers=2)
|
||||
|
||||
async def _redis_exec(func, *args, **kwargs):
|
||||
"""在线程池中执行同步Redis操作"""
|
||||
loop = asyncio.get_event_loop()
|
||||
return await loop.run_in_executor(_executor, func, *args, **kwargs)
|
||||
|
||||
class ServerStats(BaseModel):
|
||||
"""服务器统计信息响应模型"""
|
||||
registered_users: int
|
||||
online_users: int
|
||||
playing_users: int
|
||||
timestamp: datetime
|
||||
|
||||
class OnlineHistoryPoint(BaseModel):
|
||||
"""在线历史数据点"""
|
||||
timestamp: datetime
|
||||
online_count: int
|
||||
playing_count: int
|
||||
|
||||
class OnlineHistoryResponse(BaseModel):
|
||||
"""24小时在线历史响应模型"""
|
||||
history: list[OnlineHistoryPoint]
|
||||
current_stats: ServerStats
|
||||
|
||||
@router.get("/stats", response_model=ServerStats, tags=["统计"])
|
||||
async def get_server_stats() -> ServerStats:
|
||||
"""
|
||||
获取服务器实时统计信息
|
||||
|
||||
返回服务器注册用户数、在线用户数、正在游玩用户数等实时统计信息
|
||||
"""
|
||||
redis = get_redis()
|
||||
|
||||
try:
|
||||
# 并行获取所有统计数据
|
||||
registered_count, online_count, playing_count = await asyncio.gather(
|
||||
_get_registered_users_count(redis),
|
||||
_get_online_users_count(redis),
|
||||
_get_playing_users_count(redis)
|
||||
)
|
||||
|
||||
return ServerStats(
|
||||
registered_users=registered_count,
|
||||
online_users=online_count,
|
||||
playing_users=playing_count,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting server stats: {e}")
|
||||
# 返回默认值
|
||||
return ServerStats(
|
||||
registered_users=0,
|
||||
online_users=0,
|
||||
playing_users=0,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
|
||||
@router.get("/stats/history", response_model=OnlineHistoryResponse, tags=["统计"])
|
||||
async def get_online_history() -> OnlineHistoryResponse:
|
||||
"""
|
||||
获取最近24小时在线统计历史
|
||||
|
||||
返回过去24小时内每小时的在线用户数和游玩用户数统计
|
||||
"""
|
||||
redis = get_redis()
|
||||
|
||||
try:
|
||||
# 获取历史数据 - 使用同步Redis客户端
|
||||
redis_sync = get_redis_message()
|
||||
history_data = await _redis_exec(redis_sync.lrange, REDIS_ONLINE_HISTORY_KEY, 0, -1)
|
||||
history_points = []
|
||||
|
||||
# 处理历史数据
|
||||
for data in history_data:
|
||||
try:
|
||||
point_data = json.loads(data)
|
||||
history_points.append(OnlineHistoryPoint(
|
||||
timestamp=datetime.fromisoformat(point_data["timestamp"]),
|
||||
online_count=point_data["online_count"],
|
||||
playing_count=point_data["playing_count"]
|
||||
))
|
||||
except (json.JSONDecodeError, KeyError, ValueError) as e:
|
||||
logger.warning(f"Invalid history data point: {data}, error: {e}")
|
||||
continue
|
||||
|
||||
# 按时间排序(最新的在前)
|
||||
history_points.sort(key=lambda x: x.timestamp, reverse=True)
|
||||
|
||||
# 获取当前统计信息
|
||||
current_stats = await get_server_stats()
|
||||
|
||||
return OnlineHistoryResponse(
|
||||
history=history_points,
|
||||
current_stats=current_stats
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting online history: {e}")
|
||||
# 返回空历史和当前状态
|
||||
current_stats = await get_server_stats()
|
||||
return OnlineHistoryResponse(
|
||||
history=[],
|
||||
current_stats=current_stats
|
||||
)
|
||||
|
||||
async def _get_registered_users_count(redis) -> int:
|
||||
"""获取注册用户总数(从缓存)"""
|
||||
try:
|
||||
count = await redis.get(REDIS_REGISTERED_USERS_KEY)
|
||||
return int(count) if count else 0
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting registered users count: {e}")
|
||||
return 0
|
||||
|
||||
async def _get_online_users_count(redis) -> int:
|
||||
"""获取当前在线用户数"""
|
||||
try:
|
||||
count = await redis.scard(REDIS_ONLINE_USERS_KEY)
|
||||
return count
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting online users count: {e}")
|
||||
return 0
|
||||
|
||||
async def _get_playing_users_count(redis) -> int:
|
||||
"""获取当前游玩用户数"""
|
||||
try:
|
||||
count = await redis.scard(REDIS_PLAYING_USERS_KEY)
|
||||
return count
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting playing users count: {e}")
|
||||
return 0
|
||||
|
||||
# 统计更新功能
|
||||
async def update_registered_users_count() -> None:
|
||||
"""更新注册用户数缓存"""
|
||||
from app.dependencies.database import with_db
|
||||
from app.database import User
|
||||
from app.const import BANCHOBOT_ID
|
||||
from sqlmodel import select, func
|
||||
|
||||
redis = get_redis()
|
||||
try:
|
||||
async with with_db() as db:
|
||||
# 排除机器人用户(BANCHOBOT_ID)
|
||||
result = await db.exec(
|
||||
select(func.count()).select_from(User).where(User.id != BANCHOBOT_ID)
|
||||
)
|
||||
count = result.first()
|
||||
await redis.set(REDIS_REGISTERED_USERS_KEY, count or 0, ex=300) # 5分钟过期
|
||||
logger.debug(f"Updated registered users count: {count}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating registered users count: {e}")
|
||||
|
||||
async def add_online_user(user_id: int) -> None:
|
||||
"""添加在线用户"""
|
||||
redis_sync = get_redis_message()
|
||||
redis_async = get_redis()
|
||||
try:
|
||||
await _redis_exec(redis_sync.sadd, REDIS_ONLINE_USERS_KEY, str(user_id))
|
||||
await redis_async.expire(REDIS_ONLINE_USERS_KEY, 3600) # 1小时过期
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding online user {user_id}: {e}")
|
||||
|
||||
async def remove_online_user(user_id: int) -> None:
|
||||
"""移除在线用户"""
|
||||
redis_sync = get_redis_message()
|
||||
try:
|
||||
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))
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing online user {user_id}: {e}")
|
||||
|
||||
async def add_playing_user(user_id: int) -> None:
|
||||
"""添加游玩用户"""
|
||||
redis_sync = get_redis_message()
|
||||
redis_async = get_redis()
|
||||
try:
|
||||
await _redis_exec(redis_sync.sadd, REDIS_PLAYING_USERS_KEY, str(user_id))
|
||||
await redis_async.expire(REDIS_PLAYING_USERS_KEY, 3600) # 1小时过期
|
||||
except Exception as e:
|
||||
logger.error(f"Error adding playing user {user_id}: {e}")
|
||||
|
||||
async def remove_playing_user(user_id: int) -> None:
|
||||
"""移除游玩用户"""
|
||||
redis_sync = get_redis_message()
|
||||
try:
|
||||
await _redis_exec(redis_sync.srem, REDIS_PLAYING_USERS_KEY, str(user_id))
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing playing user {user_id}: {e}")
|
||||
|
||||
async def record_hourly_stats() -> None:
|
||||
"""记录每小时统计数据"""
|
||||
redis_sync = get_redis_message()
|
||||
redis_async = get_redis()
|
||||
try:
|
||||
online_count = await _get_online_users_count(redis_async)
|
||||
playing_count = await _get_playing_users_count(redis_async)
|
||||
|
||||
history_point = {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"online_count": online_count,
|
||||
"playing_count": playing_count
|
||||
}
|
||||
|
||||
# 添加到历史记录
|
||||
await _redis_exec(redis_sync.lpush, REDIS_ONLINE_HISTORY_KEY, json.dumps(history_point))
|
||||
# 只保留48个数据点
|
||||
await _redis_exec(redis_sync.ltrim, REDIS_ONLINE_HISTORY_KEY, 0, 47)
|
||||
# 设置过期时间为25小时
|
||||
await redis_async.expire(REDIS_ONLINE_HISTORY_KEY, 25 * 3600)
|
||||
|
||||
logger.debug(f"Recorded hourly stats: online={online_count}, playing={playing_count}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error recording hourly stats: {e}")
|
||||
Reference in New Issue
Block a user