fix(user): rank lost event uses the owner of the displaced score's username
This commit is contained in:
@@ -12,8 +12,6 @@ from app.calculator import (
|
|||||||
calculate_weighted_pp,
|
calculate_weighted_pp,
|
||||||
clamp,
|
clamp,
|
||||||
)
|
)
|
||||||
from app.config import settings
|
|
||||||
from app.database.events import Event, EventType
|
|
||||||
from app.database.team import TeamMember
|
from app.database.team import TeamMember
|
||||||
from app.dependencies.database import get_redis
|
from app.dependencies.database import get_redis
|
||||||
from app.models.model import (
|
from app.models.model import (
|
||||||
@@ -484,7 +482,10 @@ async def get_score_position_by_user(
|
|||||||
rownum = (
|
rownum = (
|
||||||
func.row_number()
|
func.row_number()
|
||||||
.over(
|
.over(
|
||||||
partition_by=col(BestScore.beatmap_id),
|
partition_by=(
|
||||||
|
col(BestScore.beatmap_id),
|
||||||
|
col(BestScore.gamemode),
|
||||||
|
),
|
||||||
order_by=col(BestScore.total_score).desc(),
|
order_by=col(BestScore.total_score).desc(),
|
||||||
)
|
)
|
||||||
.label("row_number")
|
.label("row_number")
|
||||||
@@ -511,7 +512,10 @@ async def get_score_position_by_id(
|
|||||||
rownum = (
|
rownum = (
|
||||||
func.row_number()
|
func.row_number()
|
||||||
.over(
|
.over(
|
||||||
partition_by=col(BestScore.beatmap_id),
|
partition_by=(
|
||||||
|
col(BestScore.beatmap_id),
|
||||||
|
col(BestScore.gamemode),
|
||||||
|
),
|
||||||
order_by=col(BestScore.total_score).desc(),
|
order_by=col(BestScore.total_score).desc(),
|
||||||
)
|
)
|
||||||
.label("row_number")
|
.label("row_number")
|
||||||
@@ -710,50 +714,6 @@ async def process_user(
|
|||||||
statistics.ranked_score += difference
|
statistics.ranked_score += difference
|
||||||
statistics.level_current = calculate_score_to_level(statistics.total_score)
|
statistics.level_current = calculate_score_to_level(statistics.total_score)
|
||||||
statistics.maximum_combo = max(statistics.maximum_combo, score.max_combo)
|
statistics.maximum_combo = max(statistics.maximum_combo, score.max_combo)
|
||||||
new_score_position = await get_score_position_by_user(session, score.beatmap_id, user, score.gamemode)
|
|
||||||
total_users = await session.exec(select(func.count()).select_from(User))
|
|
||||||
score_range = min(50, math.ceil(float(total_users.one()) * 0.01))
|
|
||||||
if new_score_position <= score_range and new_score_position > 0:
|
|
||||||
# Get the scores that might be displaced
|
|
||||||
displaced_scores_query = (
|
|
||||||
select(BestScore)
|
|
||||||
.where(
|
|
||||||
BestScore.beatmap_id == score.beatmap_id,
|
|
||||||
BestScore.gamemode == score.gamemode,
|
|
||||||
BestScore.user_id != user.id, # Not the current user
|
|
||||||
)
|
|
||||||
.order_by(col(BestScore.total_score).desc())
|
|
||||||
.limit(score_range)
|
|
||||||
)
|
|
||||||
displaced_scores = (await session.exec(displaced_scores_query)).all()
|
|
||||||
|
|
||||||
# Check if any scores were pushed out of the top positions
|
|
||||||
for i, displaced_score in enumerate(displaced_scores):
|
|
||||||
# Get the position of this displaced score
|
|
||||||
displaced_position = await get_score_position_by_id(
|
|
||||||
session, score.beatmap_id, displaced_score.score_id, score.gamemode
|
|
||||||
)
|
|
||||||
|
|
||||||
# If this score was previously in top positions but now pushed out
|
|
||||||
if i < score_range and displaced_position > score_range and displaced_position is not None:
|
|
||||||
# Create rank lost event for the displaced user
|
|
||||||
rank_lost_event = Event(
|
|
||||||
created_at=utcnow(),
|
|
||||||
type=EventType.RANK_LOST,
|
|
||||||
user_id=displaced_score.user_id,
|
|
||||||
)
|
|
||||||
rank_lost_event.event_payload = {
|
|
||||||
"mode": str(score.gamemode),
|
|
||||||
"beatmap": {
|
|
||||||
"title": score.beatmap.version,
|
|
||||||
"url": score.beatmap.url,
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"username": user.username,
|
|
||||||
"url": settings.web_url + "users/" + str(user.id),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
session.add(rank_lost_event)
|
|
||||||
if score.passed and has_leaderboard:
|
if score.passed and has_leaderboard:
|
||||||
# 情况1: 没有最佳分数记录,直接添加
|
# 情况1: 没有最佳分数记录,直接添加
|
||||||
# 情况2: 有最佳分数记录但没有该mod组合的记录,添加新记录
|
# 情况2: 有最佳分数记录但没有该mod组合的记录,添加新记录
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from app.database import (
|
|||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from app.database.achievement import process_achievements
|
from app.database.achievement import process_achievements
|
||||||
|
from app.database.best_score import BestScore
|
||||||
from app.database.counts import ReplayWatchedCount
|
from app.database.counts import ReplayWatchedCount
|
||||||
from app.database.daily_challenge import process_daily_challenge_score
|
from app.database.daily_challenge import process_daily_challenge_score
|
||||||
from app.database.events import Event, EventType
|
from app.database.events import Event, EventType
|
||||||
@@ -34,7 +35,7 @@ from app.database.score import (
|
|||||||
process_score,
|
process_score,
|
||||||
process_user,
|
process_user,
|
||||||
)
|
)
|
||||||
from app.dependencies.database import Database, get_redis
|
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
|
||||||
from app.dependencies.user import get_client_user, get_current_user
|
from app.dependencies.user import get_client_user, get_current_user
|
||||||
@@ -163,7 +164,8 @@ async def submit_score(
|
|||||||
)
|
)
|
||||||
score = (await db.exec(select(Score).options(joinedload(Score.user)).where(Score.id == score_id))).one()
|
score = (await db.exec(select(Score).options(joinedload(Score.user)).where(Score.id == score_id))).one()
|
||||||
|
|
||||||
resp = await ScoreResp.from_db(db, score)
|
resp: ScoreResp = await ScoreResp.from_db(db, score)
|
||||||
|
score_gamemode = score.gamemode
|
||||||
total_users = (await db.exec(select(func.count()).select_from(User))).one()
|
total_users = (await db.exec(select(func.count()).select_from(User))).one()
|
||||||
if resp.rank_global is not None and resp.rank_global <= min(math.ceil(float(total_users) * 0.01), 50):
|
if resp.rank_global is not None and resp.rank_global <= min(math.ceil(float(total_users) * 0.01), 50):
|
||||||
rank_event = Event(
|
rank_event = Event(
|
||||||
@@ -175,7 +177,7 @@ async def submit_score(
|
|||||||
rank_event.event_payload = {
|
rank_event.event_payload = {
|
||||||
"scorerank": score.rank.value,
|
"scorerank": score.rank.value,
|
||||||
"rank": resp.rank_global,
|
"rank": resp.rank_global,
|
||||||
"mode": resp.beatmap.mode.readable(), # pyright: ignore[reportOptionalMemberAccess]
|
"mode": score.gamemode.readable(),
|
||||||
"beatmap": {
|
"beatmap": {
|
||||||
"title": f"{resp.beatmap.beatmapset.artist} - {resp.beatmap.beatmapset.title} [{resp.beatmap.version}]", # pyright: ignore[reportOptionalMemberAccess]
|
"title": f"{resp.beatmap.beatmapset.artist} - {resp.beatmap.beatmapset.title} [{resp.beatmap.version}]", # pyright: ignore[reportOptionalMemberAccess]
|
||||||
"url": resp.beatmap.url, # pyright: ignore[reportOptionalMemberAccess]
|
"url": resp.beatmap.url, # pyright: ignore[reportOptionalMemberAccess]
|
||||||
@@ -186,13 +188,41 @@ async def submit_score(
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
db.add(rank_event)
|
db.add(rank_event)
|
||||||
await db.commit()
|
if resp.rank_global is not None and resp.rank_global == 1:
|
||||||
|
displaced_score = (
|
||||||
|
await db.exec(
|
||||||
|
select(BestScore)
|
||||||
|
.where(
|
||||||
|
BestScore.beatmap_id == score.beatmap_id,
|
||||||
|
BestScore.gamemode == score.gamemode,
|
||||||
|
)
|
||||||
|
.order_by(col(BestScore.total_score).desc())
|
||||||
|
.limit(1)
|
||||||
|
.offset(1)
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
if displaced_score and displaced_score.user_id != resp.user_id:
|
||||||
|
username = (await db.exec(select(User.username).where(User.id == displaced_score.user_id))).one()
|
||||||
|
|
||||||
# 成绩提交后刷新用户缓存 - 移至后台任务避免阻塞主流程
|
rank_lost_event = Event(
|
||||||
# 确保score对象已刷新,避免在后台任务中触发延迟加载
|
created_at=utcnow(),
|
||||||
await db.refresh(score)
|
type=EventType.RANK_LOST,
|
||||||
score_gamemode = score.gamemode
|
user_id=displaced_score.user_id,
|
||||||
|
)
|
||||||
|
rank_lost_event.event_payload = {
|
||||||
|
"mode": score.gamemode.readable(),
|
||||||
|
"beatmap": {
|
||||||
|
"title": score.beatmap.version,
|
||||||
|
"url": score.beatmap.url,
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"username": username,
|
||||||
|
"url": settings.web_url + "users/" + str(displaced_score.user.id),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
db.add(rank_lost_event)
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
if user_id is not None:
|
if user_id is not None:
|
||||||
background_task.add_task(_refresh_user_cache_background, redis, user_id, score_gamemode)
|
background_task.add_task(_refresh_user_cache_background, redis, user_id, score_gamemode)
|
||||||
background_task.add_task(process_user_achievement, resp.id)
|
background_task.add_task(process_user_achievement, resp.id)
|
||||||
@@ -202,17 +232,10 @@ async def submit_score(
|
|||||||
async def _refresh_user_cache_background(redis: Redis, user_id: int, mode: GameMode):
|
async def _refresh_user_cache_background(redis: Redis, user_id: int, mode: GameMode):
|
||||||
"""后台任务:刷新用户缓存"""
|
"""后台任务:刷新用户缓存"""
|
||||||
try:
|
try:
|
||||||
from app.dependencies.database import engine
|
|
||||||
|
|
||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
||||||
|
|
||||||
user_cache_service = get_user_cache_service(redis)
|
user_cache_service = get_user_cache_service(redis)
|
||||||
# 创建独立的数据库会话
|
# 创建独立的数据库会话
|
||||||
session = AsyncSession(engine)
|
async with with_db() as session:
|
||||||
try:
|
|
||||||
await user_cache_service.refresh_user_cache_on_score_submit(session, user_id, mode)
|
await user_cache_service.refresh_user_cache_on_score_submit(session, user_id, mode)
|
||||||
finally:
|
|
||||||
await session.close()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to refresh user cache after score submit: {e}")
|
logger.error(f"Failed to refresh user cache after score submit: {e}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user