添加在线统计
This commit is contained in:
37
app/models/stats.py
Normal file
37
app/models/stats.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class OnlineStats(BaseModel):
|
||||||
|
"""在线统计信息"""
|
||||||
|
registered_users: int
|
||||||
|
online_users: int
|
||||||
|
playing_users: int
|
||||||
|
timestamp: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class OnlineHistoryPoint(BaseModel):
|
||||||
|
"""在线历史数据点"""
|
||||||
|
timestamp: datetime
|
||||||
|
online_count: int
|
||||||
|
playing_count: int
|
||||||
|
|
||||||
|
|
||||||
|
class OnlineHistoryStats(BaseModel):
|
||||||
|
"""24小时在线历史统计"""
|
||||||
|
history: list[OnlineHistoryPoint]
|
||||||
|
current_stats: OnlineStats
|
||||||
|
|
||||||
|
|
||||||
|
class ServerStatistics(BaseModel):
|
||||||
|
"""服务器统计信息"""
|
||||||
|
total_users: int
|
||||||
|
online_users: int
|
||||||
|
playing_users: int
|
||||||
|
spectating_users: int
|
||||||
|
multiplayer_users: int
|
||||||
|
last_updated: datetime
|
||||||
@@ -3,3 +3,6 @@ from __future__ import annotations
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v2")
|
router = APIRouter(prefix="/api/v2")
|
||||||
|
|
||||||
|
# 导入所有子路由模块来注册路由
|
||||||
|
from . import stats # 统计路由
|
||||||
|
|||||||
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}")
|
||||||
78
app/service/stats_scheduler.py
Normal file
78
app/service/stats_scheduler.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app.log import logger
|
||||||
|
from app.router.v2.stats import record_hourly_stats, update_registered_users_count
|
||||||
|
|
||||||
|
|
||||||
|
class StatsScheduler:
|
||||||
|
"""统计数据调度器"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._running = False
|
||||||
|
self._stats_task: asyncio.Task | None = None
|
||||||
|
self._registered_task: asyncio.Task | None = None
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
"""启动调度器"""
|
||||||
|
if self._running:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._running = True
|
||||||
|
self._stats_task = asyncio.create_task(self._stats_loop())
|
||||||
|
self._registered_task = asyncio.create_task(self._registered_users_loop())
|
||||||
|
logger.info("Stats scheduler started")
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
"""停止调度器"""
|
||||||
|
if not self._running:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._running = False
|
||||||
|
|
||||||
|
if self._stats_task:
|
||||||
|
self._stats_task.cancel()
|
||||||
|
if self._registered_task:
|
||||||
|
self._registered_task.cancel()
|
||||||
|
|
||||||
|
logger.info("Stats scheduler stopped")
|
||||||
|
|
||||||
|
async def _stats_loop(self) -> None:
|
||||||
|
"""统计数据记录循环 - 每30分钟记录一次"""
|
||||||
|
while self._running:
|
||||||
|
try:
|
||||||
|
await record_hourly_stats()
|
||||||
|
logger.debug("Recorded hourly statistics")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in stats loop: {e}")
|
||||||
|
|
||||||
|
# 等待30分钟
|
||||||
|
await asyncio.sleep(30 * 60)
|
||||||
|
|
||||||
|
async def _registered_users_loop(self) -> None:
|
||||||
|
"""注册用户数更新循环 - 每5分钟更新一次"""
|
||||||
|
while self._running:
|
||||||
|
try:
|
||||||
|
await update_registered_users_count()
|
||||||
|
logger.debug("Updated registered users count")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in registered users loop: {e}")
|
||||||
|
|
||||||
|
# 等待5分钟
|
||||||
|
await asyncio.sleep(5 * 60)
|
||||||
|
|
||||||
|
|
||||||
|
# 全局调度器实例
|
||||||
|
stats_scheduler = StatsScheduler()
|
||||||
|
|
||||||
|
|
||||||
|
def start_stats_scheduler() -> None:
|
||||||
|
"""启动统计调度器"""
|
||||||
|
stats_scheduler.start()
|
||||||
|
|
||||||
|
|
||||||
|
def stop_stats_scheduler() -> None:
|
||||||
|
"""停止统计调度器"""
|
||||||
|
stats_scheduler.stop()
|
||||||
@@ -91,8 +91,14 @@ class MetadataHub(Hub[MetadataClientState]):
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
async def _clean_state(self, state: MetadataClientState) -> None:
|
async def _clean_state(self, state: MetadataClientState) -> None:
|
||||||
|
user_id = int(state.connection_id)
|
||||||
|
|
||||||
|
# Remove from online user tracking
|
||||||
|
from app.router.v2.stats import remove_online_user
|
||||||
|
asyncio.create_task(remove_online_user(user_id))
|
||||||
|
|
||||||
if state.pushable:
|
if state.pushable:
|
||||||
await asyncio.gather(*self.broadcast_tasks(int(state.connection_id), None))
|
await asyncio.gather(*self.broadcast_tasks(user_id, None))
|
||||||
redis = get_redis()
|
redis = get_redis()
|
||||||
if await redis.exists(f"metadata:online:{state.connection_id}"):
|
if await redis.exists(f"metadata:online:{state.connection_id}"):
|
||||||
await redis.delete(f"metadata:online:{state.connection_id}")
|
await redis.delete(f"metadata:online:{state.connection_id}")
|
||||||
@@ -117,6 +123,10 @@ class MetadataHub(Hub[MetadataClientState]):
|
|||||||
user_id = int(client.connection_id)
|
user_id = int(client.connection_id)
|
||||||
self.get_or_create_state(client)
|
self.get_or_create_state(client)
|
||||||
|
|
||||||
|
# Track online user
|
||||||
|
from app.router.v2.stats import add_online_user
|
||||||
|
asyncio.create_task(add_online_user(user_id))
|
||||||
|
|
||||||
async with with_db() as session:
|
async with with_db() as session:
|
||||||
async with session.begin():
|
async with session.begin():
|
||||||
friends = (
|
friends = (
|
||||||
|
|||||||
@@ -163,6 +163,11 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
|||||||
@override
|
@override
|
||||||
async def _clean_state(self, state: MultiplayerClientState):
|
async def _clean_state(self, state: MultiplayerClientState):
|
||||||
user_id = int(state.connection_id)
|
user_id = int(state.connection_id)
|
||||||
|
|
||||||
|
# Remove from online user tracking
|
||||||
|
from app.router.v2.stats import remove_online_user
|
||||||
|
asyncio.create_task(remove_online_user(user_id))
|
||||||
|
|
||||||
if state.room_id != 0 and state.room_id in self.rooms:
|
if state.room_id != 0 and state.room_id in self.rooms:
|
||||||
server_room = self.rooms[state.room_id]
|
server_room = self.rooms[state.room_id]
|
||||||
room = server_room.room
|
room = server_room.room
|
||||||
@@ -172,6 +177,14 @@ class MultiplayerHub(Hub[MultiplayerClientState]):
|
|||||||
self.get_client_by_id(str(user_id)), server_room, user
|
self.get_client_by_id(str(user_id)), server_room, user
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def on_client_connect(self, client: Client) -> None:
|
||||||
|
"""Track online users when connecting to multiplayer hub"""
|
||||||
|
logger.info(f"[MultiplayerHub] Client {client.user_id} connected")
|
||||||
|
|
||||||
|
# Track online user
|
||||||
|
from app.router.v2.stats import add_online_user
|
||||||
|
asyncio.create_task(add_online_user(client.user_id))
|
||||||
|
|
||||||
def _ensure_in_room(self, client: Client) -> ServerMultiplayerRoom:
|
def _ensure_in_room(self, client: Client) -> ServerMultiplayerRoom:
|
||||||
store = self.get_or_create_state(client)
|
store = self.get_or_create_state(client)
|
||||||
if store.room_id == 0:
|
if store.room_id == 0:
|
||||||
|
|||||||
@@ -169,12 +169,17 @@ class SpectatorHub(Hub[StoreClientState]):
|
|||||||
Enhanced cleanup based on official osu-server-spectator implementation.
|
Enhanced cleanup based on official osu-server-spectator implementation.
|
||||||
Properly notifies watched users when spectator disconnects.
|
Properly notifies watched users when spectator disconnects.
|
||||||
"""
|
"""
|
||||||
|
user_id = int(state.connection_id)
|
||||||
|
|
||||||
|
# Remove from online and playing tracking
|
||||||
|
from app.router.v2.stats import remove_online_user
|
||||||
|
asyncio.create_task(remove_online_user(user_id))
|
||||||
|
|
||||||
if state.state:
|
if state.state:
|
||||||
await self._end_session(int(state.connection_id), state.state, state)
|
await self._end_session(user_id, state.state, state)
|
||||||
|
|
||||||
# Critical fix: Notify all watched users that this spectator has disconnected
|
# Critical fix: Notify all watched users that this spectator has disconnected
|
||||||
# This matches the official CleanUpState implementation
|
# This matches the official CleanUpState implementation
|
||||||
user_id = int(state.connection_id)
|
|
||||||
for watched_user_id in state.watched_user:
|
for watched_user_id in state.watched_user:
|
||||||
if (target_client := self.get_client_by_id(str(watched_user_id))) is not None:
|
if (target_client := self.get_client_by_id(str(watched_user_id))) is not None:
|
||||||
await self.call_noblock(
|
await self.call_noblock(
|
||||||
@@ -191,6 +196,10 @@ class SpectatorHub(Hub[StoreClientState]):
|
|||||||
"""
|
"""
|
||||||
logger.info(f"[SpectatorHub] Client {client.user_id} connected")
|
logger.info(f"[SpectatorHub] Client {client.user_id} connected")
|
||||||
|
|
||||||
|
# Track online user
|
||||||
|
from app.router.v2.stats import add_online_user
|
||||||
|
asyncio.create_task(add_online_user(client.user_id))
|
||||||
|
|
||||||
# Send all current player states to the new client
|
# Send all current player states to the new client
|
||||||
# This matches the official OnConnectedAsync behavior
|
# This matches the official OnConnectedAsync behavior
|
||||||
active_states = []
|
active_states = []
|
||||||
@@ -295,6 +304,10 @@ class SpectatorHub(Hub[StoreClientState]):
|
|||||||
)
|
)
|
||||||
logger.info(f"[SpectatorHub] {client.user_id} began playing {state.beatmap_id}")
|
logger.info(f"[SpectatorHub] {client.user_id} began playing {state.beatmap_id}")
|
||||||
|
|
||||||
|
# Track playing user
|
||||||
|
from app.router.v2.stats import add_playing_user
|
||||||
|
asyncio.create_task(add_playing_user(user_id))
|
||||||
|
|
||||||
# # 预缓存beatmap文件以加速后续PP计算
|
# # 预缓存beatmap文件以加速后续PP计算
|
||||||
# await self._preload_beatmap_for_pp_calculation(state.beatmap_id)
|
# await self._preload_beatmap_for_pp_calculation(state.beatmap_id)
|
||||||
|
|
||||||
@@ -343,6 +356,11 @@ class SpectatorHub(Hub[StoreClientState]):
|
|||||||
) and any(k.is_hit() and v > 0 for k, v in score.score_info.statistics.items()):
|
) and any(k.is_hit() and v > 0 for k, v in score.score_info.statistics.items()):
|
||||||
await self._process_score(store, client)
|
await self._process_score(store, client)
|
||||||
await self._end_session(user_id, state, store)
|
await self._end_session(user_id, state, store)
|
||||||
|
|
||||||
|
# Remove from playing user tracking
|
||||||
|
from app.router.v2.stats import remove_playing_user
|
||||||
|
asyncio.create_task(remove_playing_user(user_id))
|
||||||
|
|
||||||
store.state = None
|
store.state = None
|
||||||
store.beatmap_status = None
|
store.beatmap_status = None
|
||||||
store.checksum = None
|
store.checksum = None
|
||||||
|
|||||||
3
main.py
3
main.py
@@ -33,6 +33,7 @@ from app.service.load_achievements import load_achievements
|
|||||||
from app.service.osu_rx_statistics import create_rx_statistics
|
from app.service.osu_rx_statistics import create_rx_statistics
|
||||||
from app.service.recalculate import recalculate
|
from app.service.recalculate import recalculate
|
||||||
from app.service.redis_message_system import redis_message_system
|
from app.service.redis_message_system import redis_message_system
|
||||||
|
from app.service.stats_scheduler import start_stats_scheduler, stop_stats_scheduler
|
||||||
|
|
||||||
# 检查 New Relic 配置文件是否存在,如果存在则初始化 New Relic
|
# 检查 New Relic 配置文件是否存在,如果存在则初始化 New Relic
|
||||||
newrelic_config_path = os.path.join(os.path.dirname(__file__), "newrelic.ini")
|
newrelic_config_path = os.path.join(os.path.dirname(__file__), "newrelic.ini")
|
||||||
@@ -79,11 +80,13 @@ async def lifespan(app: FastAPI):
|
|||||||
await download_service.start_health_check() # 启动下载服务健康检查
|
await download_service.start_health_check() # 启动下载服务健康检查
|
||||||
await start_cache_scheduler() # 启动缓存调度器
|
await start_cache_scheduler() # 启动缓存调度器
|
||||||
redis_message_system.start() # 启动 Redis 消息系统
|
redis_message_system.start() # 启动 Redis 消息系统
|
||||||
|
start_stats_scheduler() # 启动统计调度器
|
||||||
load_achievements()
|
load_achievements()
|
||||||
# on shutdown
|
# on shutdown
|
||||||
yield
|
yield
|
||||||
stop_scheduler()
|
stop_scheduler()
|
||||||
redis_message_system.stop() # 停止 Redis 消息系统
|
redis_message_system.stop() # 停止 Redis 消息系统
|
||||||
|
stop_stats_scheduler() # 停止统计调度器
|
||||||
await stop_cache_scheduler() # 停止缓存调度器
|
await stop_cache_scheduler() # 停止缓存调度器
|
||||||
await download_service.stop_health_check() # 停止下载服务健康检查
|
await download_service.stop_health_check() # 停止下载服务健康检查
|
||||||
await engine.dispose()
|
await engine.dispose()
|
||||||
|
|||||||
112
test_stats_api.py
Normal file
112
test_stats_api.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
服务器统计API测试脚本
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
async def test_stats_api():
|
||||||
|
"""测试统计API"""
|
||||||
|
base_url = "http://localhost:8000" # 根据实际服务器地址修改
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
print("🧪 测试服务器统计API...")
|
||||||
|
|
||||||
|
# 测试服务器统计信息接口
|
||||||
|
print("\n1. 测试 /api/v2/stats 端点...")
|
||||||
|
try:
|
||||||
|
response = await client.get(f"{base_url}/api/v2/stats")
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
print(f"✅ 成功获取服务器统计信息:")
|
||||||
|
print(f" - 注册用户: {data['registered_users']}")
|
||||||
|
print(f" - 在线用户: {data['online_users']}")
|
||||||
|
print(f" - 游玩用户: {data['playing_users']}")
|
||||||
|
print(f" - 更新时间: {data['timestamp']}")
|
||||||
|
else:
|
||||||
|
print(f"❌ 请求失败: HTTP {response.status_code}")
|
||||||
|
print(f" 响应: {response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 请求异常: {e}")
|
||||||
|
|
||||||
|
# 测试在线历史接口
|
||||||
|
print("\n2. 测试 /api/v2/stats/history 端点...")
|
||||||
|
try:
|
||||||
|
response = await client.get(f"{base_url}/api/v2/stats/history")
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
print(f"✅ 成功获取在线历史信息:")
|
||||||
|
print(f" - 历史数据点数: {len(data['history'])}")
|
||||||
|
print(f" - 当前统计信息:")
|
||||||
|
current = data['current_stats']
|
||||||
|
print(f" - 注册用户: {current['registered_users']}")
|
||||||
|
print(f" - 在线用户: {current['online_users']}")
|
||||||
|
print(f" - 游玩用户: {current['playing_users']}")
|
||||||
|
|
||||||
|
if data['history']:
|
||||||
|
latest = data['history'][0]
|
||||||
|
print(f" - 最新历史记录:")
|
||||||
|
print(f" - 时间: {latest['timestamp']}")
|
||||||
|
print(f" - 在线数: {latest['online_count']}")
|
||||||
|
print(f" - 游玩数: {latest['playing_count']}")
|
||||||
|
else:
|
||||||
|
print(f" - 暂无历史数据(需要等待调度器记录)")
|
||||||
|
else:
|
||||||
|
print(f"❌ 请求失败: HTTP {response.status_code}")
|
||||||
|
print(f" 响应: {response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 请求异常: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_internal_functions():
|
||||||
|
"""测试内部函数"""
|
||||||
|
print("\n🔧 测试内部Redis函数...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from app.router.v2.stats import (
|
||||||
|
add_online_user,
|
||||||
|
remove_online_user,
|
||||||
|
add_playing_user,
|
||||||
|
remove_playing_user,
|
||||||
|
record_hourly_stats,
|
||||||
|
update_registered_users_count
|
||||||
|
)
|
||||||
|
|
||||||
|
# 测试添加用户
|
||||||
|
print(" 测试添加在线用户...")
|
||||||
|
await add_online_user(999999) # 测试用户ID
|
||||||
|
|
||||||
|
print(" 测试添加游玩用户...")
|
||||||
|
await add_playing_user(999999)
|
||||||
|
|
||||||
|
print(" 测试记录统计数据...")
|
||||||
|
await record_hourly_stats()
|
||||||
|
|
||||||
|
print(" 测试移除用户...")
|
||||||
|
await remove_playing_user(999999)
|
||||||
|
await remove_online_user(999999)
|
||||||
|
|
||||||
|
print(" 测试更新注册用户数...")
|
||||||
|
await update_registered_users_count()
|
||||||
|
|
||||||
|
print("✅ 内部函数测试完成")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 内部函数测试异常: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("🚀 开始测试服务器统计功能...")
|
||||||
|
|
||||||
|
# 首先测试内部函数
|
||||||
|
asyncio.run(test_internal_functions())
|
||||||
|
|
||||||
|
# 然后测试API端点
|
||||||
|
asyncio.run(test_stats_api())
|
||||||
|
|
||||||
|
print("\n✨ 测试完成!")
|
||||||
Reference in New Issue
Block a user