feat(daily-challenge): show statistics in profile

This commit is contained in:
MingxuanGame
2025-08-20 04:24:00 +00:00
parent 0b3e725eea
commit ef1b699547
5 changed files with 157 additions and 2 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -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() # 启动缓存调度器