14 KiB
Архитектура
Общий подход
Backend строится на Go по принципам Clean Architecture. Зависимости направлены внутрь: инфраструктура и доставка зависят от сценариев приложения, сценарии — от доменной модели.
Backend
Предлагаемая структура:
backend/
cmd/api/ точка входа HTTP-сервера
internal/
domain/ сущности, value objects, правила, доменные ошибки
application/
usecase/ сценарии приложения
port/ интерфейсы репозиториев, часов, генераторов, событий
adapter/
in/http/ handlers, middleware, DTO
out/postgres/ реализации репозиториев
out/realtime/ доставка событий клиентам
platform/ конфигурация, логирование, подключение инфраструктуры
migrations/
Технологии первой версии:
- Go;
- PostgreSQL;
- REST API для команд и запросов;
- WebSocket или Server-Sent Events для синхронизации драфта и сетки;
- Discord OAuth 2.0 для входа;
- серверная cookie-сессия с HttpOnly, Secure и SameSite;
- структурированное логирование;
- OpenAPI как контракт frontend/backend.
Frontend
Предлагаемый стек:
- React + TypeScript;
- Vite;
- TanStack Router и TanStack Query;
- собственный типизированный слой локализации RU/EN с сохранением языка в localStorage;
- Tailwind CSS;
- shadcn/ui как основа компонентов;
- Framer Motion для умеренных анимаций;
- React Hook Form + Zod;
- dnd-kit для ручной корректировки составов;
- библиотека турнирной сетки только после проверки мобильного UX; при необходимости — собственный SVG-компонент.
Структура frontend группируется по продуктовым возможностям:
frontend/src/
app/
pages/
widgets/
features/
entities/
shared/
Границы модулей
- Players: профили, рейтинги, предпочтения ролей, желаемых союзников и avoid-списки;
- Auth: Discord OAuth, сессии и роли доступа;
- Staff (Admin/Moderator): управление игроками, регистрациями, событиями и капитанами;
- Events: календарь, регистрации участников и состояние игрового вечера;
- Balancing: генерация и оценка составов;
- Draft: жеребьёвка, карты и герои;
- Matches: карты, счёт и завершение серии;
- Tournaments: сетка и продвижение команд;
- Realtime: подписка клиентов на изменения.
Ключевые решения
Балансировщик
В MVP — детерминированный оптимизатор с весовой функцией качества и ограниченным поиском вариантов. Overwatch-ранги Bronze 5 → Champion 1 отображаются в ordinal 1–40 только для внутренних вычислений. Жёсткие ограничения обеспечивают состав 1/2/2 и уникальность игроков. Мягкие штрафы учитывают общий и ролевой разброс рейтинга, назначение вне предпочитаемой роли, разделение желаемых союзников и попадание avoid-пары в одну команду. Avoid имеет больший мягкий вес, чем пожелание играть вместе, но не нарушает требования баланса. После построения нескольких начальных вариантов выполняется локальный поиск обменами игроков одной роли.
Алгоритм располагается в доменном слое и не зависит от БД. При росте сообщества его можно заменить на CP-SAT/MILP solver без изменения API сценария.
Оркестрация события и серии
Application-слой управляет единым versioned workflow события. Состояние события, выбранные команды, резерв и активная серия сохраняются транзакционно. Команды close-registration, select-balance, edit-rosters, confirm-rosters и start-scrim проверяют текущее состояние и expectedVersion, поэтому повторные клики и конкурирующие вкладки не создают две серии.
Ручное редактирование работает с серверным roster draft. Backend разрешает обмен только между одинаковыми ролевыми слотами и замену слота игроком из резерва, после чего пересчитывает средние рейтинги и метрики. Подтверждение требует полного состава 1/2/2, уникальных игроков и капитана внутри каждой команды.
start-scrim атомарно блокирует составы, создаёт одну или несколько Bo3-серий и первый draft state. Для двух команд создаётся одиночная серия; для четырёх и более подтверждённых команд создаётся single-elimination турнир. Tournament read-model гидратируется свежими версиями всех сохранённых серий, поэтому параллельные матчи одного раунда независимо обновляют счёт и фазу в общей сетке. Умный event-level live-вход направляет участника в его матч, капитана/staff — к доступным командам, а остальных — в spectator mode; над серией остаются компактная сетка и навигация между активными матчами раунда.
Драфт как конечный автомат
Жеребьёвка и баны моделируются состояниями и допустимыми командами. После жеребьёвки победитель получает первый бан для первой карты, а проигравший — первый бан героев. Первая карта определяется map draft. После результата второй и последующих карт FSM переходит в MapPick, где проигравшая команда напрямую выбирает карту из следующего пула; затем запускается hero draft. При ничьей право выбора получает проигравший начальную жеребьёвку. Сервер является источником истины и отклоняет действие, если оно нарушает порядок, версию или регламент.
История действий
Все действия драфта сохраняются как неизменяемые записи аудита. Текущее состояние можно хранить в обычных таблицах; полноценный Event Sourcing для MVP не требуется.
Авторизация
Вход выполняется через Discord OAuth 2.0. После callback backend создаёт собственную серверную сессию и безопасную HttpOnly-cookie; OAuth-токен Discord не передаётся frontend. Discord ID является внешним идентификатором аккаунта.
Глобальные роли — Admin, Moderator и Player; Captain назначается staff-пользователем для конкретной команды, а read-only spectator-доступ доступен авторизованным игрокам. Игрок редактирует только собственный профиль, рейтинги и RSVP. Admin и Moderator управляют событиями, участниками, составами и сериями; только Admin может назначать или снимать Moderator. Начальные Admin задаются через ADMIN_DISCORD_IDS, чтобы роль нельзя было получить через публичный интерфейс.
Календарь и участие
События хранятся в UTC и отображаются в локальном часовом поясе пользователя. REST API отдаёт будущие и прошедшие события и статусы Going/Maybe/NotGoing. Изменения регистрации публикуются через realtime-канал, чтобы Admin сразу видел актуальный список. При изменении статуса за другого игрока API сохраняет actor ID и источник admin, а UI показывает, кем сделана отметка.
Realtime-синхронизация
Один глобальный authenticated SSE-канал подписывается на wildcard topic и адресно инвалидирует TanStack Query-кэш для событий, RSVP, составов, серий, турниров и игроков. Поэтому новые регистрации, участники, жеребьёвка, баны, результаты и сетка появляются без перезагрузки. WebSocket не требуется: команды остаются обычными HTTP mutations, а серверные изменения передаются клиентам однонаправленно через SSE. Nginx отключает buffering и держит соединение открытым.
Капитаны
Капитан — назначение внутри конкретной команды, а не глобальная роль аккаунта. Admin может назначить или заменить капитана только участником этой команды. Только текущий капитан выполняет командные действия драфта; Admin имеет аварийное право выполнить действие с обязательной записью в аудит.
Результаты матчей
Исход каждой карты фиксируется отдельной командой use case. Домен серии пересчитывает счёт и победителя, а турнир продвигает победившую команду только после завершения серии. Исправление результата не удаляет историю первоначального действия.
Развёртывание в Coolify
Production разворачивается из репозитория как один Docker Compose resource: frontend/reverse proxy, Go API, миграции и PostgreSQL с named volume. Coolify сам создаёт сеть стека и подключает свой proxy, поэтому в Compose не объявляются networks, container_name и вручную заданные Traefik labels.
Frontend-контейнер обслуживает SPA и проксирует /api и SSE к сервису api, поэтому приложению достаточно одного публичного домена. PostgreSQL и API не публикуют host-порты; сервисы обращаются друг к другу по Compose-именам. Секреты и production-переменные задаются в Coolify UI, а в репозитории хранится только .env.example.
Для всех долгоживущих сервисов задаются healthcheck и корректный graceful shutdown. Данные PostgreSQL сохраняются в named volume; для production обязательно настраивается регулярный backup тома/БД вне сервера.
Нефункциональные требования
- идемпотентность команд, которые могут повториться из-за сети;
- optimistic UI только там, где возможен безопасный откат;
- серверная валидация всех правил;
- миграции схемы БД;
- unit-тесты доменных правил и балансировки;
- интеграционные тесты репозиториев и API;
- production Docker Compose совместим с Coolify без пользовательских сетей;
- PostgreSQL имеет persistent volume, healthcheck и внешние резервные копии;
- адаптивность от мобильного экрана до большого дисплея.