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
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from datetime import date, datetime, timezone
|
||||
from datetime import date
|
||||
|
||||
from sqlmodel import Session, select
|
||||
|
||||
@@ -16,7 +16,7 @@ from app.core.errors import (
|
||||
NotFoundError,
|
||||
ValidationError,
|
||||
)
|
||||
from app.core.timeutil import app_today, iso_utc
|
||||
from app.core.timeutil import app_today, iso_utc, utcnow
|
||||
from app.models import Faction, GroupMember, Match, MatchParticipant, User
|
||||
from app.services import group_service
|
||||
|
||||
@@ -56,10 +56,6 @@ class ParticipantInput:
|
||||
comment: str | None = None
|
||||
|
||||
|
||||
def _utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def round_to_30(minutes: float) -> int:
|
||||
"""Округление длительности до получаса, минимум 30 минут."""
|
||||
return max(30, int(round(minutes / 30.0)) * 30)
|
||||
@@ -77,6 +73,17 @@ def match_version(match: Match) -> str:
|
||||
return iso_utc(match.updated_at)
|
||||
|
||||
|
||||
def touch(session: Session, match: Match) -> None:
|
||||
"""Пометить партию изменённой (без commit).
|
||||
|
||||
Версия для оптимистичной блокировки — это updated_at, а onupdate у SQLAlchemy
|
||||
срабатывает только при реальном UPDATE строки matches. Правки, меняющие лишь
|
||||
связанные сущности (участники, вложения), строку не трогают, поэтому каждая
|
||||
такая точка обязана позвать touch — иначе конкурентная запись пройдёт молча."""
|
||||
match.updated_at = utcnow()
|
||||
session.add(match)
|
||||
|
||||
|
||||
def assert_version(match: Match, expected: str | None) -> None:
|
||||
"""Если клиент прислал версию и она устарела — отказываем (кто-то изменил партию)."""
|
||||
if expected is not None and expected != match_version(match):
|
||||
@@ -188,7 +195,7 @@ def create_match(
|
||||
session, group_id, [r.user_id for r in roster], [r.faction_id for r in roster]
|
||||
)
|
||||
|
||||
now = _utcnow()
|
||||
now = utcnow()
|
||||
match = Match(
|
||||
group_id=group_id,
|
||||
status="in_progress",
|
||||
@@ -264,7 +271,7 @@ def finish_match(
|
||||
p.faction_id = f.faction_id
|
||||
session.add(p)
|
||||
|
||||
now = _utcnow()
|
||||
now = utcnow()
|
||||
match.finished_at = now
|
||||
started = match.started_at
|
||||
if started is not None:
|
||||
@@ -351,11 +358,7 @@ def update_match(
|
||||
)
|
||||
match.player_count = len(participants)
|
||||
|
||||
# Версия партии = updated_at, а onupdate срабатывает только при реальном UPDATE
|
||||
# строки matches. Правка одних участников его не вызывает, и тогда оптимистичная
|
||||
# блокировка молча пропускала бы конкурентную запись — поэтому бампаем явно.
|
||||
match.updated_at = _utcnow()
|
||||
session.add(match)
|
||||
touch(session, match)
|
||||
session.commit()
|
||||
session.refresh(match)
|
||||
return match
|
||||
|
||||
Reference in New Issue
Block a user