feat(score): support leaderboard for country/friends/team/selected mods

This commit is contained in:
MingxuanGame
2025-07-31 14:11:42 +00:00
parent be401e8885
commit 1635641654
6 changed files with 284 additions and 216 deletions

View File

@@ -14,6 +14,7 @@ from .lazer_user import (
User, User,
UserResp, UserResp,
) )
from .pp_best_score import PPBestScore
from .relationship import Relationship, RelationshipResp, RelationshipType from .relationship import Relationship, RelationshipResp, RelationshipType
from .score import ( from .score import (
Score, Score,
@@ -35,13 +36,13 @@ from .user_account_history import (
__all__ = [ __all__ = [
"Beatmap", "Beatmap",
"BeatmapResp",
"Beatmapset", "Beatmapset",
"BeatmapsetResp", "BeatmapsetResp",
"BestScore", "BestScore",
"DailyChallengeStats", "DailyChallengeStats",
"DailyChallengeStatsResp", "DailyChallengeStatsResp",
"OAuthToken", "OAuthToken",
"PPBestScore",
"Relationship", "Relationship",
"RelationshipResp", "RelationshipResp",
"RelationshipType", "RelationshipType",

View File

@@ -1,14 +1,14 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from app.models.score import GameMode from app.models.score import GameMode, Rank
from .lazer_user import User from .lazer_user import User
from sqlmodel import ( from sqlmodel import (
JSON,
BigInteger, BigInteger,
Column, Column,
Field, Field,
Float,
ForeignKey, ForeignKey,
Relationship, Relationship,
SQLModel, SQLModel,
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
class BestScore(SQLModel, table=True): class BestScore(SQLModel, table=True):
__tablename__ = "best_scores" # pyright: ignore[reportAssignmentType] __tablename__ = "total_score_best_scores" # pyright: ignore[reportAssignmentType]
user_id: int = Field( user_id: int = Field(
sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True) sa_column=Column(BigInteger, ForeignKey("lazer_users.id"), index=True)
) )
@@ -29,13 +29,20 @@ class BestScore(SQLModel, table=True):
) )
beatmap_id: int = Field(foreign_key="beatmaps.id", index=True) beatmap_id: int = Field(foreign_key="beatmaps.id", index=True)
gamemode: GameMode = Field(index=True) gamemode: GameMode = Field(index=True)
pp: float = Field( total_score: int = Field(
sa_column=Column(Float, default=0), default=0, sa_column=Column(BigInteger, ForeignKey("scores.total_score"))
) )
acc: float = Field( mods: list[str] = Field(
sa_column=Column(Float, default=0), default_factory=list,
sa_column=Column(JSON),
) )
rank: Rank
user: User = Relationship() user: User = Relationship()
score: "Score" = Relationship() score: "Score" = Relationship(
sa_relationship_kwargs={
"foreign_keys": "[BestScore.score_id]",
"lazy": "joined",
}
)
beatmap: "Beatmap" = Relationship() beatmap: "Beatmap" = Relationship()

View File

@@ -0,0 +1,41 @@
from typing import TYPE_CHECKING
from app.models.score import GameMode
from .lazer_user import User
from sqlmodel import (
BigInteger,
Column,
Field,
Float,
ForeignKey,
Relationship,
SQLModel,
)
if TYPE_CHECKING:
from .beatmap import Beatmap
from .score import Score
class PPBestScore(SQLModel, table=True):
__tablename__ = "best_scores" # pyright: ignore[reportAssignmentType]
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)
)
beatmap_id: int = Field(foreign_key="beatmaps.id", index=True)
gamemode: GameMode = Field(index=True)
pp: float = Field(
sa_column=Column(Float, default=0),
)
acc: float = Field(
sa_column=Column(Float, default=0),
)
user: User = Relationship()
score: "Score" = Relationship()
beatmap: "Beatmap" = Relationship()

View File

@@ -1,6 +1,7 @@
import asyncio import asyncio
from collections.abc import Sequence from collections.abc import Sequence
from datetime import UTC, date, datetime from datetime import UTC, date, datetime
import json
import math import math
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -12,7 +13,7 @@ from app.calculator import (
calculate_weighted_pp, calculate_weighted_pp,
clamp, clamp,
) )
from app.models.beatmap import BeatmapRankStatus from app.database.team import TeamMember
from app.models.model import UTCBaseModel from app.models.model import UTCBaseModel
from app.models.mods import APIMod, mods_can_get_pp from app.models.mods import APIMod, mods_can_get_pp
from app.models.score import ( from app.models.score import (
@@ -31,12 +32,18 @@ from .beatmapset import BeatmapsetResp
from .best_score import BestScore from .best_score import BestScore
from .lazer_user import User, UserResp from .lazer_user import User, UserResp
from .monthly_playcounts import MonthlyPlaycounts from .monthly_playcounts import MonthlyPlaycounts
from .pp_best_score import PPBestScore
from .relationship import (
Relationship as DBRelationship,
RelationshipType,
)
from .score_token import ScoreToken from .score_token import ScoreToken
from redis import Redis from redis import Redis
from sqlalchemy import Column, ColumnExpressionArgument, DateTime from sqlalchemy import Column, ColumnExpressionArgument, DateTime
from sqlalchemy.ext.asyncio import AsyncAttrs from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import aliased from sqlalchemy.orm import aliased
from sqlalchemy.sql.elements import ColumnElement
from sqlmodel import ( from sqlmodel import (
JSON, JSON,
BigInteger, BigInteger,
@@ -45,9 +52,10 @@ from sqlmodel import (
Relationship, Relationship,
SQLModel, SQLModel,
col, col,
false,
func, func,
select, select,
text,
true,
) )
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
from sqlmodel.sql._expression_select_cls import SelectOfScalar from sqlmodel.sql._expression_select_cls import SelectOfScalar
@@ -156,9 +164,7 @@ class ScoreResp(ScoreBase):
rank_country: int | None = None rank_country: int | None = None
@classmethod @classmethod
async def from_db( async def from_db(cls, session: AsyncSession, score: Score) -> "ScoreResp":
cls, session: AsyncSession, score: Score, user: User | None = None
) -> "ScoreResp":
s = cls.model_validate(score.model_dump()) s = cls.model_validate(score.model_dump())
assert score.id assert score.id
await score.awaitable_attrs.beatmap await score.awaitable_attrs.beatmap
@@ -195,30 +201,30 @@ class ScoreResp(ScoreBase):
s.maximum_statistics = { s.maximum_statistics = {
HitResult.GREAT: score.beatmap.max_combo, HitResult.GREAT: score.beatmap.max_combo,
} }
if user: s.user = await UserResp.from_db(
s.user = await UserResp.from_db( score.user,
user, session,
session, include=["statistics", "team", "daily_challenge_user_stats"],
include=["statistics", "team", "daily_challenge_user_stats"], ruleset=score.gamemode,
ruleset=score.gamemode, )
)
s.rank_global = ( s.rank_global = (
await get_score_position_by_id( await get_score_position_by_id(
session, session,
score.map_md5, score.beatmap_id,
score.id, score.id,
mode=score.gamemode, mode=score.gamemode,
user=user or score.user, user=score.user,
) )
or None or None
) )
s.rank_country = ( s.rank_country = (
await get_score_position_by_id( await get_score_position_by_id(
session, session,
score.map_md5, score.beatmap_id,
score.id, score.id,
score.gamemode, score.gamemode,
user or score.user, score.user,
type=LeaderboardType.COUNTRY,
) )
or None or None
) )
@@ -228,134 +234,137 @@ class ScoreResp(ScoreBase):
async def get_best_id(session: AsyncSession, score_id: int) -> None: async def get_best_id(session: AsyncSession, score_id: int) -> None:
rownum = ( rownum = (
func.row_number() func.row_number()
.over(partition_by=col(BestScore.user_id), order_by=col(BestScore.pp).desc()) .over(
partition_by=col(PPBestScore.user_id), order_by=col(PPBestScore.pp).desc()
)
.label("rn") .label("rn")
) )
subq = select(BestScore, rownum).subquery() subq = select(PPBestScore, rownum).subquery()
stmt = select(subq.c.rn).where(subq.c.score_id == score_id) stmt = select(subq.c.rn).where(subq.c.score_id == score_id)
result = await session.exec(stmt) result = await session.exec(stmt)
return result.one_or_none() return result.one_or_none()
async def _score_where(
type: LeaderboardType,
beatmap: int,
mode: GameMode,
mods: list[str] | None = None,
user: User | None = None,
) -> list[ColumnElement[bool]] | None:
wheres = [
col(BestScore.beatmap_id) == beatmap,
col(BestScore.gamemode) == mode,
]
if type == LeaderboardType.FRIENDS:
if user and user.is_supporter:
subq = (
select(DBRelationship.target_id)
.where(
DBRelationship.type == RelationshipType.FOLLOW,
DBRelationship.user_id == user.id,
)
.subquery()
)
wheres.append(col(BestScore.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)
)
else:
return None
elif type == LeaderboardType.TEAM:
if 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)
)
)
if mods:
if user and user.is_supporter:
wheres.append(
text(
"JSON_CONTAINS(total_score_best_scores.mods, :w)"
" AND JSON_CONTAINS(:w, total_score_best_scores.mods)"
) # pyright: ignore[reportArgumentType]
)
else:
return None
return wheres
async def get_leaderboard( async def get_leaderboard(
session: AsyncSession, session: AsyncSession,
beatmap_md5: str, beatmap: int,
mode: GameMode, mode: GameMode,
type: LeaderboardType = LeaderboardType.GLOBAL, type: LeaderboardType = LeaderboardType.GLOBAL,
mods: list[APIMod] | None = None, mods: list[str] | None = None,
user: User | None = None, user: User | None = None,
limit: int = 50, limit: int = 50,
) -> list[Score]: ) -> tuple[list[Score], Score | None]:
scores = [] wheres = await _score_where(type, beatmap, mode, mods, user)
if type == LeaderboardType.GLOBAL: if wheres is None:
query = ( return [], None
select(Score) query = (
.where( select(BestScore)
col(Beatmap.beatmap_status).in_( .where(*wheres)
[ .limit(limit)
BeatmapRankStatus.RANKED, .order_by(col(BestScore.total_score).desc())
BeatmapRankStatus.LOVED, )
BeatmapRankStatus.QUALIFIED, if mods:
BeatmapRankStatus.APPROVED, query = query.params(w=json.dumps(mods))
] scores = [s.score for s in await session.exec(query)]
), user_score = None
Score.map_md5 == beatmap_md5,
Score.gamemode == mode,
col(Score.passed).is_(True),
Score.mods == mods if user and user.is_supporter else false(),
)
.limit(limit)
.order_by(
col(Score.total_score).desc(),
)
)
result = await session.exec(query)
scores = list[Score](result.all())
elif type == LeaderboardType.FRIENDS and user and user.is_supporter:
# TODO
...
elif type == LeaderboardType.TEAM and user and user.team_membership:
team_id = user.team_membership.team_id
query = (
select(Score)
.join(Beatmap)
.where(
Score.map_md5 == beatmap_md5,
Score.gamemode == mode,
col(Score.passed).is_(True),
col(Score.user.team_membership).is_not(None),
Score.user.team_membership.team_id == team_id, # pyright: ignore[reportOptionalMemberAccess]
Score.mods == mods if user and user.is_supporter else false(),
)
.limit(limit)
.order_by(
col(Score.total_score).desc(),
)
)
result = await session.exec(query)
scores = list[Score](result.all())
if user: if user:
user_score = ( self_query = (
await session.exec( select(BestScore)
select(Score).where( .where(BestScore.user_id == user.id)
Score.map_md5 == beatmap_md5, .order_by(col(BestScore.total_score).desc())
Score.gamemode == mode, .limit(1)
Score.user_id == user.id, )
col(Score.passed).is_(True), if mods:
self_query = self_query.where(
text(
"JSON_CONTAINS(total_score_best_scores.mods, :w)"
" AND JSON_CONTAINS(:w, total_score_best_scores.mods)"
) )
) ).params(w=json.dumps(mods))
).first() user_bs = (await session.exec(self_query)).first()
if user_bs:
user_score = user_bs.score
if user_score and user_score not in scores: if user_score and user_score not in scores:
scores.append(user_score) scores.append(user_score)
return scores return scores, user_score
async def get_score_position_by_user( async def get_score_position_by_user(
session: AsyncSession, session: AsyncSession,
beatmap_md5: str, beatmap: int,
user: User, user: User,
mode: GameMode, mode: GameMode,
type: LeaderboardType = LeaderboardType.GLOBAL, type: LeaderboardType = LeaderboardType.GLOBAL,
mods: list[APIMod] | None = None, mods: list[str] | None = None,
) -> int: ) -> int:
where_clause = [ wheres = await _score_where(type, beatmap, mode, mods, user=user)
Score.map_md5 == beatmap_md5, if wheres is None:
Score.gamemode == mode, return 0
col(Score.passed).is_(True),
col(Beatmap.beatmap_status).in_(
[
BeatmapRankStatus.RANKED,
BeatmapRankStatus.LOVED,
BeatmapRankStatus.QUALIFIED,
BeatmapRankStatus.APPROVED,
]
),
]
if mods and user.is_supporter:
where_clause.append(Score.mods == mods)
else:
where_clause.append(false())
if type == LeaderboardType.FRIENDS and user.is_supporter:
# TODO
...
elif type == LeaderboardType.TEAM and user.team_membership:
team_id = user.team_membership.team_id
where_clause.append(
col(Score.user.team_membership).is_not(None),
)
where_clause.append(
Score.user.team_membership.team_id == team_id, # pyright: ignore[reportOptionalMemberAccess]
)
rownum = ( rownum = (
func.row_number() func.row_number()
.over( .over(
partition_by=Score.map_md5, partition_by=col(BestScore.beatmap_id),
order_by=col(Score.total_score).desc(), order_by=col(BestScore.total_score).desc(),
) )
.label("row_number") .label("row_number")
) )
subq = select(Score, rownum).join(Beatmap).where(*where_clause).subquery() subq = select(BestScore, rownum).join(Beatmap).where(*wheres).subquery()
stmt = select(subq.c.row_number).where(subq.c.user == user) stmt = select(subq.c.row_number).where(subq.c.user_id == user.id)
result = await session.exec(stmt) result = await session.exec(stmt)
s = result.one_or_none() s = result.one_or_none()
return s if s else 0 return s if s else 0
@@ -363,57 +372,26 @@ async def get_score_position_by_user(
async def get_score_position_by_id( async def get_score_position_by_id(
session: AsyncSession, session: AsyncSession,
beatmap_md5: str, beatmap: int,
score_id: int, score_id: int,
mode: GameMode, mode: GameMode,
user: User | None = None, user: User | None = None,
type: LeaderboardType = LeaderboardType.GLOBAL, type: LeaderboardType = LeaderboardType.GLOBAL,
mods: list[APIMod] | None = None, mods: list[str] | None = None,
) -> int: ) -> int:
where_clause = [ wheres = await _score_where(type, beatmap, mode, mods, user=user)
Score.map_md5 == beatmap_md5, if wheres is None:
Score.id == score_id, return 0
Score.gamemode == mode,
col(Score.passed).is_(True),
col(Beatmap.beatmap_status).in_(
[
BeatmapRankStatus.RANKED,
BeatmapRankStatus.LOVED,
BeatmapRankStatus.QUALIFIED,
BeatmapRankStatus.APPROVED,
]
),
]
if mods and user and user.is_supporter:
where_clause.append(Score.mods == mods)
elif mods:
where_clause.append(false())
rownum = ( rownum = (
func.row_number() func.row_number()
.over( .over(
partition_by=[col(Score.user_id), col(Score.map_md5)], partition_by=col(BestScore.beatmap_id),
order_by=col(Score.total_score).desc(), order_by=col(BestScore.total_score).desc(),
) )
.label("rownum") .label("row_number")
) )
subq = ( subq = select(BestScore, rownum).join(Beatmap).where(*wheres).subquery()
select(Score.user_id, Score.id, Score.total_score, rownum) stmt = select(subq.c.row_number).where(subq.c.score_id == score_id)
.join(Beatmap)
.where(*where_clause)
.subquery()
)
best_scores = aliased(subq)
overall_rank = (
func.rank().over(order_by=best_scores.c.total_score.desc()).label("global_rank")
)
final_q = (
select(best_scores.c.id, overall_rank)
.select_from(best_scores)
.where(best_scores.c.rownum == 1)
.subquery()
)
stmt = select(final_q.c.global_rank).where(final_q.c.id == score_id)
result = await session.exec(stmt) result = await session.exec(stmt)
s = result.one_or_none() s = result.one_or_none()
return s if s else 0 return s if s else 0
@@ -424,16 +402,38 @@ async def get_user_best_score_in_beatmap(
beatmap: int, beatmap: int,
user: int, user: int,
mode: GameMode | None = None, mode: GameMode | None = None,
) -> Score | None: ) -> BestScore | None:
return ( return (
await session.exec( await session.exec(
select(Score) select(BestScore)
.where( .where(
Score.gamemode == mode if mode is not None else True, BestScore.gamemode == mode if mode is not None else true(),
Score.beatmap_id == beatmap, BestScore.beatmap_id == beatmap,
Score.user_id == user, BestScore.user_id == user,
) )
.order_by(col(Score.total_score).desc()) .order_by(col(BestScore.total_score).desc())
)
).first()
# FIXME
async def get_user_best_score_with_mod_in_beatmap(
session: AsyncSession,
beatmap: int,
user: int,
mod: list[str],
mode: GameMode | None = None,
) -> BestScore | None:
return (
await session.exec(
select(BestScore)
.where(
BestScore.gamemode == mode if mode is not None else True,
BestScore.beatmap_id == beatmap,
BestScore.user_id == user,
# BestScore.mods == mod,
)
.order_by(col(BestScore.total_score).desc())
) )
).first() ).first()
@@ -443,13 +443,13 @@ async def get_user_best_pp_in_beatmap(
beatmap: int, beatmap: int,
user: int, user: int,
mode: GameMode, mode: GameMode,
) -> BestScore | None: ) -> PPBestScore | None:
return ( return (
await session.exec( await session.exec(
select(BestScore).where( select(PPBestScore).where(
BestScore.beatmap_id == beatmap, PPBestScore.beatmap_id == beatmap,
BestScore.user_id == user, PPBestScore.user_id == user,
BestScore.gamemode == mode, PPBestScore.gamemode == mode,
) )
) )
).first() ).first()
@@ -459,12 +459,12 @@ async def get_user_best_pp(
session: AsyncSession, session: AsyncSession,
user: int, user: int,
limit: int = 200, limit: int = 200,
) -> Sequence[BestScore]: ) -> Sequence[PPBestScore]:
return ( return (
await session.exec( await session.exec(
select(BestScore) select(PPBestScore)
.where(BestScore.user_id == user) .where(PPBestScore.user_id == user)
.order_by(col(BestScore.pp).desc()) .order_by(col(PPBestScore.pp).desc())
.limit(limit) .limit(limit)
) )
).all() ).all()
@@ -474,9 +474,15 @@ async def process_user(
session: AsyncSession, user: User, score: Score, ranked: bool = False session: AsyncSession, user: User, score: Score, ranked: bool = False
): ):
assert user.id assert user.id
assert score.id
mod_for_save = list({mod["acronym"] for mod in score.mods})
previous_score_best = await get_user_best_score_in_beatmap( previous_score_best = await get_user_best_score_in_beatmap(
session, score.beatmap_id, user.id, score.gamemode session, score.beatmap_id, user.id, score.gamemode
) )
previous_score_best_mod = await get_user_best_score_with_mod_in_beatmap(
session, score.beatmap_id, user.id, mod_for_save, score.gamemode
)
print(previous_score_best, previous_score_best_mod)
add_to_db = False add_to_db = False
mouthly_playcount = ( mouthly_playcount = (
await session.exec( await session.exec(
@@ -493,7 +499,7 @@ async def process_user(
) )
add_to_db = True add_to_db = True
statistics = None statistics = None
for i in user.statistics: for i in await user.awaitable_attrs.statistics:
if i.mode == score.gamemode.value: if i.mode == score.gamemode.value:
statistics = i statistics = i
break break
@@ -506,7 +512,7 @@ async def process_user(
statistics.total_score += score.total_score statistics.total_score += score.total_score
difference = ( difference = (
score.total_score - previous_score_best.total_score score.total_score - previous_score_best.total_score
if previous_score_best and previous_score_best.id != score.id if previous_score_best
else score.total_score else score.total_score
) )
if difference > 0 and score.passed and ranked: if difference > 0 and score.passed and ranked:
@@ -533,9 +539,41 @@ async def process_user(
statistics.grade_sh -= 1 statistics.grade_sh -= 1
case Rank.A: case Rank.A:
statistics.grade_a -= 1 statistics.grade_a -= 1
else:
previous_score_best = BestScore(
user_id=user.id,
beatmap_id=score.beatmap_id,
gamemode=score.gamemode,
score_id=score.id,
total_score=score.total_score,
rank=score.rank,
mods=mod_for_save,
)
session.add(previous_score_best)
statistics.ranked_score += difference statistics.ranked_score += difference
statistics.level_current = calculate_score_to_level(statistics.ranked_score) statistics.level_current = calculate_score_to_level(statistics.ranked_score)
statistics.maximum_combo = max(statistics.maximum_combo, score.max_combo) statistics.maximum_combo = max(statistics.maximum_combo, score.max_combo)
if score.passed and ranked:
if previous_score_best_mod is not None:
previous_score_best_mod.mods = mod_for_save
previous_score_best_mod.score_id = score.id
previous_score_best_mod.rank = score.rank
previous_score_best_mod.total_score = score.total_score
elif (
previous_score_best is not None and previous_score_best.score_id != score.id
):
session.add(
BestScore(
user_id=user.id,
beatmap_id=score.beatmap_id,
gamemode=score.gamemode,
score_id=score.id,
total_score=score.total_score,
rank=score.rank,
mods=mod_for_save,
)
)
statistics.play_count += 1 statistics.play_count += 1
mouthly_playcount.playcount += 1 mouthly_playcount.playcount += 1
statistics.play_time += int((score.ended_at - score.started_at).total_seconds()) statistics.play_time += int((score.ended_at - score.started_at).total_seconds())
@@ -623,7 +661,7 @@ async def process_score(
) )
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:
assert score.id assert score.id
best_score = BestScore( best_score = PPBestScore(
user_id=user_id, user_id=user_id,
score_id=score.id, score_id=score.id,
beatmap_id=beatmap_id, beatmap_id=beatmap_id,

View File

@@ -132,7 +132,7 @@ class HitResultInt(IntEnum):
class LeaderboardType(Enum): class LeaderboardType(Enum):
GLOBAL = "global" GLOBAL = "global"
FRIENDS = "friends" FRIENDS = "friend"
COUNTRY = "country" COUNTRY = "country"
TEAM = "team" TEAM = "team"

View File

@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from app.database import Beatmap, Score, ScoreResp, ScoreToken, ScoreTokenResp, User from app.database import Beatmap, Score, ScoreResp, ScoreToken, ScoreTokenResp, User
from app.database.score import process_score, process_user from app.database.score import get_leaderboard, process_score, process_user
from app.dependencies.database import get_db, get_redis from app.dependencies.database import get_db, get_redis
from app.dependencies.fetcher import get_fetcher from app.dependencies.fetcher import get_fetcher
from app.dependencies.user import get_current_user from app.dependencies.user import get_current_user
@@ -9,6 +9,7 @@ from app.models.beatmap import BeatmapRankStatus
from app.models.score import ( from app.models.score import (
INT_TO_MODE, INT_TO_MODE,
GameMode, GameMode,
LeaderboardType,
Rank, Rank,
SoloScoreSubmissionInfo, SoloScoreSubmissionInfo,
) )
@@ -19,7 +20,7 @@ from fastapi import Depends, Form, HTTPException, Query
from pydantic import BaseModel from pydantic import BaseModel
from redis import Redis from redis import Redis
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from sqlmodel import col, select, true from sqlmodel import col, select
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
@@ -33,44 +34,26 @@ class BeatmapScores(BaseModel):
) )
async def get_beatmap_scores( async def get_beatmap_scores(
beatmap: int, beatmap: int,
mode: GameMode,
legacy_only: bool = Query(None), # TODO:加入对这个参数的查询 legacy_only: bool = Query(None), # TODO:加入对这个参数的查询
mode: GameMode | None = Query(None), mods: list[str] = Query(default_factory=set, alias="mods[]"),
# mods: List[APIMod] = Query(None), # TODO:加入指定MOD的查询 type: LeaderboardType = Query(LeaderboardType.GLOBAL),
type: str = Query(None),
current_user: User = Depends(get_current_user), current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
limit: int = Query(50, ge=1, le=200),
): ):
if legacy_only: if legacy_only:
raise HTTPException( raise HTTPException(
status_code=404, detail="this server only contains lazer scores" status_code=404, detail="this server only contains lazer scores"
) )
all_scores = ( all_scores, user_score = await get_leaderboard(
await db.exec( db, beatmap, mode, type=type, user=current_user, limit=limit, mods=mods
Score.select_clause_unique( )
Score.beatmap_id == beatmap,
col(Score.passed).is_(True),
Score.gamemode == mode if mode is not None else true(),
)
)
).all()
user_score = (
await db.exec(
Score.select_clause_unique(
Score.beatmap_id == beatmap,
Score.user_id == current_user.id,
col(Score.passed).is_(True),
Score.gamemode == mode if mode is not None else true(),
)
)
).first()
return BeatmapScores( return BeatmapScores(
scores=[await ScoreResp.from_db(db, score, score.user) for score in all_scores], scores=[await ScoreResp.from_db(db, score) for score in all_scores],
userScore=await ScoreResp.from_db(db, user_score, user_score.user) userScore=await ScoreResp.from_db(db, user_score) if user_score else None,
if user_score
else None,
) )
@@ -116,7 +99,7 @@ async def get_user_beatmap_score(
else: else:
return BeatmapUserScore( return BeatmapUserScore(
position=user_score.position if user_score.position is not None else 0, position=user_score.position if user_score.position is not None else 0,
score=await ScoreResp.from_db(db, user_score, user_score.user), score=await ScoreResp.from_db(db, user_score),
) )
@@ -149,9 +132,7 @@ async def get_user_all_beatmap_scores(
) )
).all() ).all()
return [ return [await ScoreResp.from_db(db, score) for score in all_user_scores]
await ScoreResp.from_db(db, score, current_user) for score in all_user_scores
]
@router.post( @router.post(
@@ -243,4 +224,4 @@ async def submit_solo_score(
await process_user(db, current_user, score, ranked) await process_user(db, current_user, score, ranked)
score = (await db.exec(select(Score).where(Score.id == score_id))).first() score = (await db.exec(select(Score).where(Score.id == score_id))).first()
assert score is not None assert score is not None
return await ScoreResp.from_db(db, score, current_user) return await ScoreResp.from_db(db, score)