diff --git a/app/database/daily_challenge.py b/app/database/daily_challenge.py index abf874f..476f326 100644 --- a/app/database/daily_challenge.py +++ b/app/database/daily_challenge.py @@ -1,7 +1,8 @@ -from datetime import datetime +from datetime import UTC, datetime, timedelta from typing import TYPE_CHECKING from app.models.model import UTCBaseModel +from app.utils import are_adjacent_weeks from sqlmodel import ( BigInteger, @@ -11,7 +12,9 @@ from sqlmodel import ( ForeignKey, Relationship, SQLModel, + select, ) +from sqlmodel.ext.asyncio.session import AsyncSession if TYPE_CHECKING: from .lazer_user import User @@ -56,3 +59,52 @@ class DailyChallengeStatsResp(DailyChallengeStatsBase): obj: DailyChallengeStats, ) -> "DailyChallengeStatsResp": return cls.model_validate(obj) + + +async def process_daily_challenge_score( + session: AsyncSession, user_id: int, room_id: int +): + from .playlist_best_score import PlaylistBestScore + + score = ( + await session.exec( + select(PlaylistBestScore).where( + PlaylistBestScore.user_id == user_id, + PlaylistBestScore.room_id == room_id, + PlaylistBestScore.playlist_id == 0, + ) + ) + ).first() + if not score or not score.score.passed: + return + stats = await session.get(DailyChallengeStats, user_id) + if not stats: + stats = DailyChallengeStats(user_id=user_id) + session.add(stats) + + stats.playcount += 1 + now = datetime.now(UTC) + if stats.last_update is None: + stats.daily_streak_best = 1 + stats.daily_streak_current = 1 + elif stats.last_update.replace(tzinfo=UTC).date() == now.date() - timedelta(days=1): + stats.daily_streak_current += 1 + if stats.daily_streak_current > stats.daily_streak_best: + stats.daily_streak_best = stats.daily_streak_current + elif stats.last_update.replace(tzinfo=UTC).date() == now.date(): + stats.playcount -= 1 + else: + stats.daily_streak_current = 1 + if stats.last_weekly_streak is None: + stats.weekly_streak_current = 1 + stats.weekly_streak_best = 1 + elif are_adjacent_weeks(stats.last_weekly_streak, now): + stats.weekly_streak_current += 1 + if stats.weekly_streak_current > stats.weekly_streak_best: + stats.weekly_streak_best = stats.weekly_streak_current + elif stats.last_weekly_streak.replace(tzinfo=UTC).date() == now.date(): + pass + else: + stats.weekly_streak_current = 1 + stats.last_update = now + stats.last_weekly_streak = now diff --git a/app/router/v2/score.py b/app/router/v2/score.py index 93eeb3c..bec9fed 100644 --- a/app/router/v2/score.py +++ b/app/router/v2/score.py @@ -17,6 +17,7 @@ from app.database import ( User, ) from app.database.counts import ReplayWatchedCount +from app.database.daily_challenge import process_daily_challenge_score from app.database.events import Event, EventType from app.database.playlist_attempts import ItemAttemptsCount from app.database.playlist_best_score import ( @@ -502,6 +503,10 @@ async def submit_playlist_score( ).first() if not item: raise HTTPException(status_code=404, detail="Playlist item not found") + room = await session.get(Room, room_id) + if not room: + raise HTTPException(status_code=404, detail="Room not found") + room_category = room.category user_id = current_user.id score_resp = await submit_score( @@ -524,7 +529,11 @@ async def submit_playlist_score( session, redis, ) + await session.commit() + if room_category == RoomCategory.DAILY_CHALLENGE and score_resp.passed: + await process_daily_challenge_score(session, user_id, room_id) await ItemAttemptsCount.get_or_create(room_id, user_id, session) + await session.commit() return score_resp diff --git a/app/service/daily_challenge.py b/app/service/daily_challenge.py index 82fbb8f..0d4f93e 100644 --- a/app/service/daily_challenge.py +++ b/app/service/daily_challenge.py @@ -2,16 +2,22 @@ from __future__ import annotations from datetime import UTC, datetime, timedelta import json +from math import ceil from app.const import BANCHOBOT_ID +from app.database.daily_challenge import DailyChallengeStats +from app.database.lazer_user import User +from app.database.playlist_best_score import PlaylistBestScore from app.database.playlists import Playlist from app.database.room import Room +from app.database.score import Score from app.dependencies.database import get_redis, with_db from app.dependencies.scheduler import get_scheduler from app.log import logger from app.models.metadata_hub import DailyChallengeInfo from app.models.mods import APIMod from app.models.room import RoomCategory +from app.utils import are_same_weeks from .room import create_playlist_room @@ -119,3 +125,62 @@ async def daily_challenge_job(): "date", run_date=datetime.now(UTC) + timedelta(minutes=5), ) + + +@get_scheduler().scheduled_job( + "cron", hour=0, minute=1, second=0, id="daily_challenge_last_top" +) +async def process_daily_challenge_top(): + async with with_db() as session: + now = datetime.now(UTC) + room = ( + await session.exec( + select(Room).where( + Room.category == RoomCategory.DAILY_CHALLENGE, + col(Room.ends_at) > now - timedelta(days=1), + col(Room.ends_at) < now, + ) + ) + ).first() + if room is None: + return + scores = ( + await session.exec( + select(PlaylistBestScore).where( + PlaylistBestScore.room_id == room.id, + PlaylistBestScore.playlist_id == 0, + col(PlaylistBestScore.score).has(col(Score.passed).is_(True)), + ) + ) + ).all() + total_score_count = len(scores) + s = [] + participated_users = [] + for i, score in enumerate(scores): + stats = await session.get(DailyChallengeStats, score.user_id) + if stats is None: # not execute + return + + if total_score_count < 10 or ceil(i + 1 / total_score_count) <= 0.1: + stats.top_10p_placements += 1 + if total_score_count < 2 or ceil(i + 1 / total_score_count) <= 0.5: + stats.top_50p_placements += 1 + s.append(s) + participated_users.append(score.user_id) + await session.commit() + del s + + user_ids = ( + await session.exec( + select(User.id).where(col(User.id).not_in(participated_users)) + ) + ).all() + for id in user_ids: + stats = await session.get(DailyChallengeStats, id) + if stats is None: # not execute + return + stats.daily_streak_current = 0 + if stats.last_weekly_streak and not are_same_weeks( + stats.last_weekly_streak.replace(tzinfo=UTC), now - timedelta(days=7) + ): + stats.weekly_streak_current = 0 diff --git a/app/utils.py b/app/utils.py index 22f06dd..444eb79 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,5 +1,7 @@ from __future__ import annotations +from datetime import datetime + def unix_timestamp_to_windows(timestamp: int) -> int: """Convert a Unix timestamp to a Windows timestamp.""" @@ -90,3 +92,29 @@ def snake_to_pascal(name: str, use_abbr: bool = True) -> str: result.append(part.capitalize()) return "".join(result) + + +def are_adjacent_weeks(dt1: datetime, dt2: datetime) -> bool: + y1, w1, _ = dt1.isocalendar() + y2, w2, _ = dt2.isocalendar() + + # 按 (年, 周) 排序,保证 dt1 <= dt2 + if (y1, w1) > (y2, w2): + y1, w1, y2, w2 = y2, w2, y1, w1 + + # 同一年,周数相邻 + if y1 == y2 and w2 - w1 == 1: + return True + + # 跨年,判断 y2 是否是下一年,且 w2 == 1,并且 w1 是 y1 的最后一周 + if y2 == y1 + 1 and w2 == 1: + # 判断 y1 的最后一周是多少 + last_week_y1 = datetime(y1, 12, 28).isocalendar()[1] # 12-28 保证在最后一周 + if w1 == last_week_y1: + return True + + return False + + +def are_same_weeks(dt1: datetime, dt2: datetime) -> bool: + return dt1.isocalendar()[:2] == dt2.isocalendar()[:2] diff --git a/main.py b/main.py index 1ac73e5..4db161d 100644 --- a/main.py +++ b/main.py @@ -26,7 +26,7 @@ from app.scheduler.cache_scheduler import start_cache_scheduler, stop_cache_sche from app.service.beatmap_download_service import download_service from app.service.calculate_all_user_rank import calculate_user_rank from app.service.create_banchobot import create_banchobot -from app.service.daily_challenge import daily_challenge_job +from app.service.daily_challenge import daily_challenge_job, process_daily_challenge_top from app.service.geoip_scheduler import schedule_geoip_updates from app.service.init_geoip import init_geoip from app.service.osu_rx_statistics import create_rx_statistics @@ -72,6 +72,7 @@ async def lifespan(app: FastAPI): start_scheduler() schedule_geoip_updates() # 调度 GeoIP 定时更新任务 await daily_challenge_job() + await process_daily_challenge_top() await create_banchobot() await download_service.start_health_check() # 启动下载服务健康检查 await start_cache_scheduler() # 启动缓存调度器