feat(recalculate): add scheduled job to recalculate failed scores

This commit is contained in:
MingxuanGame
2025-08-28 16:53:15 +00:00
parent fdb08fe31f
commit 224e890e31
4 changed files with 71 additions and 10 deletions

View File

@@ -14,6 +14,7 @@ from app.models.score import GameMode
from osupyparser import HitObject, OsuFile from osupyparser import HitObject, OsuFile
from osupyparser.osu.objects import Slider from osupyparser.osu.objects import Slider
from redis.asyncio import Redis
from sqlmodel import col, exists, select from sqlmodel import col, exists, select
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
@@ -29,6 +30,7 @@ except ImportError:
if TYPE_CHECKING: if TYPE_CHECKING:
from app.database.score import Score from app.database.score import Score
from app.fetcher import Fetcher
def clamp[T: int | float](n: T, min_value: T, max_value: T) -> T: def clamp[T: int | float](n: T, min_value: T, max_value: T) -> T:
@@ -123,28 +125,30 @@ async def calculate_pp(score: "Score", beatmap: str, session: AsyncSession) -> f
return pp return pp
async def pre_fetch_and_calculate_pp(score: "Score", beatmap_id: int, session: AsyncSession, redis, fetcher) -> float: async def pre_fetch_and_calculate_pp(
score: "Score", session: AsyncSession, redis: Redis, fetcher: "Fetcher"
) -> tuple[float, bool]:
""" """
优化版PP计算预先获取beatmap文件并使用缓存 优化版PP计算预先获取beatmap文件并使用缓存
""" """
import asyncio
from app.database.beatmap import BannedBeatmaps from app.database.beatmap import BannedBeatmaps
beatmap_id = score.beatmap_id
# 快速检查是否被封禁 # 快速检查是否被封禁
if settings.suspicious_score_check: if settings.suspicious_score_check:
beatmap_banned = ( beatmap_banned = (
await session.exec(select(exists()).where(col(BannedBeatmaps.beatmap_id) == beatmap_id)) await session.exec(select(exists()).where(col(BannedBeatmaps.beatmap_id) == beatmap_id))
).first() ).first()
if beatmap_banned: if beatmap_banned:
return 0 return 0, False
# 异步获取beatmap原始文件利用已有的Redis缓存机制 # 异步获取beatmap原始文件利用已有的Redis缓存机制
try: try:
beatmap_raw = await fetcher.get_or_fetch_beatmap_raw(redis, beatmap_id) beatmap_raw = await fetcher.get_or_fetch_beatmap_raw(redis, beatmap_id)
except Exception as e: except Exception as e:
logger.error(f"Failed to fetch beatmap {beatmap_id}: {e}") logger.error(f"Failed to fetch beatmap {beatmap_id}: {e}")
return 0 return 0, False
# 在获取文件的同时可以检查可疑beatmap # 在获取文件的同时可以检查可疑beatmap
if settings.suspicious_score_check: if settings.suspicious_score_check:
@@ -158,12 +162,12 @@ async def pre_fetch_and_calculate_pp(score: "Score", beatmap_id: int, session: A
if is_sus: if is_sus:
session.add(BannedBeatmaps(beatmap_id=beatmap_id)) session.add(BannedBeatmaps(beatmap_id=beatmap_id))
logger.warning(f"Beatmap {beatmap_id} is suspicious, banned") logger.warning(f"Beatmap {beatmap_id} is suspicious, banned")
return 0 return 0, True
except Exception: except Exception:
logger.exception(f"Error checking if beatmap {beatmap_id} is suspicious") logger.exception(f"Error checking if beatmap {beatmap_id} is suspicious")
# 调用已优化的PP计算函数 # 调用已优化的PP计算函数
return await calculate_pp(score, beatmap_raw, session) return await calculate_pp(score, beatmap_raw, session), True
async def batch_calculate_pp( async def batch_calculate_pp(

View File

@@ -11,6 +11,7 @@ from app.calculator import (
calculate_weighted_acc, calculate_weighted_acc,
calculate_weighted_pp, calculate_weighted_pp,
clamp, clamp,
pre_fetch_and_calculate_pp,
) )
from app.database.team import TeamMember from app.database.team import TeamMember
from app.dependencies.database import get_redis from app.dependencies.database import get_redis
@@ -880,15 +881,16 @@ async def process_score(
maximum_statistics=info.maximum_statistics, maximum_statistics=info.maximum_statistics,
processed=True, processed=True,
) )
successed = True
if can_get_pp: if can_get_pp:
from app.calculator import pre_fetch_and_calculate_pp pp, successed = await pre_fetch_and_calculate_pp(score, session, redis, fetcher)
pp = await pre_fetch_and_calculate_pp(score, beatmap_id, session, redis, fetcher)
score.pp = pp score.pp = pp
session.add(score) session.add(score)
user_id = user.id user_id = user.id
await session.commit() await session.commit()
await session.refresh(score) await session.refresh(score)
if not successed:
await redis.rpush("score:need_recalculate", score.id) # pyright: ignore[reportGeneralTypeIssues]
if can_get_pp and score.pp != 0: if can_get_pp and score.pp != 0:
previous_pp_best = await get_user_best_pp_in_beatmap(session, beatmap_id, user_id, score.gamemode) previous_pp_best = await get_user_best_pp_in_beatmap(session, beatmap_id, user_id, score.gamemode)
if previous_pp_best is None or score.pp > previous_pp_best.pp: if previous_pp_best is None or score.pp > previous_pp_best.pp:

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from .daily_challenge import create_daily_challenge_room from .daily_challenge import create_daily_challenge_room
from .recalculate_banned_beatmap import recalculate_banned_beatmap from .recalculate_banned_beatmap import recalculate_banned_beatmap
from .recalculate_failed_score import recalculate_failed_score
from .room import create_playlist_room, create_playlist_room_from_api from .room import create_playlist_room, create_playlist_room_from_api
__all__ = [ __all__ = [
@@ -9,4 +10,5 @@ __all__ = [
"create_playlist_room", "create_playlist_room",
"create_playlist_room_from_api", "create_playlist_room_from_api",
"recalculate_banned_beatmap", "recalculate_banned_beatmap",
"recalculate_failed_score",
] ]

View File

@@ -0,0 +1,53 @@
from __future__ import annotations
from app.calculator import pre_fetch_and_calculate_pp
from app.database.score import Score, calculate_user_pp
from app.database.statistics import UserStatistics
from app.dependencies.database import get_redis, with_db
from app.dependencies.fetcher import get_fetcher
from app.dependencies.scheduler import get_scheduler
from app.log import logger
from sqlmodel import select
@get_scheduler().scheduled_job("interval", id="recalculate_banned_beatmap", minutes=5)
async def recalculate_failed_score():
redis = get_redis()
fetcher = await get_fetcher()
need_add = set()
affected_user = set()
while True:
scores = await redis.lpop("score:need_recalculate", 100) # pyright: ignore[reportGeneralTypeIssues]
if not scores:
break
if isinstance(scores, bytes):
scores = [scores]
async with with_db() as session:
for score_id in scores:
score_id = int(score_id)
score = await session.get(Score, score_id)
if score is None:
continue
pp, successed = await pre_fetch_and_calculate_pp(score, session, redis, fetcher)
if not successed:
need_add.add(score_id)
else:
score.pp = pp
logger.info(
f"Recalculated PP for score {score.id} (user: {score.user_id}) at {score.ended_at}: {pp}"
)
affected_user.add((score.user_id, score.gamemode))
await session.commit()
for user_id, gamemode in affected_user:
stats = (
await session.exec(
select(UserStatistics).where(UserStatistics.user_id == user_id, UserStatistics.mode == gamemode)
)
).first()
if not stats:
continue
stats.pp, stats.hit_accuracy = await calculate_user_pp(session, user_id, gamemode)
await session.commit()
if need_add:
await redis.rpush("score:need_recalculate", *need_add) # pyright: ignore[reportGeneralTypeIssues]