feat(api): 支持 x-api-version (#29)

* feat(relationship): support legacy-compatible response format

* feat(score): add support for legacy score response format in API

* fix(score): avoid missing greenlet

* fix(score): fix missing field for model validation

* feat(user): apply legacy score format for user

* feat(api): use `int` to hint `APIVersion`
This commit is contained in:
MingxuanGame
2025-09-14 14:09:53 +08:00
committed by GitHub
parent e591280620
commit 19f94fffbb
6 changed files with 218 additions and 35 deletions

View File

@@ -47,7 +47,7 @@ from .relationship import (
)
from .score_token import ScoreToken
from pydantic import field_serializer, field_validator
from pydantic import BaseModel, field_serializer, field_validator
from redis.asyncio import Redis
from sqlalchemy import Boolean, Column, DateTime, TextClause
from sqlalchemy.ext.asyncio import AsyncAttrs
@@ -194,6 +194,11 @@ class Score(ScoreBase, table=True):
def is_perfect_combo(self) -> bool:
return self.max_combo == self.beatmap.max_combo
async def to_resp(self, session: AsyncSession, api_version: int) -> "ScoreResp | LegacyScoreResp":
if api_version >= 20220705:
return await ScoreResp.from_db(session, self)
return await LegacyScoreResp.from_db(session, self)
class ScoreResp(ScoreBase):
id: int
@@ -334,6 +339,90 @@ class ScoreResp(ScoreBase):
return s
class LegacyStatistics(BaseModel):
count_300: int
count_100: int
count_50: int
count_miss: int
count_geki: int | None = None
count_katu: int | None = None
class LegacyScoreResp(UTCBaseModel):
accuracy: float
best_id: int
created_at: datetime
id: int
max_combo: int
mode: GameMode
mode_int: int
mods: list[str] # acronym
passed: bool
perfect: bool = False
pp: float
rank: Rank
replay: bool
score: int
statistics: LegacyStatistics
type: str
user_id: int
current_user_attributes: CurrentUserAttributes
user: UserResp
beatmap: BeatmapResp
rank_global: int | None = Field(default=None, exclude=True)
@classmethod
async def from_db(cls, session: AsyncSession, score: Score) -> "LegacyScoreResp":
await session.refresh(score)
await score.awaitable_attrs.beatmap
return cls(
accuracy=score.accuracy,
best_id=await get_best_id(session, score.id) or 0,
created_at=score.started_at,
id=score.id,
max_combo=score.max_combo,
mode=score.gamemode,
mode_int=int(score.gamemode),
mods=[m["acronym"] for m in score.mods],
passed=score.passed,
pp=score.pp,
rank=score.rank,
replay=score.has_replay,
score=score.total_score,
statistics=LegacyStatistics(
count_300=score.n300,
count_100=score.n100,
count_50=score.n50,
count_miss=score.nmiss,
count_geki=score.ngeki or 0,
count_katu=score.nkatu or 0,
),
type=score.type,
user_id=score.user_id,
current_user_attributes=CurrentUserAttributes(
pin=PinAttributes(is_pinned=bool(score.pinned_order), score_id=score.id)
),
user=await UserResp.from_db(
score.user,
session,
include=["statistics", "team", "daily_challenge_user_stats"],
ruleset=score.gamemode,
),
beatmap=await BeatmapResp.from_db(score.beatmap),
perfect=score.is_perfect_combo,
rank_global=(
await get_score_position_by_id(
session,
score.beatmap_id,
score.id,
mode=score.gamemode,
user=score.user,
)
or None
),
)
class MultiplayerScores(RespWithCursor):
scores: list[ScoreResp] = Field(default_factory=list)
params: dict[str, Any] = Field(default_factory=dict)