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
+7 -6
View File
@@ -5,6 +5,7 @@ GroupMember). Лимит участников — общий с membership_servi
"""
from __future__ import annotations
from sqlalchemy.orm import aliased
from sqlmodel import Session, select
from app.core.errors import ConflictError, NotFoundError, ValidationError
@@ -70,17 +71,17 @@ def create_invitation(
def list_for_user(session: Session, user_id: int) -> list[dict]:
inviter = aliased(User)
rows = session.exec(
select(GroupInvitation, Group.name).join(Group, Group.id == GroupInvitation.group_id)
select(GroupInvitation, Group.name, inviter.nickname)
.join(Group, Group.id == GroupInvitation.group_id)
# LEFT JOIN: пригласивший мог быть удалён (invited_by_id → SET NULL).
.join(inviter, inviter.id == GroupInvitation.invited_by_id, isouter=True)
.where(GroupInvitation.user_id == user_id)
.order_by(GroupInvitation.created_at.desc())
).all()
out = []
for inv, gname in rows:
inviter_nick = None
if inv.invited_by_id is not None:
inviter = session.get(User, inv.invited_by_id)
inviter_nick = inviter.nickname if inviter else None
for inv, gname, inviter_nick in rows:
out.append(
{
"id": inv.id,