Initial import

This commit is contained in:
2026-03-05 14:27:30 +00:00
commit bcbf9155d7
26 changed files with 841 additions and 0 deletions

29
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,29 @@
# Архитектура и бизнес-правила (Ticket System)
## 1. Базовые принципы
- **Стек:** FastAPI, SQLAlchemy 2.0 (asyncpg), PostgreSQL, Redis, RabbitMQ.
- **Модели данных:** Описаны в `backend/database/models.py`.
- **Ключевые сущности:** User, Tournament, Seat, Ticket.
## 2. Жизненный цикл билета (State Machine)
Статус билета (`TicketStatus` в таблице `tickets`) строго ограничен:
1. `AVAILABLE` — место свободно для бронирования.
2. `LOCKED` — место временно захвачено пользователем (15 минут на оплату).
3. `PAID` — оплата прошла успешно.
4. `SCANNED` — билет погашен на входе.
5. `REFUNDED` — возврат.
## 3. Сценарий А: Конкурентное бронирование (Критический путь)
Захват места должен исключать "состояние гонки" (race condition).
1. При POST-запросе на захват места (`/api/seats/{seat_id}/lock`), FastAPI обращается к Redis.
2. Пытается установить ключ `lock:seat:{seat_id}` с помощью `SETNX` и TTL 15 минут.
3. **Успех:** Если Redis вернул 1, обновляем статус билета в БД на `LOCKED` и привязываем `user_id`. Возвращаем HTTP 200.
4. **Отказ:** Если ключ уже существует, немедленно возвращаем HTTP 409 Conflict. БД не трогаем.
## 4. Сценарий Б: Асинхронная выдача билета (Idempotency)
Защита от двойных списаний и зависаний интерфейса при долгой генерации PDF.
1. Платежный шлюз присылает Webhook об успешной оплате.
2. FastAPI проверяет поле `idempotency_key` в таблице `tickets`. Если ключ уже обработан — игнорируем запрос.
3. Обновляем статус билета на `PAID`.
4. Отправляем событие `ticket_paid` (содержит `ticket_id` и данные пользователя) в очередь RabbitMQ. Отвечаем шлюзу HTTP 200.
5. Фоновый воркер забирает задачу, генерирует PDF (reportlab), грузит в MinIO (boto3) и сохраняет ссылку в БД.