fix(leaderboard): fix failed & duplicated scores in leaderboard
This commit is contained in:
@@ -14,10 +14,10 @@ from app.models.score import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .beatmap import Beatmap, BeatmapResp
|
from .beatmap import Beatmap, BeatmapResp
|
||||||
from .beatmapset import BeatmapsetResp
|
from .beatmapset import Beatmapset, BeatmapsetResp
|
||||||
|
|
||||||
from sqlalchemy import Column, DateTime
|
from sqlalchemy import Column, ColumnExpressionArgument, DateTime
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import aliased, joinedload
|
||||||
from sqlmodel import (
|
from sqlmodel import (
|
||||||
JSON,
|
JSON,
|
||||||
BigInteger,
|
BigInteger,
|
||||||
@@ -31,6 +31,7 @@ from sqlmodel import (
|
|||||||
select,
|
select,
|
||||||
)
|
)
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
from sqlmodel.sql._expression_select_cls import SelectOfScalar
|
||||||
|
|
||||||
|
|
||||||
class ScoreBase(SQLModel):
|
class ScoreBase(SQLModel):
|
||||||
@@ -97,6 +98,43 @@ 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
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def select_clause() -> SelectOfScalar["Score"]:
|
||||||
|
return select(Score).options(
|
||||||
|
joinedload(Score.beatmap) # pyright: ignore[reportArgumentType]
|
||||||
|
.joinedload(Beatmap.beatmapset) # pyright: ignore[reportArgumentType]
|
||||||
|
.selectinload(
|
||||||
|
Beatmapset.beatmaps # pyright: ignore[reportArgumentType]
|
||||||
|
),
|
||||||
|
joinedload(Score.user).joinedload(User.lazer_profile), # pyright: ignore[reportArgumentType]
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def select_clause_unique(
|
||||||
|
*where_clauses: ColumnExpressionArgument[bool] | bool,
|
||||||
|
) -> SelectOfScalar["Score"]:
|
||||||
|
rownum = (
|
||||||
|
func.row_number()
|
||||||
|
.over(
|
||||||
|
partition_by=col(Score.user_id), order_by=col(Score.total_score).desc()
|
||||||
|
)
|
||||||
|
.label("rn")
|
||||||
|
)
|
||||||
|
subq = select(Score, rownum).where(*where_clauses).subquery()
|
||||||
|
best = aliased(Score, subq, adapt_on_names=True)
|
||||||
|
return (
|
||||||
|
select(best)
|
||||||
|
.where(subq.c.rn == 1)
|
||||||
|
.options(
|
||||||
|
joinedload(best.beatmap) # pyright: ignore[reportArgumentType]
|
||||||
|
.joinedload(Beatmap.beatmapset) # pyright: ignore[reportArgumentType]
|
||||||
|
.selectinload(
|
||||||
|
Beatmapset.beatmaps # pyright: ignore[reportArgumentType]
|
||||||
|
),
|
||||||
|
joinedload(best.user).joinedload(User.lazer_profile), # pyright: ignore[reportArgumentType]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ScoreResp(ScoreBase):
|
class ScoreResp(ScoreBase):
|
||||||
id: int
|
id: int
|
||||||
@@ -300,6 +338,7 @@ async def get_score_position_by_id(
|
|||||||
Score.map_md5 == beatmap_md5,
|
Score.map_md5 == beatmap_md5,
|
||||||
Score.id == score_id,
|
Score.id == score_id,
|
||||||
Score.gamemode == mode,
|
Score.gamemode == mode,
|
||||||
|
col(Score.passed).is_(True),
|
||||||
col(Beatmap.beatmap_status).in_(
|
col(Beatmap.beatmap_status).in_(
|
||||||
[
|
[
|
||||||
BeatmapRankStatus.RANKED,
|
BeatmapRankStatus.RANKED,
|
||||||
@@ -316,13 +355,29 @@ async def get_score_position_by_id(
|
|||||||
rownum = (
|
rownum = (
|
||||||
func.row_number()
|
func.row_number()
|
||||||
.over(
|
.over(
|
||||||
partition_by=Score.map_md5,
|
partition_by=[col(Score.user_id), col(Score.map_md5)],
|
||||||
order_by=col(Score.total_score).desc(),
|
order_by=col(Score.total_score).desc(),
|
||||||
)
|
)
|
||||||
.label("row_number")
|
.label("rownum")
|
||||||
)
|
)
|
||||||
subq = select(Score, rownum).join(Beatmap).where(*where_clause).subquery()
|
subq = (
|
||||||
stmt = select(subq.c.row_number).where(subq.c.id == score_id)
|
select(Score.user_id, Score.id, Score.total_score, rownum)
|
||||||
|
.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
|
||||||
|
|||||||
@@ -3,23 +3,26 @@ from __future__ import annotations
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from app.database import (
|
from app.database import (
|
||||||
Beatmap,
|
|
||||||
User as DBUser,
|
User as DBUser,
|
||||||
)
|
)
|
||||||
from app.database.beatmapset import Beatmapset
|
|
||||||
from app.database.score import Score, ScoreResp
|
from app.database.score import Score, ScoreResp
|
||||||
from app.database.score_token import ScoreToken, ScoreTokenResp
|
from app.database.score_token import ScoreToken, ScoreTokenResp
|
||||||
from app.database.user import User
|
|
||||||
from app.dependencies.database import get_db
|
from app.dependencies.database import get_db
|
||||||
from app.dependencies.user import get_current_user
|
from app.dependencies.user import get_current_user
|
||||||
from app.models.score import INT_TO_MODE, HitResult, Rank, SoloScoreSubmissionInfo
|
from app.models.score import (
|
||||||
|
INT_TO_MODE,
|
||||||
|
GameMode,
|
||||||
|
HitResult,
|
||||||
|
Rank,
|
||||||
|
SoloScoreSubmissionInfo,
|
||||||
|
)
|
||||||
|
|
||||||
from .api_router import router
|
from .api_router import router
|
||||||
|
|
||||||
from fastapi import Depends, Form, HTTPException, Query
|
from fastapi import Depends, Form, HTTPException, Query
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from sqlmodel import col, select
|
from sqlmodel import col, select, true
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +37,7 @@ class BeatmapScores(BaseModel):
|
|||||||
async def get_beatmap_scores(
|
async def get_beatmap_scores(
|
||||||
beatmap: int,
|
beatmap: int,
|
||||||
legacy_only: bool = Query(None), # TODO:加入对这个参数的查询
|
legacy_only: bool = Query(None), # TODO:加入对这个参数的查询
|
||||||
mode: str = Query(None),
|
mode: GameMode | None = Query(None),
|
||||||
# mods: List[APIMod] = Query(None), # TODO:加入指定MOD的查询
|
# mods: List[APIMod] = Query(None), # TODO:加入指定MOD的查询
|
||||||
type: str = Query(None),
|
type: str = Query(None),
|
||||||
current_user: DBUser = Depends(get_current_user),
|
current_user: DBUser = Depends(get_current_user),
|
||||||
@@ -47,23 +50,22 @@ async def get_beatmap_scores(
|
|||||||
|
|
||||||
all_scores = (
|
all_scores = (
|
||||||
await db.exec(
|
await db.exec(
|
||||||
select(Score).where(Score.beatmap_id == beatmap)
|
Score.select_clause_unique(
|
||||||
# .where(Score.mods == mods if mods else True)
|
Score.beatmap_id == beatmap,
|
||||||
|
col(Score.passed).is_(True),
|
||||||
|
Score.gamemode == mode if mode is not None else true(),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
user_score = (
|
user_score = (
|
||||||
await db.exec(
|
await db.exec(
|
||||||
select(Score)
|
Score.select_clause_unique(
|
||||||
.options(
|
Score.beatmap_id == beatmap,
|
||||||
joinedload(Score.beatmap) # pyright: ignore[reportArgumentType]
|
Score.user_id == current_user.id,
|
||||||
.joinedload(Beatmap.beatmapset) # pyright: ignore[reportArgumentType]
|
col(Score.passed).is_(True),
|
||||||
.selectinload(
|
Score.gamemode == mode if mode is not None else true(),
|
||||||
Beatmapset.beatmaps # pyright: ignore[reportArgumentType]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.where(Score.beatmap_id == beatmap)
|
|
||||||
.where(Score.user_id == current_user.id)
|
|
||||||
)
|
)
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
@@ -98,18 +100,13 @@ async def get_user_beatmap_score(
|
|||||||
)
|
)
|
||||||
user_score = (
|
user_score = (
|
||||||
await db.exec(
|
await db.exec(
|
||||||
select(Score)
|
Score.select_clause()
|
||||||
.options(
|
.where(
|
||||||
joinedload(Score.beatmap) # pyright: ignore[reportArgumentType]
|
Score.gamemode == mode if mode is not None else True,
|
||||||
.joinedload(Beatmap.beatmapset) # pyright: ignore[reportArgumentType]
|
Score.beatmap_id == beatmap,
|
||||||
.selectinload(
|
Score.user_id == user,
|
||||||
Beatmapset.beatmaps # pyright: ignore[reportArgumentType]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.where(Score.gamemode == mode if mode is not None else True)
|
.order_by(col(Score.total_score).desc())
|
||||||
.where(Score.beatmap_id == beatmap)
|
|
||||||
.where(Score.user_id == user)
|
|
||||||
.order_by(col(Score.classic_total_score).desc())
|
|
||||||
)
|
)
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
@@ -143,17 +140,12 @@ async def get_user_all_beatmap_scores(
|
|||||||
)
|
)
|
||||||
all_user_scores = (
|
all_user_scores = (
|
||||||
await db.exec(
|
await db.exec(
|
||||||
select(Score)
|
Score.select_clause()
|
||||||
.options(
|
.where(
|
||||||
joinedload(Score.beatmap) # pyright: ignore[reportArgumentType]
|
Score.gamemode == ruleset if ruleset is not None else True,
|
||||||
.joinedload(Beatmap.beatmapset) # pyright: ignore[reportArgumentType]
|
Score.beatmap_id == beatmap,
|
||||||
.selectinload(
|
Score.user_id == user,
|
||||||
Beatmapset.beatmaps # pyright: ignore[reportArgumentType]
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.where(Score.gamemode == ruleset if ruleset is not None else True)
|
|
||||||
.where(Score.beatmap_id == beatmap)
|
|
||||||
.where(Score.user_id == user)
|
|
||||||
.order_by(col(Score.classic_total_score).desc())
|
.order_by(col(Score.classic_total_score).desc())
|
||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
@@ -255,16 +247,7 @@ async def submit_solo_score(
|
|||||||
score_token.score_id = score_id
|
score_token.score_id = score_id
|
||||||
await db.commit()
|
await db.commit()
|
||||||
score = (
|
score = (
|
||||||
await db.exec(
|
await db.exec(Score.select_clause().where(Score.id == score_id))
|
||||||
select(Score)
|
|
||||||
.options(
|
|
||||||
joinedload(Score.beatmap) # pyright: ignore[reportArgumentType]
|
|
||||||
.joinedload(Beatmap.beatmapset) # pyright: ignore[reportArgumentType]
|
|
||||||
.selectinload(Beatmapset.beatmaps), # pyright: ignore[reportArgumentType]
|
|
||||||
joinedload(Score.user).joinedload(User.lazer_profile), # pyright: ignore[reportArgumentType]
|
|
||||||
)
|
|
||||||
.where(Score.id == score_id)
|
|
||||||
)
|
|
||||||
).first()
|
).first()
|
||||||
assert score is not None
|
assert score is not None
|
||||||
return await ScoreResp.from_db(db, score)
|
return await ScoreResp.from_db(db, score)
|
||||||
|
|||||||
Reference in New Issue
Block a user