Профиль: история партий игрока и настройки её витрины

Выборки партий по игроку в бэкенде не было — только по группе. Добавлен
stats_service.user_match_list: завершённые партии игрока, свежие сверху;
сборка элементов вынесена из group_match_list в общий _match_items, чтобы
не дублировать её в двух местах.

Витрина профиля задаётся двумя колонками в users (миграция 0011):
history_mode (all/best) и history_detail (compact/full). Режим применяется
на бэкенде, а не на клиенте: это витрина владельца, и в том же виде
профиль видят гости. В режиме best берётся партия с максимальными League
Points из SCORED_CTE (при равных очках — более свежая).

GET /api/users/{user_id}/matches отдаёт список вместе с mode и detail —
гостю хватает одного запроса, чтобы отрисовать историю как задумал владелец.

#1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 19:56:06 +03:00
co-authored by Claude Opus 5
parent bca2d7a058
commit be80891449
7 changed files with 372 additions and 15 deletions
+74 -12
View File
@@ -7,7 +7,7 @@ from sqlalchemy import text
from sqlmodel import Session, select
from app.core.timeutil import iso_utc
from app.models import Faction, Group, GroupMember, Match, User
from app.models import Faction, Group, GroupMember, Match, MatchParticipant, User
from app.services import faction_service, group_service, membership_service, user_service
from app.services.scoring import (
FACTION_MIN_GAMES,
@@ -300,18 +300,10 @@ def group_stats(session: Session, group_id: int) -> dict:
}
def group_match_list(session: Session, group_id: int, limit: int = 20, offset: int = 0) -> dict:
def _match_items(session: Session, matches) -> list[dict]:
"""Элементы списка партий (общее для списка группы и истории игрока)."""
from app.services.match_service import participants_detail # избегаем цикла импорта
total = len(session.exec(select(Match.id).where(Match.group_id == group_id)).all())
matches = session.exec(
select(Match)
.where(Match.group_id == group_id)
.order_by(Match.played_at.desc(), Match.id.desc())
.offset(offset)
.limit(limit)
).all()
items = []
for m in matches:
parts = []
@@ -343,7 +335,77 @@ def group_match_list(session: Session, group_id: int, limit: int = 20, offset: i
"participants": parts,
}
)
return {"items": items, "total": total, "limit": limit, "offset": offset}
return items
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())
matches = session.exec(
select(Match)
.where(Match.group_id == group_id)
.order_by(Match.played_at.desc(), Match.id.desc())
.offset(offset)
.limit(limit)
).all()
return {
"items": _match_items(session, matches),
"total": total,
"limit": limit,
"offset": offset,
}
def user_match_list(
session: Session,
user_id: int,
limit: int = 20,
offset: int = 0,
best_only: bool = False,
) -> dict:
"""История партий игрока: только ЗАВЕРШЁННЫЕ, свежие сверху.
best_only — одна лучшая партия по League Points (s.points из SCORED_CTE учитывает
место и размер стола); при равных очках берём более свежую."""
if best_only:
sql = f"""
{SCORED_CTE}
SELECT s.match_id AS match_id
FROM scored s
WHERE s.user_id = :uid
ORDER BY s.points DESC, s.played_at DESC, s.match_id DESC
LIMIT 1
"""
row = session.execute(text(sql), {"uid": user_id}).mappings().first()
matches = [session.get(Match, row["match_id"])] if row else []
return {
"items": _match_items(session, matches),
"total": len(matches),
"limit": 1,
"offset": 0,
}
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()
)
matches = session.exec(
select(Match)
.join(MatchParticipant, MatchParticipant.match_id == Match.id)
.where(*where)
.order_by(Match.played_at.desc(), Match.id.desc())
.offset(offset)
.limit(limit)
).all()
return {
"items": _match_items(session, matches),
"total": total,
"limit": limit,
"offset": offset,
}
def user_in_progress_matches(session: Session, user_id: int) -> list[dict]:
+22
View File
@@ -185,6 +185,28 @@ def update_bio(session: Session, user: User, bio: str | None) -> User:
return user
_HISTORY_MODES = {"all", "best"}
_HISTORY_DETAILS = {"compact", "full"}
def update_history_prefs(
session: Session, user: User, *, mode: str | None = None, detail: str | None = None
) -> User:
"""Витрина истории партий: что показывать и насколько подробно. None — не менять."""
if mode is not None:
if mode not in _HISTORY_MODES:
raise ValidationError("Неизвестный режим истории партий.")
user.history_mode = mode
if detail is not None:
if detail not in _HISTORY_DETAILS:
raise ValidationError("Неизвестная подробность истории партий.")
user.history_detail = detail
session.add(user)
session.commit()
session.refresh(user)
return user
def update_favorite_faction(session: Session, user: User, faction_id: int | None) -> User:
"""Любимая фракция — личный выбор игрока; None очищает выбор."""
if faction_id is not None and session.get(Faction, faction_id) is None: