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:
@@ -47,7 +47,7 @@ from .relationship import (
|
|||||||
)
|
)
|
||||||
from .score_token import ScoreToken
|
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 redis.asyncio import Redis
|
||||||
from sqlalchemy import Boolean, Column, DateTime, TextClause
|
from sqlalchemy import Boolean, Column, DateTime, TextClause
|
||||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
from sqlalchemy.ext.asyncio import AsyncAttrs
|
||||||
@@ -194,6 +194,11 @@ class Score(ScoreBase, table=True):
|
|||||||
def is_perfect_combo(self) -> bool:
|
def is_perfect_combo(self) -> bool:
|
||||||
return self.max_combo == self.beatmap.max_combo
|
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):
|
class ScoreResp(ScoreBase):
|
||||||
id: int
|
id: int
|
||||||
@@ -334,6 +339,90 @@ class ScoreResp(ScoreBase):
|
|||||||
return s
|
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):
|
class MultiplayerScores(RespWithCursor):
|
||||||
scores: list[ScoreResp] = Field(default_factory=list)
|
scores: list[ScoreResp] = Field(default_factory=list)
|
||||||
params: dict[str, Any] = Field(default_factory=dict)
|
params: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|||||||
16
app/dependencies/api_version.py
Normal file
16
app/dependencies/api_version.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Depends, Header
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_version(version: int | None = Header(None, alias="x-api-version")) -> int:
|
||||||
|
if version is None:
|
||||||
|
return 0
|
||||||
|
if version < 1:
|
||||||
|
raise ValueError
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
APIVersion = Annotated[int, Depends(get_api_version)]
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from app.database import Relationship, RelationshipResp, RelationshipType, User
|
from app.database import Relationship, RelationshipResp, RelationshipType, User
|
||||||
|
from app.database.lazer_user import UserResp
|
||||||
|
from app.dependencies.api_version import APIVersion
|
||||||
from app.dependencies.database import Database
|
from app.dependencies.database import Database
|
||||||
from app.dependencies.user import get_client_user, get_current_user
|
from app.dependencies.user import get_client_user, get_current_user
|
||||||
|
|
||||||
@@ -14,9 +16,34 @@ from sqlmodel import exists, select
|
|||||||
@router.get(
|
@router.get(
|
||||||
"/friends",
|
"/friends",
|
||||||
tags=["用户关系"],
|
tags=["用户关系"],
|
||||||
response_model=list[RelationshipResp],
|
responses={
|
||||||
|
200: {
|
||||||
|
"description": "好友列表",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/components/schemas/RelationshipResp"},
|
||||||
|
"description": "好友列表",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {"$ref": "#/components/schemas/UserResp"},
|
||||||
|
"description": "好友列表 (`x-api-version < 20241022`)",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
name="获取好友列表",
|
name="获取好友列表",
|
||||||
description="获取当前用户的好友列表。",
|
description=(
|
||||||
|
"获取当前用户的好友列表。\n\n"
|
||||||
|
"如果 `x-api-version < 20241022`,返回值为 `UserResp` 列表,否则为 `RelationshipResp` 列表。"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
@router.get(
|
@router.get(
|
||||||
"/blocks",
|
"/blocks",
|
||||||
@@ -28,6 +55,7 @@ from sqlmodel import exists, select
|
|||||||
async def get_relationship(
|
async def get_relationship(
|
||||||
db: Database,
|
db: Database,
|
||||||
request: Request,
|
request: Request,
|
||||||
|
api_version: APIVersion,
|
||||||
current_user: User = Security(get_current_user, scopes=["friends.read"]),
|
current_user: User = Security(get_current_user, scopes=["friends.read"]),
|
||||||
):
|
):
|
||||||
relationship_type = RelationshipType.FOLLOW if request.url.path.endswith("/friends") else RelationshipType.BLOCK
|
relationship_type = RelationshipType.FOLLOW if request.url.path.endswith("/friends") else RelationshipType.BLOCK
|
||||||
@@ -37,7 +65,22 @@ async def get_relationship(
|
|||||||
Relationship.type == relationship_type,
|
Relationship.type == relationship_type,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return [await RelationshipResp.from_db(db, rel) for rel in relationships.unique()]
|
if api_version >= 20241022 or relationship_type == RelationshipType.BLOCK:
|
||||||
|
return [await RelationshipResp.from_db(db, rel) for rel in relationships.unique()]
|
||||||
|
else:
|
||||||
|
return [
|
||||||
|
await UserResp.from_db(
|
||||||
|
rel.target,
|
||||||
|
db,
|
||||||
|
include=[
|
||||||
|
"team",
|
||||||
|
"daily_challenge_user_stats",
|
||||||
|
"statistics",
|
||||||
|
"statistics_rulesets",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
for rel in relationships.unique()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class AddFriendResp(BaseModel):
|
class AddFriendResp(BaseModel):
|
||||||
|
|||||||
@@ -29,12 +29,14 @@ from app.database.playlist_best_score import (
|
|||||||
)
|
)
|
||||||
from app.database.relationship import Relationship, RelationshipType
|
from app.database.relationship import Relationship, RelationshipType
|
||||||
from app.database.score import (
|
from app.database.score import (
|
||||||
|
LegacyScoreResp,
|
||||||
MultiplayerScores,
|
MultiplayerScores,
|
||||||
ScoreAround,
|
ScoreAround,
|
||||||
get_leaderboard,
|
get_leaderboard,
|
||||||
process_score,
|
process_score,
|
||||||
process_user,
|
process_user,
|
||||||
)
|
)
|
||||||
|
from app.dependencies.api_version import APIVersion
|
||||||
from app.dependencies.database import Database, get_redis, with_db
|
from app.dependencies.database import Database, get_redis, with_db
|
||||||
from app.dependencies.fetcher import get_fetcher
|
from app.dependencies.fetcher import get_fetcher
|
||||||
from app.dependencies.storage import get_storage_service
|
from app.dependencies.storage import get_storage_service
|
||||||
@@ -264,21 +266,31 @@ async def _preload_beatmap_for_pp_calculation(beatmap_id: int) -> None:
|
|||||||
logger.warning(f"Failed to preload beatmap {beatmap_id}: {e}")
|
logger.warning(f"Failed to preload beatmap {beatmap_id}: {e}")
|
||||||
|
|
||||||
|
|
||||||
class BeatmapScores(BaseModel):
|
class BeatmapUserScore[T: ScoreResp | LegacyScoreResp](BaseModel):
|
||||||
scores: list[ScoreResp]
|
position: int
|
||||||
user_score: BeatmapUserScore | None = None
|
score: T
|
||||||
|
|
||||||
|
|
||||||
|
class BeatmapScores[T: ScoreResp | LegacyScoreResp](BaseModel):
|
||||||
|
scores: list[T]
|
||||||
|
user_score: BeatmapUserScore[T] | None = None
|
||||||
score_count: int = 0
|
score_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/beatmaps/{beatmap_id}/scores",
|
"/beatmaps/{beatmap_id}/scores",
|
||||||
tags=["成绩"],
|
tags=["成绩"],
|
||||||
response_model=BeatmapScores,
|
response_model=BeatmapScores[ScoreResp] | BeatmapScores[LegacyScoreResp],
|
||||||
name="获取谱面排行榜",
|
name="获取谱面排行榜",
|
||||||
description="获取指定谱面在特定条件下的排行榜及当前用户成绩。",
|
description=(
|
||||||
|
"获取指定谱面在特定条件下的排行榜及当前用户成绩。\n\n"
|
||||||
|
"如果 `x-api-version >= 20220705`,返回值为 `BeatmapScores[ScoreResp]`,"
|
||||||
|
"否则为 `BeatmapScores[LegacyScoreResp]`。"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
async def get_beatmap_scores(
|
async def get_beatmap_scores(
|
||||||
db: Database,
|
db: Database,
|
||||||
|
api_version: APIVersion,
|
||||||
beatmap_id: int = Path(description="谱面 ID"),
|
beatmap_id: int = Path(description="谱面 ID"),
|
||||||
mode: GameMode = Query(description="指定 auleset"),
|
mode: GameMode = Query(description="指定 auleset"),
|
||||||
legacy_only: bool = Query(None, description="是否只查询 Stable 分数"),
|
legacy_only: bool = Query(None, description="是否只查询 Stable 分数"),
|
||||||
@@ -303,9 +315,9 @@ async def get_beatmap_scores(
|
|||||||
mods=sorted(mods),
|
mods=sorted(mods),
|
||||||
)
|
)
|
||||||
|
|
||||||
user_score_resp = await ScoreResp.from_db(db, user_score) if user_score else None
|
user_score_resp = await user_score.to_resp(db, api_version) if user_score else None
|
||||||
resp = BeatmapScores(
|
resp = BeatmapScores(
|
||||||
scores=[await ScoreResp.from_db(db, score) for score in all_scores],
|
scores=[await score.to_resp(db, api_version) for score in all_scores],
|
||||||
user_score=BeatmapUserScore(score=user_score_resp, position=user_score_resp.rank_global or 0)
|
user_score=BeatmapUserScore(score=user_score_resp, position=user_score_resp.rank_global or 0)
|
||||||
if user_score_resp
|
if user_score_resp
|
||||||
else None,
|
else None,
|
||||||
@@ -314,20 +326,20 @@ async def get_beatmap_scores(
|
|||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
class BeatmapUserScore(BaseModel):
|
|
||||||
position: int
|
|
||||||
score: ScoreResp
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/beatmaps/{beatmap_id}/scores/users/{user_id}",
|
"/beatmaps/{beatmap_id}/scores/users/{user_id}",
|
||||||
tags=["成绩"],
|
tags=["成绩"],
|
||||||
response_model=BeatmapUserScore,
|
response_model=BeatmapUserScore[ScoreResp] | BeatmapUserScore[LegacyScoreResp],
|
||||||
name="获取用户谱面最高成绩",
|
name="获取用户谱面最高成绩",
|
||||||
description="获取指定用户在指定谱面上的最高成绩。",
|
description=(
|
||||||
|
"获取指定用户在指定谱面上的最高成绩。\n\n"
|
||||||
|
"如果 `x-api-version >= 20220705`,返回值为 `BeatmapUserScore[ScoreResp]`,"
|
||||||
|
"否则为 `BeatmapUserScore[LegacyScoreResp]`。"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
async def get_user_beatmap_score(
|
async def get_user_beatmap_score(
|
||||||
db: Database,
|
db: Database,
|
||||||
|
api_version: APIVersion,
|
||||||
beatmap_id: int = Path(description="谱面 ID"),
|
beatmap_id: int = Path(description="谱面 ID"),
|
||||||
user_id: int = Path(description="用户 ID"),
|
user_id: int = Path(description="用户 ID"),
|
||||||
legacy_only: bool = Query(None, description="是否只查询 Stable 分数"),
|
legacy_only: bool = Query(None, description="是否只查询 Stable 分数"),
|
||||||
@@ -355,7 +367,7 @@ async def get_user_beatmap_score(
|
|||||||
detail=f"Cannot find user {user_id}'s score on this beatmap",
|
detail=f"Cannot find user {user_id}'s score on this beatmap",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
resp = await ScoreResp.from_db(db, user_score)
|
resp = await user_score.to_resp(db, api_version=api_version)
|
||||||
return BeatmapUserScore(
|
return BeatmapUserScore(
|
||||||
position=resp.rank_global or 0,
|
position=resp.rank_global or 0,
|
||||||
score=resp,
|
score=resp,
|
||||||
@@ -365,12 +377,17 @@ async def get_user_beatmap_score(
|
|||||||
@router.get(
|
@router.get(
|
||||||
"/beatmaps/{beatmap_id}/scores/users/{user_id}/all",
|
"/beatmaps/{beatmap_id}/scores/users/{user_id}/all",
|
||||||
tags=["成绩"],
|
tags=["成绩"],
|
||||||
response_model=list[ScoreResp],
|
response_model=list[ScoreResp] | list[LegacyScoreResp],
|
||||||
name="获取用户谱面全部成绩",
|
name="获取用户谱面全部成绩",
|
||||||
description="获取指定用户在指定谱面上的全部成绩列表。",
|
description=(
|
||||||
|
"获取指定用户在指定谱面上的全部成绩列表。\n\n"
|
||||||
|
"如果 `x-api-version >= 20220705`,返回值为 `ScoreResp`列表,"
|
||||||
|
"否则为 `LegacyScoreResp`列表。"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
async def get_user_all_beatmap_scores(
|
async def get_user_all_beatmap_scores(
|
||||||
db: Database,
|
db: Database,
|
||||||
|
api_version: APIVersion,
|
||||||
beatmap_id: int = Path(description="谱面 ID"),
|
beatmap_id: int = Path(description="谱面 ID"),
|
||||||
user_id: int = Path(description="用户 ID"),
|
user_id: int = Path(description="用户 ID"),
|
||||||
legacy_only: bool = Query(None, description="是否只查询 Stable 分数"),
|
legacy_only: bool = Query(None, description="是否只查询 Stable 分数"),
|
||||||
@@ -390,7 +407,7 @@ async def get_user_all_beatmap_scores(
|
|||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
return [await ScoreResp.from_db(db, score) for score in all_user_scores]
|
return [await score.to_resp(db, api_version) for score in all_user_scores]
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ from app.database import (
|
|||||||
from app.database.events import Event
|
from app.database.events import Event
|
||||||
from app.database.lazer_user import SEARCH_INCLUDED
|
from app.database.lazer_user import SEARCH_INCLUDED
|
||||||
from app.database.pp_best_score import PPBestScore
|
from app.database.pp_best_score import PPBestScore
|
||||||
from app.database.score import Score, ScoreResp, get_user_first_scores
|
from app.database.score import LegacyScoreResp, Score, ScoreResp, get_user_first_scores
|
||||||
|
from app.dependencies.api_version import APIVersion
|
||||||
from app.dependencies.database import Database, get_redis
|
from app.dependencies.database import Database, get_redis
|
||||||
from app.dependencies.user import get_current_user
|
from app.dependencies.user import get_current_user
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
@@ -313,13 +314,18 @@ async def get_user_beatmapsets(
|
|||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/users/{user_id}/scores/{type}",
|
"/users/{user_id}/scores/{type}",
|
||||||
response_model=list[ScoreResp],
|
response_model=list[ScoreResp] | list[LegacyScoreResp],
|
||||||
name="获取用户成绩列表",
|
name="获取用户成绩列表",
|
||||||
description="获取用户特定类型的成绩列表,如最好成绩、最近成绩等。",
|
description=(
|
||||||
|
"获取用户特定类型的成绩列表,如最好成绩、最近成绩等。\n\n"
|
||||||
|
"如果 `x-api-version >= 20220705`,返回值为 `ScoreResp`列表,"
|
||||||
|
"否则为 `LegacyScoreResp`列表。"
|
||||||
|
),
|
||||||
tags=["用户"],
|
tags=["用户"],
|
||||||
)
|
)
|
||||||
async def get_user_scores(
|
async def get_user_scores(
|
||||||
session: Database,
|
session: Database,
|
||||||
|
api_version: APIVersion,
|
||||||
background_task: BackgroundTasks,
|
background_task: BackgroundTasks,
|
||||||
user_id: int = Path(description="用户 ID"),
|
user_id: int = Path(description="用户 ID"),
|
||||||
type: Literal["best", "recent", "firsts", "pinned"] = Path(
|
type: Literal["best", "recent", "firsts", "pinned"] = Path(
|
||||||
@@ -332,12 +338,15 @@ async def get_user_scores(
|
|||||||
offset: int = Query(0, ge=0, description="偏移量"),
|
offset: int = Query(0, ge=0, description="偏移量"),
|
||||||
current_user: User = Security(get_current_user, scopes=["public"]),
|
current_user: User = Security(get_current_user, scopes=["public"]),
|
||||||
):
|
):
|
||||||
|
is_legacy_api = api_version < 20220705
|
||||||
redis = get_redis()
|
redis = get_redis()
|
||||||
cache_service = get_user_cache_service(redis)
|
cache_service = get_user_cache_service(redis)
|
||||||
|
|
||||||
# 先尝试从缓存获取(对于recent类型使用较短的缓存时间)
|
# 先尝试从缓存获取(对于recent类型使用较短的缓存时间)
|
||||||
cache_expire = 30 if type == "recent" else settings.user_scores_cache_expire_seconds
|
cache_expire = 30 if type == "recent" else settings.user_scores_cache_expire_seconds
|
||||||
cached_scores = await cache_service.get_user_scores_from_cache(user_id, type, include_fails, mode, limit, offset)
|
cached_scores = await cache_service.get_user_scores_from_cache(
|
||||||
|
user_id, type, include_fails, mode, limit, offset, is_legacy_api
|
||||||
|
)
|
||||||
if cached_scores is not None:
|
if cached_scores is not None:
|
||||||
return cached_scores
|
return cached_scores
|
||||||
|
|
||||||
@@ -373,9 +382,9 @@ async def get_user_scores(
|
|||||||
scores = [best_score.score for best_score in best_scores]
|
scores = [best_score.score for best_score in best_scores]
|
||||||
|
|
||||||
score_responses = [
|
score_responses = [
|
||||||
await ScoreResp.from_db(
|
await score.to_resp(
|
||||||
session,
|
session,
|
||||||
score,
|
api_version,
|
||||||
)
|
)
|
||||||
for score in scores
|
for score in scores
|
||||||
]
|
]
|
||||||
@@ -385,12 +394,13 @@ async def get_user_scores(
|
|||||||
cache_service.cache_user_scores,
|
cache_service.cache_user_scores,
|
||||||
user_id,
|
user_id,
|
||||||
type,
|
type,
|
||||||
score_responses,
|
score_responses, # pyright: ignore[reportArgumentType]
|
||||||
include_fails,
|
include_fails,
|
||||||
mode,
|
mode,
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
cache_expire,
|
cache_expire,
|
||||||
|
is_legacy_api,
|
||||||
)
|
)
|
||||||
|
|
||||||
return score_responses
|
return score_responses
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from app.config import settings
|
|||||||
from app.const import BANCHOBOT_ID
|
from app.const import BANCHOBOT_ID
|
||||||
from app.database import User, UserResp
|
from app.database import User, UserResp
|
||||||
from app.database.lazer_user import SEARCH_INCLUDED
|
from app.database.lazer_user import SEARCH_INCLUDED
|
||||||
from app.database.score import ScoreResp
|
from app.database.score import LegacyScoreResp, ScoreResp
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.models.score import GameMode
|
from app.models.score import GameMode
|
||||||
from app.service.asset_proxy_service import get_asset_proxy_service
|
from app.service.asset_proxy_service import get_asset_proxy_service
|
||||||
@@ -111,11 +111,13 @@ class UserCacheService:
|
|||||||
mode: GameMode | None = None,
|
mode: GameMode | None = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
|
is_legacy: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""生成用户成绩缓存键"""
|
"""生成用户成绩缓存键"""
|
||||||
mode_part = f":{mode}" if mode else ""
|
mode_part = f":{mode}" if mode else ""
|
||||||
return (
|
return (
|
||||||
f"user:{user_id}:scores:{score_type}{mode_part}:limit:{limit}:offset:{offset}:include_fail:{include_fail}"
|
f"user:{user_id}:scores:{score_type}{mode_part}:limit:{limit}:offset:"
|
||||||
|
f"{offset}:include_fail:{include_fail}:is_legacy:{is_legacy}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_user_beatmapsets_cache_key(
|
def _get_user_beatmapsets_cache_key(
|
||||||
@@ -166,10 +168,13 @@ class UserCacheService:
|
|||||||
mode: GameMode | None = None,
|
mode: GameMode | None = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> list[ScoreResp] | None:
|
is_legacy: bool = False,
|
||||||
|
) -> list[ScoreResp] | list[LegacyScoreResp] | None:
|
||||||
"""从缓存获取用户成绩"""
|
"""从缓存获取用户成绩"""
|
||||||
try:
|
try:
|
||||||
cache_key = self._get_user_scores_cache_key(user_id, score_type, include_fail, mode, limit, offset)
|
cache_key = self._get_user_scores_cache_key(
|
||||||
|
user_id, score_type, include_fail, mode, limit, offset, is_legacy
|
||||||
|
)
|
||||||
cached_data = await self.redis.get(cache_key)
|
cached_data = await self.redis.get(cache_key)
|
||||||
if cached_data:
|
if cached_data:
|
||||||
logger.debug(f"User scores cache hit for user {user_id}, type {score_type}")
|
logger.debug(f"User scores cache hit for user {user_id}, type {score_type}")
|
||||||
@@ -184,18 +189,21 @@ class UserCacheService:
|
|||||||
self,
|
self,
|
||||||
user_id: int,
|
user_id: int,
|
||||||
score_type: str,
|
score_type: str,
|
||||||
scores: list[ScoreResp],
|
scores: list[ScoreResp] | list[LegacyScoreResp],
|
||||||
include_fail: bool,
|
include_fail: bool,
|
||||||
mode: GameMode | None = None,
|
mode: GameMode | None = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
expire_seconds: int | None = None,
|
expire_seconds: int | None = None,
|
||||||
|
is_legacy: bool = False,
|
||||||
):
|
):
|
||||||
"""缓存用户成绩"""
|
"""缓存用户成绩"""
|
||||||
try:
|
try:
|
||||||
if expire_seconds is None:
|
if expire_seconds is None:
|
||||||
expire_seconds = settings.user_scores_cache_expire_seconds
|
expire_seconds = settings.user_scores_cache_expire_seconds
|
||||||
cache_key = self._get_user_scores_cache_key(user_id, score_type, include_fail, mode, limit, offset)
|
cache_key = self._get_user_scores_cache_key(
|
||||||
|
user_id, score_type, include_fail, mode, limit, offset, is_legacy
|
||||||
|
)
|
||||||
# 使用 model_dump_json() 而不是 model_dump() + json.dumps()
|
# 使用 model_dump_json() 而不是 model_dump() + json.dumps()
|
||||||
scores_json_list = [score.model_dump_json() for score in scores]
|
scores_json_list = [score.model_dump_json() for score in scores]
|
||||||
cached_data = f"[{','.join(scores_json_list)}]"
|
cached_data = f"[{','.join(scores_json_list)}]"
|
||||||
|
|||||||
Reference in New Issue
Block a user