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:
2026-09-09 18:45:50 +03:00
co-authored by Claude Opus 5
parent 1831a3e033
commit 8ac2757cd6
6 changed files with 129 additions and 132 deletions
+24 -33
View File
@@ -5,6 +5,7 @@ from sqlmodel import Session, select
from app.core.errors import ConflictError, ForbiddenError, NotFoundError, ValidationError
from app.models import Group, GroupMember, User
from app.services import group_service
MAX_GROUP_SIZE = 10
@@ -33,11 +34,7 @@ def add_member_by_nickname(session: Session, group: Group, nickname: str) -> tup
if member_count >= MAX_GROUP_SIZE:
raise ConflictError(f"В группе уже максимум участников ({MAX_GROUP_SIZE}).")
existing = session.exec(
select(GroupMember).where(
GroupMember.group_id == group.id, GroupMember.user_id == user.id
)
).first()
existing = group_service.get_membership(session, group.id, user.id)
if existing is not None:
raise ConflictError("Игрок уже в группе.")
@@ -48,22 +45,26 @@ def add_member_by_nickname(session: Session, group: Group, nickname: str) -> tup
return member, user
def remove_member(session: Session, group: Group, user_id: int) -> None:
member = session.exec(
select(GroupMember).where(
GroupMember.group_id == group.id, GroupMember.user_id == user_id
def _assert_not_last_owner(session: Session, group: Group, member: GroupMember, message: str) -> None:
"""Группа без владельца неисправима: назначить нового становится некому."""
if member.role != "owner":
return
owners = session.exec(
select(GroupMember.id).where(
GroupMember.group_id == group.id, GroupMember.role == "owner"
)
).first()
).all()
if len(owners) <= 1:
raise ForbiddenError(message)
def remove_member(session: Session, group: Group, user_id: int) -> None:
member = group_service.get_membership(session, group.id, user_id)
if member is None:
raise NotFoundError("Игрок не состоит в группе.")
if member.role == "owner":
owners = session.exec(
select(GroupMember).where(
GroupMember.group_id == group.id, GroupMember.role == "owner"
)
).all()
if len(owners) <= 1:
raise ForbiddenError("Нельзя удалить последнего владельца группы.")
_assert_not_last_owner(
session, group, member, "Нельзя удалить последнего владельца группы."
)
# Сбросить активную группу у тех, для кого она была активной.
user = session.get(User, user_id)
@@ -78,23 +79,13 @@ def remove_member(session: Session, group: Group, user_id: int) -> None:
def change_role(session: Session, group: Group, user_id: int, role: str) -> GroupMember:
if role not in ("owner", "member"):
raise ValidationError("Недопустимая роль.")
member = session.exec(
select(GroupMember).where(
GroupMember.group_id == group.id, GroupMember.user_id == user_id
)
).first()
member = group_service.get_membership(session, group.id, user_id)
if member is None:
raise NotFoundError("Игрок не состоит в группе.")
if member.role == "owner" and role != "owner":
# Без этого единственный владелец мог разжаловать сам себя, и группа
# оставалась без владельца навсегда: назначить нового уже некому.
owners = session.exec(
select(GroupMember).where(
GroupMember.group_id == group.id, GroupMember.role == "owner"
)
).all()
if len(owners) <= 1:
raise ForbiddenError("Нельзя снять роль с последнего владельца группы.")
if role != "owner":
_assert_not_last_owner(
session, group, member, "Нельзя снять роль с последнего владельца группы."
)
member.role = role
session.add(member)
session.commit()