Модель данных
Все сервисы делят одну базу PostgreSQL, а границы доменов проведены схемами Postgres: схема принадлежит одному сервису, и он один в неё пишет. Эта статья объясняет карту схем, четыре слоя идентичности, привязку строк к воркспейсу и две инфраструктурные таблицы, поведение которых видно снаружи: аутбокс событий и журнал realtime.
Схемы как границы доменов
| Схема | Домен | Владелец |
|---|---|---|
auth | Аккаунты входа, сессии, OAuth, ключи API, RBAC | identity-service |
players | Идентичность игрока и привязанные социальные аккаунты | app-service |
public | Воркспейсы, участники, сетки дивизионов, настройки, аутбокс | app-service |
tournament | Структура турнира, сетка, встречи, pick/ban, отчёты | tournament-service |
casual | Казуальные матчи и скримы вне турнирного дерева | tournament-service |
overwatch | Каталог игры — герои, карты, режимы | app-service / parser-service |
overwatch_rank | Телеметрия рангов Overwatch из OverFast | parser-service |
matches | Разобранные логи матчей и производная статистика | parser-service |
balancer | Регистрация, балансировка, живой драфт, ранги участников | balancer-service |
achievements | Правила достижений, вычисления и ручные правки | parser-service / app-service |
analytics | Аналитические сигналы и реестр ML-моделей | analytics-service |
log_processing | Записи о загрузке и разборе логов | parser-service / discord-service |
realtime | Журнал событий для доигрывания по WebSocket | шлюз (Go) |
subscriptions | Провайдеры подписок, требования и вердикты | tournament-service / parser-service |
quota | Планы квот, стоимость операций и переопределения | все сервисы, через shared.quota |
Схема balancer шире своего названия: в ней живут и заявки на турнир, и результаты балансировки, и ранги участников. Имя пакета моделей тоже не всегда совпадает со схемой — например, модели рангов пишут в overwatch_rank, а модели загрузки логов — в log_processing.
Узлы, в которые сходится почти всё: public.workspace — арендатор; public.workspace_member — участие игрока в одном воркспейсе; players.user — игрок; auth.user — аккаунт входа; tournament.tournament — корень стадий, команд, встреч и заявок; overwatch.hero — на него ссылаются статистика, предпочтения в заявке и достижения.
Четыре слоя идентичности
Единой сущности «пользователь» в системе нет. Есть четыре слоя, которые иногда сходятся на одном человеке, а иногда годами живут порознь.
| Слой | Таблица | На какой вопрос отвечает |
|---|---|---|
| Аккаунт входа | auth.user | Кто может войти и что эта сессия вправе делать |
| Игрок | players.user | Кто этот человек в турнирах, логах и статистике |
| Участие в воркспейсе | public.workspace_member | Существует ли этот игрок в этом сообществе |
| Ранги | balancer.member_rank и registration_role.rank_value | Какой у участника SR на роли |
Связь аккаунта и игрока — 1:0..1 через уникальную nullable-колонку players.user.auth_user_id. Пустое значение означает виртуального игрока: он пришёл из лога матча, импорта CSV или был добавлен организатором в ростер, и войти не может. Разделять эти слои приходится потому, что игрок без аккаунта — нормальное состояние, а аккаунт без игрока — дыра: на него нельзя повесить участие в воркспейсе, поэтому регистрация аккаунта сразу создаёт пустую строку игрока.
public.workspace_member уникален по паре workspace_id + player_id и не хранит ни auth_user_id, ни колонку роли. Это и есть якорь арендатора: ростеры, заявки, драфт, ранги и достижения ссылаются на участника, а не на голого игрока, поэтому изоляция сообществ получается по построению, а виртуальный игрок спокойно живёт в составе и накапливает историю. Права при этом остаются на аккаунте: RBAC вычисляется по ролям auth.user, и до привязки аккаунта у участника прав нет.
Ранг — четвёртый слой, потому что «SR игрока» не одно число. В balancer.member_rank два уровня различаются одним полем author_user_id: NULL — канон воркспейса, который видят все, любое другое значение — личная книга конкретного автора. Остальные слои лежат вне этой таблицы: значение из заявки (registration_role.rank_value) и снимок ранга Overwatch. Побеждает первый слой, в котором есть число, а порядок задаёт контекст: микс читает автор → канон воркспейса → Overwatch, турнир — заявка → канон воркспейса → Overwatch. Поэтому «следовать канону» — это отсутствие строки, а не копия значения, и очистка ранга удаляет запись, а не пишет ноль.
Привязка к воркспейсу
Почти каждая бизнес-строка несёт workspace_id — напрямую либо транзитивно через tournament или workspace_member. Строки, которые намеренно глобальны (системные роли, точечные запреты прав, сетки дивизионов, статусы заявок), допускают workspace_id = NULL. Для API это значит ровно то, что описано в обзоре: доменное чтение без названного воркспейса отклоняется, а не возвращает чужие строки.
Аутбокс событий
public.event_outbox — транзакционный аутбокс: доменное событие пишется в той же транзакции, что и сама бизнес-правка, поэтому «поменяли, но не опубликовали» невозможно. Строку разгребает единственный подметальщик в tournament-сервисе и публикует её с повторами; event_id уникален, чтобы потребитель мог дедуплицировать, а status, attempts и next_attempt_at хранят состояние повторов. Доставка at-least-once: потребители обязаны быть идемпотентными, а пока подметальщик лежит, события всех сервисов копятся в очереди, но не теряются.
Журнал realtime
realtime.workspace_event — журнал, из которого шлюз доигрывает пропущенное. У строки есть topic, event_type, payload, время и schema_version, позволяющая менять форму полезной нагрузки, не обесценивая старые записи; при переподключении клиент называет последний виденный id, и шлюз отдаёт всё, что было после него по его топикам. Записан в журнал не каждый сигнал: часть топиков намеренно недолговечна — подписки воркспейса, статусы трансляций турнира и личные уведомления не оставляют строки и не доигрываются, потому что клиент всё равно перечитывает список, ради которого он их ждал. Подробности подписок — в статье Realtime (WebSocket).
Ключи воркспейса и турнира в журнальных таблицах (включая аудит и уведомления) хранятся как обычные числа без внешних ключей. Это не недосмотр: журнал должен пережить строки, которые описывает, и не должен утягиваться каскадным удалением.
Где смотреть точную схему
- Схема БД — интерактивные диаграммы таблиц прямо на сайте, сгенерированные из моделей.
- docs/database_erd.md — тот же источник в репозитории: каждая колонка, связь и уникальный индекс. Диаграммы генерируются из метаданных ORM, а CI падает, если они разошлись с кодом.
- docs/users-identity.md — полная семантика идентичности: виртуальные игроки, привязка аккаунта, слияние.
См. также
- Воркспейсы и права — арендаторы, участники и RBAC со стороны API.
- Аутентификация и ключи API — как аккаунт превращается в права запроса.
- Обзор для разработчиков — как компоненты связаны между собой.