Simplify: единый источник времени, версии партии и запросов
Проход /simplify по backend/app: - Три источника «текущего времени» (_utcnow в models.py и match_service.py при живом timeutil.utcnow) сведены к одному — это прямо инвариант из CLAUDE.md. - Бамп версии партии из двух независимых мест собран в match_service.touch: следующая точка мутации, не трогающая строку matches, теперь имеет очевидный способ сделать правильно. - membership_service переиспользует group_service.get_membership вместо трёх копий одного запроса; защита последнего владельца — один хелпер на удаление и смену роли вместо двух похожих блоков. - Убраны N+1: участники страницы партий и ники пригласивших берутся одним запросом вместо запроса на строку (20 партий = 20 лишних запросов с двумя join каждый). - Счётчики партий считает СУБД (COUNT/MAX) вместо выгрузки всех строк ради len() и max() в Python. #8 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BoiJK9ux8peeyjLb8TYjFf
This commit is contained in:
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy import func, text
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.core.timeutil import iso_utc
|
||||
@@ -235,12 +235,13 @@ def profile_stats(session: Session, user_id: int, group_id: int | None = None) -
|
||||
|
||||
def group_stats(session: Session, group_id: int) -> dict:
|
||||
board = leaderboard(session, group_id=group_id)
|
||||
total_matches = session.exec(
|
||||
select(Match).where(Match.group_id == group_id, Match.status == "finished")
|
||||
).all()
|
||||
last_at = None
|
||||
if total_matches:
|
||||
last_at = str(max(m.played_at for m in total_matches))
|
||||
# Нужны только счётчик и дата последней партии — тянуть строки целиком незачем.
|
||||
games_count, last_played = session.exec(
|
||||
select(func.count(), func.max(Match.played_at)).where(
|
||||
Match.group_id == group_id, Match.status == "finished"
|
||||
)
|
||||
).one()
|
||||
last_at = str(last_played) if last_played else None
|
||||
|
||||
available_ids = group_service.available_faction_ids(session, group_id)
|
||||
faction_meta = []
|
||||
@@ -290,7 +291,7 @@ def group_stats(session: Session, group_id: int) -> dict:
|
||||
|
||||
return {
|
||||
"group_id": group_id,
|
||||
"total_matches": len(total_matches),
|
||||
"total_matches": games_count,
|
||||
"last_match_at": last_at,
|
||||
"leaderboard": board["entries"],
|
||||
"provisional": board["provisional"],
|
||||
@@ -300,26 +301,44 @@ def group_stats(session: Session, group_id: int) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _participant_row(p: MatchParticipant, u: User, f: Faction) -> dict:
|
||||
return {
|
||||
"user_id": u.id,
|
||||
"nickname": u.nickname,
|
||||
"faction_id": f.id,
|
||||
"faction_name": f.name_ru,
|
||||
"place": p.place,
|
||||
"eliminated": p.eliminated,
|
||||
"was_random": p.was_random,
|
||||
"comment": p.comment,
|
||||
}
|
||||
|
||||
|
||||
def _participants_by_match(session: Session, match_ids: list[int]) -> dict[int, list[dict]]:
|
||||
"""Участники сразу всей страницы партий: иначе запрос на каждую партию (N+1)."""
|
||||
if not match_ids:
|
||||
return {}
|
||||
rows = session.exec(
|
||||
select(MatchParticipant, User, Faction)
|
||||
.join(User, User.id == MatchParticipant.user_id)
|
||||
.join(Faction, Faction.id == MatchParticipant.faction_id)
|
||||
.where(MatchParticipant.match_id.in_(match_ids))
|
||||
# place может быть NULL (партия идёт) — NULL уходит в конец сортировки.
|
||||
.order_by(MatchParticipant.place.is_(None), MatchParticipant.place, User.nickname)
|
||||
).all()
|
||||
out: dict[int, list[dict]] = {}
|
||||
for p, u, f in rows:
|
||||
out.setdefault(p.match_id, []).append(_participant_row(p, u, f))
|
||||
return out
|
||||
|
||||
|
||||
def _match_items(session: Session, matches) -> list[dict]:
|
||||
"""Элементы списка партий (общее для списка группы и истории игрока)."""
|
||||
from app.services.match_service import participants_detail # избегаем цикла импорта
|
||||
by_match = _participants_by_match(session, [m.id for m in matches])
|
||||
|
||||
items = []
|
||||
for m in matches:
|
||||
parts = []
|
||||
for p, u, f in participants_detail(session, m.id): # type: ignore[arg-type]
|
||||
parts.append(
|
||||
{
|
||||
"user_id": u.id,
|
||||
"nickname": u.nickname,
|
||||
"faction_id": f.id,
|
||||
"faction_name": f.name_ru,
|
||||
"place": p.place,
|
||||
"eliminated": p.eliminated,
|
||||
"was_random": p.was_random,
|
||||
"comment": p.comment,
|
||||
}
|
||||
)
|
||||
parts = by_match.get(m.id, [])
|
||||
items.append(
|
||||
{
|
||||
"id": m.id,
|
||||
@@ -339,7 +358,9 @@ def _match_items(session: Session, matches) -> list[dict]:
|
||||
|
||||
|
||||
def group_match_list(session: Session, group_id: int, limit: int = 20, offset: int = 0) -> dict:
|
||||
total = len(session.exec(select(Match.id).where(Match.group_id == group_id)).all())
|
||||
total = session.exec(
|
||||
select(func.count()).select_from(Match).where(Match.group_id == group_id)
|
||||
).one()
|
||||
matches = session.exec(
|
||||
select(Match)
|
||||
.where(Match.group_id == group_id)
|
||||
@@ -385,13 +406,12 @@ def user_match_list(
|
||||
}
|
||||
|
||||
where = (MatchParticipant.user_id == user_id, Match.status == "finished")
|
||||
total = len(
|
||||
session.exec(
|
||||
select(Match.id)
|
||||
.join(MatchParticipant, MatchParticipant.match_id == Match.id)
|
||||
.where(*where)
|
||||
).all()
|
||||
)
|
||||
total = session.exec(
|
||||
select(func.count())
|
||||
.select_from(Match)
|
||||
.join(MatchParticipant, MatchParticipant.match_id == Match.id)
|
||||
.where(*where)
|
||||
).one()
|
||||
matches = session.exec(
|
||||
select(Match)
|
||||
.join(MatchParticipant, MatchParticipant.match_id == Match.id)
|
||||
@@ -410,8 +430,6 @@ def user_match_list(
|
||||
|
||||
def user_in_progress_matches(session: Session, user_id: int) -> list[dict]:
|
||||
"""Незавершённые партии во всех группах, где состоит пользователь (новые сверху)."""
|
||||
from app.services.match_service import participants_detail # избегаем цикла импорта
|
||||
|
||||
group_ids = list(
|
||||
session.exec(select(GroupMember.group_id).where(GroupMember.user_id == user_id)).all()
|
||||
)
|
||||
@@ -423,21 +441,10 @@ def user_in_progress_matches(session: Session, user_id: int) -> list[dict]:
|
||||
.where(Match.status == "in_progress", Match.group_id.in_(group_ids))
|
||||
.order_by(Match.started_at.desc(), Match.id.desc())
|
||||
).all()
|
||||
by_match = _participants_by_match(session, [m.id for m, _ in rows])
|
||||
out = []
|
||||
for m, gname in rows:
|
||||
parts = [
|
||||
{
|
||||
"user_id": u.id,
|
||||
"nickname": u.nickname,
|
||||
"faction_id": f.id,
|
||||
"faction_name": f.name_ru,
|
||||
"place": p.place,
|
||||
"eliminated": p.eliminated,
|
||||
"was_random": p.was_random,
|
||||
"comment": p.comment,
|
||||
}
|
||||
for p, u, f in participants_detail(session, m.id) # type: ignore[arg-type]
|
||||
]
|
||||
parts = by_match.get(m.id, [])
|
||||
out.append(
|
||||
{
|
||||
"id": m.id,
|
||||
|
||||
Reference in New Issue
Block a user