Партии: оптимистичная блокировка (version) — устаревшие правки/отмена отклоняются (фикс гонки завершить-vs-отменить)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ from app.core.errors import (
|
||||
NotFoundError,
|
||||
ValidationError,
|
||||
)
|
||||
from app.core.timeutil import app_today
|
||||
from app.core.timeutil import app_today, iso_utc
|
||||
from app.models import Faction, GroupMember, Match, MatchParticipant, User
|
||||
from app.services import group_service
|
||||
|
||||
@@ -70,6 +70,20 @@ def get_match(session: Session, match_id: int) -> Match:
|
||||
return match
|
||||
|
||||
|
||||
def match_version(match: Match) -> str:
|
||||
"""Версия партии для оптимистичной блокировки (меняется при любом изменении)."""
|
||||
return iso_utc(match.updated_at)
|
||||
|
||||
|
||||
def assert_version(match: Match, expected: str | None) -> None:
|
||||
"""Если клиент прислал версию и она устарела — отказываем (кто-то изменил партию)."""
|
||||
if expected is not None and expected != match_version(match):
|
||||
raise ConflictError(
|
||||
"Партия уже изменена на другом устройстве — обновите страницу.",
|
||||
code="STALE_WRITE",
|
||||
)
|
||||
|
||||
|
||||
def participants_detail(
|
||||
session: Session, match_id: int
|
||||
) -> list[tuple[MatchParticipant, User, Faction]]:
|
||||
@@ -193,7 +207,9 @@ def finish_match(
|
||||
win_reason: str,
|
||||
overall_comment: str | None = None,
|
||||
overall_comment_set: bool = False,
|
||||
expected_version: str | None = None,
|
||||
) -> Match:
|
||||
assert_version(match, expected_version)
|
||||
if match.status != "in_progress":
|
||||
raise ConflictError("Партия уже завершена.")
|
||||
if win_reason not in WIN_REASONS:
|
||||
@@ -274,8 +290,10 @@ def update_match(
|
||||
win_reason: str | None = None,
|
||||
win_reason_set: bool = False,
|
||||
participants: list[ParticipantInput] | None = None,
|
||||
expected_version: str | None = None,
|
||||
) -> Match:
|
||||
"""Правка завершённой партии (админ): полный список участников с местами."""
|
||||
assert_version(match, expected_version)
|
||||
if played_at is not None:
|
||||
match.played_at = played_at
|
||||
if overall_comment_set:
|
||||
@@ -317,9 +335,10 @@ def update_match(
|
||||
return match
|
||||
|
||||
|
||||
def delete_match(session: Session, match: Match) -> None:
|
||||
def delete_match(session: Session, match: Match, expected_version: str | None = None) -> None:
|
||||
from app.services import attachment_service # избегаем цикла импорта
|
||||
|
||||
assert_version(match, expected_version)
|
||||
match_id = match.id
|
||||
session.delete(match) # участники и вложения (БД) удалятся каскадом (FK ON DELETE CASCADE)
|
||||
session.commit()
|
||||
|
||||
Reference in New Issue
Block a user