feat(daily-challenge): show statistics in profile
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
28
app/utils.py
28
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]
|
||||
|
||||
3
main.py
3
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() # 启动缓存调度器
|
||||
|
||||
Reference in New Issue
Block a user