整理代码
This commit is contained in:
@@ -62,7 +62,7 @@ async def get_update(
|
||||
if db_channel:
|
||||
# 提取必要的属性避免惰性加载
|
||||
channel_type = db_channel.type
|
||||
|
||||
|
||||
resp.presence.append(
|
||||
await ChatChannelResp.from_db(
|
||||
db_channel,
|
||||
@@ -122,9 +122,7 @@ async def join_channel(
|
||||
).first()
|
||||
else:
|
||||
db_channel = (
|
||||
await session.exec(
|
||||
select(ChatChannel).where(ChatChannel.name == channel)
|
||||
)
|
||||
await session.exec(select(ChatChannel).where(ChatChannel.name == channel))
|
||||
).first()
|
||||
|
||||
if db_channel is None:
|
||||
@@ -154,9 +152,7 @@ async def leave_channel(
|
||||
).first()
|
||||
else:
|
||||
db_channel = (
|
||||
await session.exec(
|
||||
select(ChatChannel).where(ChatChannel.name == channel)
|
||||
)
|
||||
await session.exec(select(ChatChannel).where(ChatChannel.name == channel))
|
||||
).first()
|
||||
|
||||
if db_channel is None:
|
||||
@@ -187,7 +183,7 @@ async def get_channel_list(
|
||||
# 提取必要的属性避免惰性加载
|
||||
channel_id = channel.channel_id
|
||||
channel_type = channel.type
|
||||
|
||||
|
||||
assert channel_id is not None
|
||||
results.append(
|
||||
await ChatChannelResp.from_db(
|
||||
@@ -230,19 +226,17 @@ async def get_channel(
|
||||
).first()
|
||||
else:
|
||||
db_channel = (
|
||||
await session.exec(
|
||||
select(ChatChannel).where(ChatChannel.name == channel)
|
||||
)
|
||||
await session.exec(select(ChatChannel).where(ChatChannel.name == channel))
|
||||
).first()
|
||||
|
||||
|
||||
if db_channel is None:
|
||||
raise HTTPException(status_code=404, detail="Channel not found")
|
||||
|
||||
|
||||
# 立即提取需要的属性
|
||||
channel_id = db_channel.channel_id
|
||||
channel_type = db_channel.type
|
||||
channel_name = db_channel.name
|
||||
|
||||
|
||||
assert channel_id is not None
|
||||
|
||||
users = []
|
||||
@@ -325,7 +319,9 @@ async def create_channel(
|
||||
channel_name = f"pm_{current_user.id}_{req.target_id}"
|
||||
else:
|
||||
channel_name = req.channel.name if req.channel else "Unnamed Channel"
|
||||
result = await session.exec(select(ChatChannel).where(ChatChannel.name == channel_name))
|
||||
result = await session.exec(
|
||||
select(ChatChannel).where(ChatChannel.name == channel_name)
|
||||
)
|
||||
channel = result.first()
|
||||
|
||||
if channel is None:
|
||||
@@ -350,11 +346,11 @@ async def create_channel(
|
||||
await server.batch_join_channel([*target_users, current_user], channel, session)
|
||||
|
||||
await server.join_channel(current_user, channel, session)
|
||||
|
||||
|
||||
# 提取必要的属性避免惰性加载
|
||||
channel_id = channel.channel_id
|
||||
assert channel_id
|
||||
|
||||
|
||||
return await ChatChannelResp.from_db(
|
||||
channel,
|
||||
session,
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from app.database import ChatMessageResp
|
||||
from app.database.chat import (
|
||||
ChannelType,
|
||||
@@ -16,14 +11,13 @@ from app.database.chat import (
|
||||
UserSilenceResp,
|
||||
)
|
||||
from app.database.lazer_user import User
|
||||
from app.dependencies.database import Database, get_redis, get_redis_message
|
||||
from app.dependencies.database import Database, get_redis
|
||||
from app.dependencies.param import BodyOrForm
|
||||
from app.dependencies.user import get_current_user
|
||||
from app.log import logger
|
||||
from app.models.notification import ChannelMessage, ChannelMessageTeam
|
||||
from app.router.v2 import api_v2_router as router
|
||||
from app.service.optimized_message import optimized_message_service
|
||||
from app.service.redis_message_system import redis_message_system
|
||||
from app.log import logger
|
||||
|
||||
from .banchobot import bot
|
||||
from .server import server
|
||||
@@ -106,11 +100,9 @@ async def send_message(
|
||||
).first()
|
||||
else:
|
||||
db_channel = (
|
||||
await session.exec(
|
||||
select(ChatChannel).where(ChatChannel.name == channel)
|
||||
)
|
||||
await session.exec(select(ChatChannel).where(ChatChannel.name == channel))
|
||||
).first()
|
||||
|
||||
|
||||
if db_channel is None:
|
||||
raise HTTPException(status_code=404, detail="Channel not found")
|
||||
|
||||
@@ -118,29 +110,29 @@ async def send_message(
|
||||
channel_id = db_channel.channel_id
|
||||
channel_type = db_channel.type
|
||||
channel_name = db_channel.name
|
||||
|
||||
|
||||
assert channel_id is not None
|
||||
assert current_user.id
|
||||
|
||||
|
||||
# 使用 Redis 消息系统发送消息 - 立即返回
|
||||
resp = await redis_message_system.send_message(
|
||||
channel_id=channel_id,
|
||||
user=current_user,
|
||||
content=req.message,
|
||||
is_action=req.is_action,
|
||||
user_uuid=req.uuid
|
||||
user_uuid=req.uuid,
|
||||
)
|
||||
|
||||
|
||||
# 立即广播消息给所有客户端
|
||||
is_bot_command = req.message.startswith("!")
|
||||
await server.send_message_to_channel(
|
||||
resp, is_bot_command and channel_type == ChannelType.PUBLIC
|
||||
)
|
||||
|
||||
|
||||
# 处理机器人命令
|
||||
if is_bot_command:
|
||||
await bot.try_handle(current_user, db_channel, req.message, session)
|
||||
|
||||
|
||||
# 为通知系统创建临时 ChatMessage 对象(仅适用于私聊和团队频道)
|
||||
if channel_type in [ChannelType.PM, ChannelType.TEAM]:
|
||||
temp_msg = ChatMessage(
|
||||
@@ -151,7 +143,7 @@ async def send_message(
|
||||
type=MessageType.ACTION if req.is_action else MessageType.PLAIN,
|
||||
uuid=req.uuid,
|
||||
)
|
||||
|
||||
|
||||
if channel_type == ChannelType.PM:
|
||||
user_ids = channel_name.split("_")[1:]
|
||||
await server.new_private_notification(
|
||||
@@ -163,7 +155,7 @@ async def send_message(
|
||||
await server.new_private_notification(
|
||||
ChannelMessageTeam.init(temp_msg, current_user)
|
||||
)
|
||||
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
@@ -191,11 +183,9 @@ async def get_message(
|
||||
).first()
|
||||
else:
|
||||
db_channel = (
|
||||
await session.exec(
|
||||
select(ChatChannel).where(ChatChannel.name == channel)
|
||||
)
|
||||
await session.exec(select(ChatChannel).where(ChatChannel.name == channel))
|
||||
).first()
|
||||
|
||||
|
||||
if db_channel is None:
|
||||
raise HTTPException(status_code=404, detail="Channel not found")
|
||||
|
||||
@@ -218,7 +208,7 @@ async def get_message(
|
||||
query = query.where(col(ChatMessage.message_id) > since)
|
||||
if until is not None:
|
||||
query = query.where(col(ChatMessage.message_id) < until)
|
||||
|
||||
|
||||
query = query.order_by(col(ChatMessage.message_id).desc()).limit(limit)
|
||||
messages = (await session.exec(query)).all()
|
||||
resp = [await ChatMessageResp.from_db(msg, session) for msg in messages]
|
||||
@@ -247,14 +237,12 @@ async def mark_as_read(
|
||||
).first()
|
||||
else:
|
||||
db_channel = (
|
||||
await session.exec(
|
||||
select(ChatChannel).where(ChatChannel.name == channel)
|
||||
)
|
||||
await session.exec(select(ChatChannel).where(ChatChannel.name == channel))
|
||||
).first()
|
||||
|
||||
|
||||
if db_channel is None:
|
||||
raise HTTPException(status_code=404, detail="Channel not found")
|
||||
|
||||
|
||||
# 立即提取需要的属性
|
||||
channel_id = db_channel.channel_id
|
||||
assert channel_id
|
||||
|
||||
@@ -96,8 +96,10 @@ class ChatServer:
|
||||
async def send_message_to_channel(
|
||||
self, message: ChatMessageResp, is_bot_command: bool = False
|
||||
):
|
||||
logger.info(f"Sending message to channel {message.channel_id}, message_id: {message.message_id}, is_bot_command: {is_bot_command}")
|
||||
|
||||
logger.info(
|
||||
f"Sending message to channel {message.channel_id}, message_id: {message.message_id}, is_bot_command: {is_bot_command}"
|
||||
)
|
||||
|
||||
event = ChatEvent(
|
||||
event="chat.message.new",
|
||||
data={"messages": [message], "users": [message.sender]},
|
||||
@@ -107,24 +109,32 @@ class ChatServer:
|
||||
self._add_task(self.send_event(message.sender_id, event))
|
||||
else:
|
||||
# 总是广播消息,无论是临时ID还是真实ID
|
||||
logger.info(f"Broadcasting message to all users in channel {message.channel_id}")
|
||||
logger.info(
|
||||
f"Broadcasting message to all users in channel {message.channel_id}"
|
||||
)
|
||||
self._add_task(
|
||||
self.broadcast(
|
||||
message.channel_id,
|
||||
event,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# 只有真实消息 ID(正数且非零)才进行标记已读和设置最后消息
|
||||
# Redis 消息系统生成的ID都是正数,所以这里应该都能正常处理
|
||||
if message.message_id and message.message_id > 0:
|
||||
await self.mark_as_read(
|
||||
message.channel_id, message.sender_id, message.message_id
|
||||
)
|
||||
await self.redis.set(f"chat:{message.channel_id}:last_msg", message.message_id)
|
||||
logger.info(f"Updated last message ID for channel {message.channel_id} to {message.message_id}")
|
||||
await self.redis.set(
|
||||
f"chat:{message.channel_id}:last_msg", message.message_id
|
||||
)
|
||||
logger.info(
|
||||
f"Updated last message ID for channel {message.channel_id} to {message.message_id}"
|
||||
)
|
||||
else:
|
||||
logger.debug(f"Skipping last message update for message ID: {message.message_id}")
|
||||
logger.debug(
|
||||
f"Skipping last message update for message ID: {message.message_id}"
|
||||
)
|
||||
|
||||
async def batch_join_channel(
|
||||
self, users: list[User], channel: ChatChannel, session: AsyncSession
|
||||
@@ -340,11 +350,9 @@ async def chat_websocket(
|
||||
server.connect(user_id, websocket)
|
||||
# 使用明确的查询避免延迟加载
|
||||
db_channel = (
|
||||
await session.exec(
|
||||
select(ChatChannel).where(ChatChannel.channel_id == 1)
|
||||
)
|
||||
await session.exec(select(ChatChannel).where(ChatChannel.channel_id == 1))
|
||||
).first()
|
||||
if db_channel is not None:
|
||||
await server.join_channel(user, db_channel, session)
|
||||
|
||||
|
||||
await _listen_stop(websocket, user_id, factory)
|
||||
|
||||
@@ -5,4 +5,3 @@ from fastapi import APIRouter
|
||||
router = APIRouter(prefix="/api/v2")
|
||||
|
||||
# 导入所有子路由模块来注册路由
|
||||
from . import stats # 统计路由
|
||||
|
||||
@@ -75,9 +75,10 @@ READ_SCORE_TIMEOUT = 10
|
||||
|
||||
|
||||
async def process_user_achievement(score_id: int):
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from app.dependencies.database import engine
|
||||
|
||||
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
session = AsyncSession(engine)
|
||||
try:
|
||||
await process_achievements(session, get_redis(), score_id)
|
||||
@@ -99,7 +100,7 @@ async def submit_score(
|
||||
):
|
||||
# 立即获取用户ID,避免后续的懒加载问题
|
||||
user_id = current_user.id
|
||||
|
||||
|
||||
if not info.passed:
|
||||
info.rank = Rank.F
|
||||
score_token = (
|
||||
@@ -166,13 +167,15 @@ async def submit_score(
|
||||
has_pp,
|
||||
has_leaderboard,
|
||||
)
|
||||
score = (await db.exec(
|
||||
select(Score)
|
||||
.options(joinedload(Score.user)) # pyright: ignore[reportArgumentType]
|
||||
.where(Score.id == score_id)
|
||||
)).first()
|
||||
score = (
|
||||
await db.exec(
|
||||
select(Score)
|
||||
.options(joinedload(Score.user)) # pyright: ignore[reportArgumentType]
|
||||
.where(Score.id == score_id)
|
||||
)
|
||||
).first()
|
||||
assert score is not None
|
||||
|
||||
|
||||
resp = await ScoreResp.from_db(db, score)
|
||||
total_users = (await db.exec(select(func.count()).select_from(User))).first()
|
||||
assert total_users is not None
|
||||
@@ -202,13 +205,10 @@ async def submit_score(
|
||||
# 确保score对象已刷新,避免在后台任务中触发延迟加载
|
||||
await db.refresh(score)
|
||||
score_gamemode = score.gamemode
|
||||
|
||||
|
||||
if user_id is not None:
|
||||
background_task.add_task(
|
||||
_refresh_user_cache_background,
|
||||
redis,
|
||||
user_id,
|
||||
score_gamemode
|
||||
_refresh_user_cache_background, redis, user_id, score_gamemode
|
||||
)
|
||||
background_task.add_task(process_user_achievement, resp.id)
|
||||
return resp
|
||||
@@ -217,9 +217,10 @@ async def submit_score(
|
||||
async def _refresh_user_cache_background(redis: Redis, user_id: int, mode: GameMode):
|
||||
"""后台任务:刷新用户缓存"""
|
||||
try:
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
from app.dependencies.database import engine
|
||||
|
||||
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
user_cache_service = get_user_cache_service(redis)
|
||||
# 创建独立的数据库会话
|
||||
session = AsyncSession(engine)
|
||||
@@ -422,7 +423,7 @@ async def create_solo_score(
|
||||
assert current_user.id is not None
|
||||
# 立即获取用户ID,避免懒加载问题
|
||||
user_id = current_user.id
|
||||
|
||||
|
||||
background_task.add_task(_preload_beatmap_for_pp_calculation, beatmap_id)
|
||||
async with db:
|
||||
score_token = ScoreToken(
|
||||
@@ -480,7 +481,7 @@ async def create_playlist_score(
|
||||
assert current_user.id is not None
|
||||
# 立即获取用户ID,避免懒加载问题
|
||||
user_id = current_user.id
|
||||
|
||||
|
||||
room = await session.get(Room, room_id)
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail="Room not found")
|
||||
@@ -557,10 +558,10 @@ async def submit_playlist_score(
|
||||
fetcher: Fetcher = Depends(get_fetcher),
|
||||
):
|
||||
assert current_user.id is not None
|
||||
|
||||
|
||||
# 立即获取用户ID,避免懒加载问题
|
||||
user_id = current_user.id
|
||||
|
||||
|
||||
item = (
|
||||
await session.exec(
|
||||
select(Playlist).where(
|
||||
@@ -627,7 +628,7 @@ async def index_playlist_scores(
|
||||
):
|
||||
# 立即获取用户ID,避免懒加载问题
|
||||
user_id = current_user.id
|
||||
|
||||
|
||||
room = await session.get(Room, room_id)
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail="Room not found")
|
||||
@@ -694,7 +695,7 @@ async def show_playlist_score(
|
||||
):
|
||||
# 立即获取用户ID,避免懒加载问题
|
||||
user_id = current_user.id
|
||||
|
||||
|
||||
room = await session.get(Room, room_id)
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail="Room not found")
|
||||
@@ -803,7 +804,7 @@ async def pin_score(
|
||||
):
|
||||
# 立即获取用户ID,避免懒加载问题
|
||||
user_id = current_user.id
|
||||
|
||||
|
||||
score_record = (
|
||||
await db.exec(
|
||||
select(Score).where(
|
||||
@@ -848,7 +849,7 @@ async def unpin_score(
|
||||
):
|
||||
# 立即获取用户ID,避免懒加载问题
|
||||
user_id = current_user.id
|
||||
|
||||
|
||||
score_record = (
|
||||
await db.exec(
|
||||
select(Score).where(Score.id == score_id, Score.user_id == user_id)
|
||||
@@ -892,7 +893,7 @@ async def reorder_score_pin(
|
||||
):
|
||||
# 立即获取用户ID,避免懒加载问题
|
||||
user_id = current_user.id
|
||||
|
||||
|
||||
score_record = (
|
||||
await db.exec(
|
||||
select(Score).where(Score.id == score_id, Score.user_id == user_id)
|
||||
@@ -986,9 +987,9 @@ async def download_score_replay(
|
||||
current_user: User = Security(get_current_user, scopes=["public"]),
|
||||
storage_service: StorageService = Depends(get_storage_service),
|
||||
):
|
||||
# 立即获取用户ID,避免懒加载问题
|
||||
# 立即获取用户ID,避免懒加载问题
|
||||
user_id = current_user.id
|
||||
|
||||
|
||||
score = (await db.exec(select(Score).where(Score.id == score_id))).first()
|
||||
if not score:
|
||||
raise HTTPException(status_code=404, detail="Score not found")
|
||||
|
||||
@@ -1,42 +1,45 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
from typing import Any
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
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_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
|
||||
@@ -44,33 +47,36 @@ class OnlineHistoryPoint(BaseModel):
|
||||
peak_playing: int | None = None # 峰值游玩数(增强数据)
|
||||
total_samples: int | None = None # 采样次数(增强数据)
|
||||
|
||||
|
||||
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)
|
||||
_get_playing_users_count(redis),
|
||||
)
|
||||
|
||||
|
||||
return ServerStats(
|
||||
registered_users=registered_count,
|
||||
online_users=online_count,
|
||||
playing_users=playing_count,
|
||||
timestamp=datetime.utcnow()
|
||||
timestamp=datetime.utcnow(),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting server stats: {e}")
|
||||
@@ -79,75 +85,86 @@ async def get_server_stats() -> ServerStats:
|
||||
registered_users=0,
|
||||
online_users=0,
|
||||
playing_users=0,
|
||||
timestamp=datetime.utcnow()
|
||||
timestamp=datetime.utcnow(),
|
||||
)
|
||||
|
||||
|
||||
@router.get("/stats/history", response_model=OnlineHistoryResponse, tags=["统计"])
|
||||
async def get_online_history() -> OnlineHistoryResponse:
|
||||
"""
|
||||
获取最近24小时在线统计历史
|
||||
|
||||
|
||||
返回过去24小时内每小时的在线用户数和游玩用户数统计,
|
||||
包含当前实时数据作为最新数据点
|
||||
"""
|
||||
try:
|
||||
# 获取历史数据 - 使用同步Redis客户端
|
||||
redis_sync = get_redis_message()
|
||||
history_data = await _redis_exec(redis_sync.lrange, REDIS_ONLINE_HISTORY_KEY, 0, -1)
|
||||
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"],
|
||||
peak_online=point_data.get("peak_online"), # 新字段,可能不存在
|
||||
peak_playing=point_data.get("peak_playing"), # 新字段,可能不存在
|
||||
total_samples=point_data.get("total_samples") # 新字段,可能不存在
|
||||
))
|
||||
history_points.append(
|
||||
OnlineHistoryPoint(
|
||||
timestamp=datetime.fromisoformat(point_data["timestamp"]),
|
||||
online_count=point_data["online_count"],
|
||||
playing_count=point_data["playing_count"],
|
||||
peak_online=point_data.get("peak_online"), # 新字段,可能不存在
|
||||
peak_playing=point_data.get(
|
||||
"peak_playing"
|
||||
), # 新字段,可能不存在
|
||||
total_samples=point_data.get(
|
||||
"total_samples"
|
||||
), # 新字段,可能不存在
|
||||
)
|
||||
)
|
||||
except (json.JSONDecodeError, KeyError, ValueError) as e:
|
||||
logger.warning(f"Invalid history data point: {data}, error: {e}")
|
||||
continue
|
||||
|
||||
|
||||
# 获取当前实时统计信息
|
||||
current_stats = await get_server_stats()
|
||||
|
||||
|
||||
# 如果历史数据为空或者最新数据超过15分钟,添加当前数据点
|
||||
if not history_points or (
|
||||
history_points and
|
||||
(current_stats.timestamp - max(history_points, key=lambda x: x.timestamp).timestamp).total_seconds() > 15 * 60
|
||||
history_points
|
||||
and (
|
||||
current_stats.timestamp
|
||||
- max(history_points, key=lambda x: x.timestamp).timestamp
|
||||
).total_seconds()
|
||||
> 15 * 60
|
||||
):
|
||||
history_points.append(OnlineHistoryPoint(
|
||||
timestamp=current_stats.timestamp,
|
||||
online_count=current_stats.online_users,
|
||||
playing_count=current_stats.playing_users,
|
||||
peak_online=current_stats.online_users, # 当前实时数据作为峰值
|
||||
peak_playing=current_stats.playing_users,
|
||||
total_samples=1
|
||||
))
|
||||
|
||||
history_points.append(
|
||||
OnlineHistoryPoint(
|
||||
timestamp=current_stats.timestamp,
|
||||
online_count=current_stats.online_users,
|
||||
playing_count=current_stats.playing_users,
|
||||
peak_online=current_stats.online_users, # 当前实时数据作为峰值
|
||||
peak_playing=current_stats.playing_users,
|
||||
total_samples=1,
|
||||
)
|
||||
)
|
||||
|
||||
# 按时间排序(最新的在前)
|
||||
history_points.sort(key=lambda x: x.timestamp, reverse=True)
|
||||
|
||||
|
||||
# 限制到最多48个数据点(24小时)
|
||||
history_points = history_points[:48]
|
||||
|
||||
|
||||
return OnlineHistoryResponse(
|
||||
history=history_points,
|
||||
current_stats=current_stats
|
||||
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
|
||||
)
|
||||
return OnlineHistoryResponse(history=[], current_stats=current_stats)
|
||||
|
||||
|
||||
async def _get_registered_users_count(redis) -> int:
|
||||
@@ -159,6 +176,7 @@ async def _get_registered_users_count(redis) -> int:
|
||||
logger.error(f"Error getting registered users count: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
async def _get_online_users_count(redis) -> int:
|
||||
"""获取当前在线用户数"""
|
||||
try:
|
||||
@@ -168,6 +186,7 @@ async def _get_online_users_count(redis) -> int:
|
||||
logger.error(f"Error getting online users count: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
async def _get_playing_users_count(redis) -> int:
|
||||
"""获取当前游玩用户数"""
|
||||
try:
|
||||
@@ -177,14 +196,16 @@ async def _get_playing_users_count(redis) -> int:
|
||||
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
|
||||
|
||||
from app.database import User
|
||||
from app.dependencies.database import with_db
|
||||
|
||||
from sqlmodel import func, select
|
||||
|
||||
redis = get_redis()
|
||||
try:
|
||||
async with with_db() as db:
|
||||
@@ -198,6 +219,7 @@ async def update_registered_users_count() -> None:
|
||||
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()
|
||||
@@ -209,14 +231,16 @@ async def add_online_user(user_id: int) -> None:
|
||||
if ttl <= 0: # -1表示永不过期,-2表示不存在,0表示已过期
|
||||
await redis_async.expire(REDIS_ONLINE_USERS_KEY, 3 * 3600) # 3小时过期
|
||||
logger.debug(f"Added online user {user_id}")
|
||||
|
||||
|
||||
# 立即更新当前区间统计
|
||||
from app.service.enhanced_interval_stats import update_user_activity_in_interval
|
||||
|
||||
asyncio.create_task(update_user_activity_in_interval(user_id, is_playing=False))
|
||||
|
||||
|
||||
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()
|
||||
@@ -226,6 +250,7 @@ async def remove_online_user(user_id: int) -> None:
|
||||
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()
|
||||
@@ -237,14 +262,16 @@ async def add_playing_user(user_id: int) -> None:
|
||||
if ttl <= 0: # -1表示永不过期,-2表示不存在,0表示已过期
|
||||
await redis_async.expire(REDIS_PLAYING_USERS_KEY, 3 * 3600) # 3小时过期
|
||||
logger.debug(f"Added playing user {user_id}")
|
||||
|
||||
|
||||
# 立即更新当前区间统计
|
||||
from app.service.enhanced_interval_stats import update_user_activity_in_interval
|
||||
|
||||
asyncio.create_task(update_user_activity_in_interval(user_id, is_playing=True))
|
||||
|
||||
|
||||
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()
|
||||
@@ -253,6 +280,7 @@ async def remove_playing_user(user_id: int) -> None:
|
||||
except Exception as e:
|
||||
logger.error(f"Error removing playing user {user_id}: {e}")
|
||||
|
||||
|
||||
async def record_hourly_stats() -> None:
|
||||
"""记录统计数据 - 简化版本,主要作为fallback使用"""
|
||||
redis_sync = get_redis_message()
|
||||
@@ -260,10 +288,10 @@ async def record_hourly_stats() -> None:
|
||||
try:
|
||||
# 先确保Redis连接正常
|
||||
await redis_async.ping()
|
||||
|
||||
|
||||
online_count = await _get_online_users_count(redis_async)
|
||||
playing_count = await _get_playing_users_count(redis_async)
|
||||
|
||||
|
||||
current_time = datetime.utcnow()
|
||||
history_point = {
|
||||
"timestamp": current_time.isoformat(),
|
||||
@@ -271,16 +299,20 @@ async def record_hourly_stats() -> None:
|
||||
"playing_count": playing_count,
|
||||
"peak_online": online_count,
|
||||
"peak_playing": playing_count,
|
||||
"total_samples": 1
|
||||
"total_samples": 1,
|
||||
}
|
||||
|
||||
|
||||
# 添加到历史记录
|
||||
await _redis_exec(redis_sync.lpush, REDIS_ONLINE_HISTORY_KEY, json.dumps(history_point))
|
||||
await _redis_exec(
|
||||
redis_sync.lpush, REDIS_ONLINE_HISTORY_KEY, json.dumps(history_point)
|
||||
)
|
||||
# 只保留48个数据点(24小时,每30分钟一个点)
|
||||
await _redis_exec(redis_sync.ltrim, REDIS_ONLINE_HISTORY_KEY, 0, 47)
|
||||
# 设置过期时间为26小时,确保有足够缓冲
|
||||
await redis_async.expire(REDIS_ONLINE_HISTORY_KEY, 26 * 3600)
|
||||
|
||||
logger.info(f"Recorded fallback stats: online={online_count}, playing={playing_count} at {current_time.strftime('%H:%M:%S')}")
|
||||
|
||||
logger.info(
|
||||
f"Recorded fallback stats: online={online_count}, playing={playing_count} at {current_time.strftime('%H:%M:%S')}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error recording fallback stats: {e}")
|
||||
|
||||
Reference in New Issue
Block a user