diff --git a/app/database/__init__.py b/app/database/__init__.py index 7140ea1..079112f 100644 --- a/app/database/__init__.py +++ b/app/database/__init__.py @@ -12,7 +12,7 @@ from .beatmapset import ( BeatmapsetResp, ) from .beatmapset_ratings import BeatmapRating -from .best_scores import PPBestScore +from .best_scores import BestScore from .chat import ( ChannelType, ChatChannel, @@ -56,7 +56,7 @@ from .statistics import ( UserStatisticsResp, ) from .team import Team, TeamMember, TeamRequest -from .total_score_best_scores import BestScore +from .total_score_best_scores import TotalScoreBestScore from .user import ( MeResp, User, @@ -105,7 +105,6 @@ __all__ = [ "Notification", "OAuthClient", "OAuthToken", - "PPBestScore", "PasswordReset", "Playlist", "PlaylistAggregateScore", @@ -131,6 +130,7 @@ __all__ = [ "Team", "TeamMember", "TeamRequest", + "TotalScoreBestScore", "TotpKeys", "TrustedDevice", "TrustedDeviceResp", diff --git a/app/database/best_scores.py b/app/database/best_scores.py index ac3b44b..b1f9059 100644 --- a/app/database/best_scores.py +++ b/app/database/best_scores.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: from .score import Score -class PPBestScore(SQLModel, table=True): +class BestScore(SQLModel, table=True): __tablename__: str = "best_scores" user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True)) score_id: int = Field(sa_column=Column(BigInteger, ForeignKey("scores.id"), primary_key=True)) diff --git a/app/database/score.py b/app/database/score.py index 172e8a4..32fd067 100644 --- a/app/database/score.py +++ b/app/database/score.py @@ -38,7 +38,7 @@ from app.utils import utcnow from .beatmap import Beatmap, BeatmapResp from .beatmapset import BeatmapsetResp -from .best_scores import PPBestScore +from .best_scores import BestScore from .counts import MonthlyPlaycounts from .events import Event, EventType from .playlist_best_score import PlaylistBestScore @@ -47,7 +47,7 @@ from .relationship import ( RelationshipType, ) from .score_token import ScoreToken -from .total_score_best_scores import BestScore +from .total_score_best_scores import TotalScoreBestScore from .user import User, UserResp from pydantic import BaseModel, field_serializer, field_validator @@ -195,13 +195,13 @@ class Score(ScoreBase, table=True): # optional beatmap: Mapped[Beatmap] = Relationship() user: Mapped[User] = Relationship(sa_relationship_kwargs={"lazy": "joined"}) - best_score: Mapped[BestScore | None] = Relationship( + best_score: Mapped[TotalScoreBestScore | None] = Relationship( back_populates="score", sa_relationship_kwargs={ "cascade": "all, delete-orphan", }, ) - ranked_score: Mapped[PPBestScore | None] = Relationship( + ranked_score: Mapped[BestScore | None] = Relationship( back_populates="score", sa_relationship_kwargs={ "cascade": "all, delete-orphan", @@ -481,10 +481,10 @@ class ScoreAround(SQLModel): async def get_best_id(session: AsyncSession, score_id: int) -> int | None: rownum = ( func.row_number() - .over(partition_by=(col(PPBestScore.user_id), col(PPBestScore.gamemode)), order_by=col(PPBestScore.pp).desc()) + .over(partition_by=(col(BestScore.user_id), col(BestScore.gamemode)), order_by=col(BestScore.pp).desc()) .label("rn") ) - subq = select(PPBestScore, rownum).subquery() + subq = select(BestScore, rownum).subquery() stmt = select(subq.c.rn).where(subq.c.score_id == score_id) result = await session.exec(stmt) return result.one_or_none() @@ -498,8 +498,8 @@ async def _score_where( user: User | None = None, ) -> list[ColumnElement[bool] | TextClause] | None: wheres: list[ColumnElement[bool] | TextClause] = [ - col(BestScore.beatmap_id) == beatmap, - col(BestScore.gamemode) == mode, + col(TotalScoreBestScore.beatmap_id) == beatmap, + col(TotalScoreBestScore.gamemode) == mode, ] if type == LeaderboardType.FRIENDS: @@ -512,19 +512,21 @@ async def _score_where( ) .subquery() ) - wheres.append(col(BestScore.user_id).in_(select(subq.c.target_id))) + wheres.append(col(TotalScoreBestScore.user_id).in_(select(subq.c.target_id))) else: return None elif type == LeaderboardType.COUNTRY: if user and user.is_supporter: - wheres.append(col(BestScore.user).has(col(User.country_code) == user.country_code)) + wheres.append(col(TotalScoreBestScore.user).has(col(User.country_code) == user.country_code)) else: return None elif type == LeaderboardType.TEAM and user: team_membership = await user.awaitable_attrs.team_membership if team_membership: team_id = team_membership.team_id - wheres.append(col(BestScore.user).has(col(User.team_membership).has(TeamMember.team_id == team_id))) + wheres.append( + col(TotalScoreBestScore.user).has(col(User.team_membership).has(TeamMember.team_id == team_id)) + ) if mods: if user and user.is_supporter: wheres.append( @@ -558,10 +560,10 @@ async def get_leaderboard( max_score = sys.maxsize while limit > 0: query = ( - select(BestScore) - .where(*wheres, BestScore.total_score < max_score) + select(TotalScoreBestScore) + .where(*wheres, TotalScoreBestScore.total_score < max_score) .limit(limit) - .order_by(col(BestScore.total_score).desc()) + .order_by(col(TotalScoreBestScore.total_score).desc()) ) extra_need = 0 for s in await session.exec(query): @@ -580,13 +582,13 @@ async def get_leaderboard( user_score = None if user: self_query = ( - select(BestScore) - .where(BestScore.user_id == user.id) + select(TotalScoreBestScore) + .where(TotalScoreBestScore.user_id == user.id) .where( - col(BestScore.beatmap_id) == beatmap, - col(BestScore.gamemode) == mode, + col(TotalScoreBestScore.beatmap_id) == beatmap, + col(TotalScoreBestScore.gamemode) == mode, ) - .order_by(col(BestScore.total_score).desc()) + .order_by(col(TotalScoreBestScore.total_score).desc()) .limit(1) ) if mods: @@ -619,14 +621,14 @@ async def get_score_position_by_user( func.row_number() .over( partition_by=( - col(BestScore.beatmap_id), - col(BestScore.gamemode), + col(TotalScoreBestScore.beatmap_id), + col(TotalScoreBestScore.gamemode), ), - order_by=col(BestScore.total_score).desc(), + order_by=col(TotalScoreBestScore.total_score).desc(), ) .label("row_number") ) - subq = select(BestScore, rownum).join(Beatmap).where(*wheres).subquery() + subq = select(TotalScoreBestScore, rownum).join(Beatmap).where(*wheres).subquery() stmt = select(subq.c.row_number).where(subq.c.user_id == user.id) result = await session.exec(stmt) s = result.first() @@ -649,14 +651,14 @@ async def get_score_position_by_id( func.row_number() .over( partition_by=( - col(BestScore.beatmap_id), - col(BestScore.gamemode), + col(TotalScoreBestScore.beatmap_id), + col(TotalScoreBestScore.gamemode), ), - order_by=col(BestScore.total_score).desc(), + order_by=col(TotalScoreBestScore.total_score).desc(), ) .label("row_number") ) - subq = select(BestScore, rownum).join(Beatmap).where(*wheres).subquery() + subq = select(TotalScoreBestScore, rownum).join(Beatmap).where(*wheres).subquery() stmt = select(subq.c.row_number).where(subq.c.score_id == score_id) result = await session.exec(stmt) s = result.one_or_none() @@ -668,16 +670,16 @@ async def get_user_best_score_in_beatmap( beatmap: int, user: int, mode: GameMode | None = None, -) -> BestScore | None: +) -> TotalScoreBestScore | None: return ( await session.exec( - select(BestScore) + select(TotalScoreBestScore) .where( - BestScore.gamemode == mode if mode is not None else true(), - BestScore.beatmap_id == beatmap, - BestScore.user_id == user, + TotalScoreBestScore.gamemode == mode if mode is not None else true(), + TotalScoreBestScore.beatmap_id == beatmap, + TotalScoreBestScore.user_id == user, ) - .order_by(col(BestScore.total_score).desc()) + .order_by(col(TotalScoreBestScore.total_score).desc()) ) ).first() @@ -688,32 +690,32 @@ async def get_user_best_score_with_mod_in_beatmap( user: int, mod: list[str], mode: GameMode | None = None, -) -> BestScore | None: +) -> TotalScoreBestScore | None: return ( await session.exec( - select(BestScore) + select(TotalScoreBestScore) .where( - BestScore.gamemode == mode if mode is not None else True, - BestScore.beatmap_id == beatmap, - BestScore.user_id == user, + TotalScoreBestScore.gamemode == mode if mode is not None else True, + TotalScoreBestScore.beatmap_id == beatmap, + TotalScoreBestScore.user_id == user, text( "JSON_CONTAINS(total_score_best_scores.mods, :w)" " AND JSON_CONTAINS(:w, total_score_best_scores.mods)" ).params(w=json.dumps(mod)), ) - .order_by(col(BestScore.total_score).desc()) + .order_by(col(TotalScoreBestScore.total_score).desc()) ) ).first() async def get_user_first_scores( session: AsyncSession, user_id: int, mode: GameMode, limit: int = 5, offset: int = 0 -) -> list[BestScore]: +) -> list[TotalScoreBestScore]: rownum = ( func.row_number() .over( - partition_by=(col(BestScore.beatmap_id), col(BestScore.gamemode)), - order_by=col(BestScore.total_score).desc(), + partition_by=(col(TotalScoreBestScore.beatmap_id), col(TotalScoreBestScore.gamemode)), + order_by=col(TotalScoreBestScore.total_score).desc(), ) .label("rn") ) @@ -721,11 +723,11 @@ async def get_user_first_scores( # Step 1: Fetch top score_ids in Python subq = ( select( - col(BestScore.score_id).label("score_id"), - col(BestScore.user_id).label("user_id"), + col(TotalScoreBestScore.score_id).label("score_id"), + col(TotalScoreBestScore.user_id).label("user_id"), rownum, ) - .where(col(BestScore.gamemode) == mode) + .where(col(TotalScoreBestScore.gamemode) == mode) .subquery() ) @@ -734,7 +736,11 @@ async def get_user_first_scores( top_ids = await session.exec(top_ids_stmt) top_ids = list(top_ids) - stmt = select(BestScore).where(col(BestScore.score_id).in_(top_ids)).order_by(col(BestScore.total_score).desc()) + stmt = ( + select(TotalScoreBestScore) + .where(col(TotalScoreBestScore.score_id).in_(top_ids)) + .order_by(col(TotalScoreBestScore.total_score).desc()) + ) result = await session.exec(stmt) return list(result.all()) @@ -744,18 +750,18 @@ async def get_user_first_score_count(session: AsyncSession, user_id: int, mode: rownum = ( func.row_number() .over( - partition_by=(col(BestScore.beatmap_id), col(BestScore.gamemode)), - order_by=col(BestScore.total_score).desc(), + partition_by=(col(TotalScoreBestScore.beatmap_id), col(TotalScoreBestScore.gamemode)), + order_by=col(TotalScoreBestScore.total_score).desc(), ) .label("rn") ) subq = ( select( - col(BestScore.score_id).label("score_id"), - col(BestScore.user_id).label("user_id"), + col(TotalScoreBestScore.score_id).label("score_id"), + col(TotalScoreBestScore.user_id).label("user_id"), rownum, ) - .where(col(BestScore.gamemode) == mode) + .where(col(TotalScoreBestScore.gamemode) == mode) .subquery() ) count_stmt = select(func.count()).where(subq.c.rn == 1, subq.c.user_id == user_id) @@ -769,13 +775,13 @@ async def get_user_best_pp_in_beatmap( beatmap: int, user: int, mode: GameMode, -) -> PPBestScore | None: +) -> BestScore | None: return ( await session.exec( - select(PPBestScore).where( - PPBestScore.beatmap_id == beatmap, - PPBestScore.user_id == user, - PPBestScore.gamemode == mode, + select(BestScore).where( + BestScore.beatmap_id == beatmap, + BestScore.user_id == user, + BestScore.gamemode == mode, ) ) ).first() @@ -800,12 +806,12 @@ async def get_user_best_pp( user: int, mode: GameMode, limit: int = 1000, -) -> Sequence[PPBestScore]: +) -> Sequence[BestScore]: return ( await session.exec( - select(PPBestScore) - .where(PPBestScore.user_id == user, PPBestScore.gamemode == mode) - .order_by(col(PPBestScore.pp).desc()) + select(BestScore) + .where(BestScore.user_id == user, BestScore.gamemode == mode) + .order_by(col(BestScore.pp).desc()) .limit(limit) ) ).all() @@ -936,7 +942,7 @@ async def _process_score_pp(score: Score, session: AsyncSession, redis: Redis, f beatmap_id = score.beatmap_id 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: - best_score = PPBestScore( + best_score = BestScore( user_id=user_id, score_id=score.id, beatmap_id=beatmap_id, @@ -1010,12 +1016,12 @@ async def _process_score_events(score: Score, session: AsyncSession): if rank_global == 1: displaced_score = ( await session.exec( - select(BestScore) + select(TotalScoreBestScore) .where( - BestScore.beatmap_id == score.beatmap_id, - BestScore.gamemode == score.gamemode, + TotalScoreBestScore.beatmap_id == score.beatmap_id, + TotalScoreBestScore.gamemode == score.gamemode, ) - .order_by(col(BestScore.total_score).desc()) + .order_by(col(TotalScoreBestScore.total_score).desc()) .limit(1) .offset(1) ) @@ -1139,7 +1145,7 @@ async def _process_statistics( # 情况2: 有最佳分数记录但没有该mod组合的记录,添加新记录 if previous_score_best is None or previous_score_best_mod is None: session.add( - BestScore( + TotalScoreBestScore( user_id=user.id, beatmap_id=score.beatmap_id, gamemode=score.gamemode, diff --git a/app/database/total_score_best_scores.py b/app/database/total_score_best_scores.py index 963678c..8c52c1e 100644 --- a/app/database/total_score_best_scores.py +++ b/app/database/total_score_best_scores.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: from .score import Score -class BestScore(SQLModel, table=True): +class TotalScoreBestScore(SQLModel, table=True): __tablename__: str = "total_score_best_scores" user_id: int = Field(sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True)) score_id: int = Field(sa_column=Column(BigInteger, ForeignKey("scores.id"), primary_key=True)) @@ -41,7 +41,7 @@ class BestScore(SQLModel, table=True): user: User = Relationship() score: "Score" = Relationship( sa_relationship_kwargs={ - "foreign_keys": "[BestScore.score_id]", + "foreign_keys": "[TotalScoreBestScore.score_id]", "lazy": "joined", }, back_populates="best_score", @@ -75,7 +75,7 @@ class BestScore(SQLModel, table=True): await session.exec( select(func.max(Score.max_combo)).where( Score.user_id == self.user_id, - col(Score.id).in_(select(BestScore.score_id)), + col(Score.id).in_(select(TotalScoreBestScore.score_id)), Score.gamemode == self.gamemode, ) ) diff --git a/app/database/user.py b/app/database/user.py index 70f3ae7..df31913 100644 --- a/app/database/user.py +++ b/app/database/user.py @@ -259,11 +259,11 @@ class UserResp(UserBase): ) -> "UserResp": from app.dependencies.database import get_redis - from .best_scores import PPBestScore + from .best_scores import BestScore from .favourite_beatmapset import FavouriteBeatmapset from .relationship import Relationship, RelationshipResp, RelationshipType from .score import Score, get_user_first_score_count - from .total_score_best_scores import BestScore + from .total_score_best_scores import TotalScoreBestScore ruleset = ruleset or obj.playmode @@ -284,9 +284,9 @@ class UserResp(UserBase): u.scores_best_count = ( await session.exec( select(func.count()) - .select_from(BestScore) + .select_from(TotalScoreBestScore) .where( - BestScore.user_id == obj.id, + TotalScoreBestScore.user_id == obj.id, ) .limit(200) ) @@ -391,10 +391,10 @@ class UserResp(UserBase): u.scores_best_count = ( await session.exec( select(func.count()) - .select_from(PPBestScore) + .select_from(BestScore) .where( - PPBestScore.user_id == obj.id, - PPBestScore.gamemode == ruleset, + BestScore.user_id == obj.id, + BestScore.gamemode == ruleset, ) .limit(200) ) diff --git a/app/router/v1/score.py b/app/router/v1/score.py index f880846..a849228 100644 --- a/app/router/v1/score.py +++ b/app/router/v1/score.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta from typing import Annotated, Literal -from app.database.best_scores import PPBestScore +from app.database.best_scores import BestScore from app.database.score import Score, get_leaderboard from app.dependencies.database import Database from app.models.mods import int_to_mods, mod_to_save, mods_to_int @@ -79,7 +79,7 @@ async def get_user_best( .where( Score.user_id == user if type == "id" or user.isdigit() else col(Score.user).has(username=user), Score.gamemode == GameMode.from_int_extra(ruleset_id), - exists().where(col(PPBestScore.score_id) == Score.id), + exists().where(col(BestScore.score_id) == Score.id), ) .order_by(col(Score.pp).desc()) .options(joinedload(Score.beatmap)) diff --git a/app/router/v2/user.py b/app/router/v2/user.py index 4ffc976..6782c83 100644 --- a/app/router/v2/user.py +++ b/app/router/v2/user.py @@ -10,7 +10,7 @@ from app.database import ( User, UserResp, ) -from app.database.best_scores import PPBestScore +from app.database.best_scores import BestScore from app.database.events import Event from app.database.score import LegacyScoreResp, Score, ScoreResp, get_user_first_scores from app.database.user import SEARCH_INCLUDED @@ -392,7 +392,7 @@ async def get_user_scores( where_clause &= Score.pinned_order > 0 order_by = col(Score.pinned_order).asc() elif type == "best": - where_clause &= exists().where(col(PPBestScore.score_id) == Score.id) + where_clause &= exists().where(col(BestScore.score_id) == Score.id) order_by = col(Score.pp).desc() elif type == "recent": where_clause &= Score.ended_at > utcnow() - timedelta(hours=24) diff --git a/app/tasks/recalculate_banned_beatmap.py b/app/tasks/recalculate_banned_beatmap.py index b88e6df..0295570 100644 --- a/app/tasks/recalculate_banned_beatmap.py +++ b/app/tasks/recalculate_banned_beatmap.py @@ -4,7 +4,7 @@ import json from app.calculator import calculate_pp from app.config import settings from app.database.beatmap import BannedBeatmaps, Beatmap -from app.database.best_scores import PPBestScore +from app.database.best_scores import BestScore from app.database.score import Score, calculate_user_pp from app.database.statistics import UserStatistics from app.dependencies.database import get_redis, with_db @@ -35,7 +35,7 @@ async def recalculate_banned_beatmap(): unbanned_beatmaps = [b for b in last_banned_beatmaps if b not in current_banned] for i in new_banned_beatmaps: last_banned_beatmaps.add(i) - await session.execute(delete(PPBestScore).where(col(PPBestScore.beatmap_id) == i)) + await session.execute(delete(BestScore).where(col(BestScore.beatmap_id) == i)) scores = (await session.exec(select(Score).where(Score.beatmap_id == i, Score.pp > 0))).all() for score in scores: score.pp = 0 @@ -58,7 +58,7 @@ async def recalculate_banned_beatmap(): logger.exception(f"Failed to query scores for unbanned beatmap {beatmap_id}") continue - prev: dict[tuple[int, int], PPBestScore] = {} + prev: dict[tuple[int, int], BestScore] = {} for score in scores: attempts = 3 while attempts > 0: @@ -90,7 +90,7 @@ async def recalculate_banned_beatmap(): continue key = (score.beatmap_id, score.user_id) if key not in prev or prev[key].pp < pp: - best_score = PPBestScore( + best_score = BestScore( user_id=score.user_id, beatmap_id=beatmap_id, acc=score.accuracy, diff --git a/tools/recalculate.py b/tools/recalculate.py index 97f8dfb..66c889a 100644 --- a/tools/recalculate.py +++ b/tools/recalculate.py @@ -12,9 +12,9 @@ from app.calculator import ( ) from app.config import settings from app.const import BANCHOBOT_ID -from app.database import BestScore, UserStatistics +from app.database import TotalScoreBestScore, UserStatistics from app.database.beatmap import Beatmap -from app.database.best_scores import PPBestScore +from app.database.best_scores import BestScore from app.database.score import Score, calculate_playtime, calculate_user_pp from app.dependencies.database import engine, get_redis from app.dependencies.fetcher import get_fetcher @@ -42,8 +42,8 @@ async def recalculate(): fetcher = await get_fetcher() redis = get_redis() for mode in GameMode: - await session.execute(delete(PPBestScore).where(col(PPBestScore.gamemode) == mode)) await session.execute(delete(BestScore).where(col(BestScore.gamemode) == mode)) + await session.execute(delete(TotalScoreBestScore).where(col(TotalScoreBestScore.gamemode) == mode)) await session.commit() logger.info(f"Recalculating for mode: {mode}") statistics_list = ( @@ -63,7 +63,7 @@ async def recalculate(): ) await run_in_batches( [ - _recalculate_best_score(statistics.user_id, statistics.mode, session) + _recalculate_total_score_best_score(statistics.user_id, statistics.mode, session) for statistics in statistics_list ], batch_size=200, @@ -97,7 +97,7 @@ async def _recalculate_pp( ) ) ).all() - prev: dict[int, PPBestScore] = {} + prev: dict[int, BestScore] = {} async def cal(score: Score): time = 10 @@ -120,7 +120,7 @@ async def _recalculate_pp( return score.pp = pp if score.beatmap_id not in prev or prev[score.beatmap_id].pp < pp: - best_score = PPBestScore( + best_score = BestScore( user_id=user_id, beatmap_id=beatmap_id, acc=score.accuracy, @@ -153,13 +153,13 @@ async def _recalculate_pp( session.add(best_score) -async def _recalculate_best_score( +async def _recalculate_total_score_best_score( user_id: int, gamemode: GameMode, session: AsyncSession, ): async with SEMAPHORE: - beatmap_best_score: dict[int, list[BestScore]] = {} + beatmap_best_score: dict[int, list[TotalScoreBestScore]] = {} scores = ( await session.exec( select(Score).where( @@ -176,7 +176,7 @@ async def _recalculate_best_score( ): continue mod_for_save = mod_to_save(score.mods) - bs = BestScore( + bs = TotalScoreBestScore( user_id=score.user_id, score_id=score.id, beatmap_id=score.beatmap_id,