Skip to main content

Seasons

1. Summary

Goal: Временные соревновательные периоды (3 месяца) с рейтингом по XP. Контролирует жизненный цикл игровой экономики: обнуление прогресса, награды для топ-10, обратный отсчёт до старта нового сезона. Периодический сброс предотвращает переизбыток накопленных предметов и контролирует объём выводов скинов.

User Value: Соревновательная мотивация. Попадание в топ-10 гарантирует реальный скин в Steam — уникальная награда за активность без денежных вложений. Игроки вне топ-10 не получают сезонных наград — это элитарная система.


2. Business Logic

Season Lifecycle

4-state machine: DRAFT → SCHEDULED → ACTIVE → COMPLETED

Период: Настройка контента через Setup Wizard (8 шагов)

Доступные действия:

  • Настройка названия, дат, наград
  • Привязка кейсов, рулеток
  • Генерация квизов, квестов, достижений
  • Клонирование контента из другого сезона

Переход в SCHEDULED: При утверждении через Approve (Step 8)

Reward Tiers

ПозицияTierТипичная награда
#1legendaryСамый ценный скин сезона
#2-3mythicalСкин высокой ценности
#4-10epicСкин средней ценности
#11+Без награды
Денормализация наград

imageUrl и itemTier сохраняются в JSON rewards при создании сезона для быстрого отображения без JOIN на таблицу Item.

XP Sources (детализация)

Все источники XP учитываются в UserSeasonStats:

ИсточникПолеОписание
QuestsxpFromTasksВыполнение квестов
AchievementsxpFromAchievementsРазблокировка достижений
CasesxpFromCasesОткрытие кейсов
SpinsxpFromSpinsDaily Spins
ReferralsxpFromReferralsПриглашение друзей
SalvagexpFromSalvageРазбор предметов
AdminxpFromAdminРучное начисление

Rank Update Mechanics

Обновление рангов: Каждые 10 минут cron-job RankUpdateJob

Алгоритм:

ROW_NUMBER() OVER (ORDER BY xp DESC) AS rank

Top-10 Threshold: Минимальный XP для попадания в топ-10, отображается пользователю как distanceToTop10.

Season Reset

При завершении сезона сбрасываются:

User поля:

  • scrap, level, xp, все streaks, counters активности

Прогресс:

  • UserInventory (кроме SEASON_REWARD и SKIN items)
  • QuizResult (история ответов)
  • UserQuest, UserAchievement (прогресс сбрасывается)

Экономика сезона:

  • LuckPoolEntry, BudgetPeriod (для старого сезона)

Технические данные:

  • RustPlayerSession, UserActiveBuff, BuffEvent

История операций (развлекательные данные):

  • CaseOpening — история открытий кейсов
  • SpinResult — история спинов рулетки
  • FeedEvent — публичная лента активности
Почему удаляем историю?

Развлекательные данные (~7M записей/сезон при 10k пользователей) не нужны для аудита. Очистка при смене сезона предотвращает неконтролируемый рост БД.

Сохраняются (аудит):

  • friendsInvited (lifetime stat)
  • SEASON_REWARD items (награды прошлых сезонов)
  • SKIN items (скрафченные скины для вывода)
  • CraftHistory — история крафта (потрачены ресурсы)
  • Withdrawal — история выводов (Steam трейды, юридически важно)
  • PromoCodeRedemption — история промокодов
  • RaffleTicket, Raffle — история розыгрышей (выигрыши, споры)
  • AdminGrant — история админских действий

Season Setup Wizard

Единый wizard для настройки и редактирования сезонов через админ-панель:

Setup Mode (DRAFT): 8 шагов с последовательным прохождением и валидацией.

Edit Mode (SCHEDULED/ACTIVE): 7 шагов со свободной навигацией, per-step save.

ШагНазваниеОписание
1Basic InfoНазвание, описание, даты, время старта (HH:MM UTC)
2RewardsНаграды для топ-1, топ-3, топ-10 (скины)
3CasesПривязка кейсов к сезону
4SpinsПривязка рулеток к сезону
5QuizzesМассовая генерация, точечная генерация или импорт квизов
6QuestsSmart Quest Generation из блюпринтов
7AchievementsSmart Achievement Generation из блюпринтов
8ApproveУтверждение и планирование старта (только Setup Mode)
Навигация в Edit Mode

В Edit Mode (SCHEDULED/ACTIVE) навигация между шагами свободная — можно переходить к любому шагу без валидации предыдущих. Шаг Approve отсутствует.

Clone Season (Quick Start)

При создании нового сезона админ может клонировать контент из существующего сезона:

Что клонируется:

  • Rewards (top-1, top-3, top-10)
  • Cases (привязки SeasonCase)
  • Spins (привязки SeasonSpin)
  • Quizzes (привязки Quiz → новый сезон)
  • Quests (привязки SeasonQuest)
  • Achievements (привязки SeasonAchievement)

Endpoint: POST /admin/season-setup/:seasonId/clone-from

UI

В шаге Basic Info (Step 1) секция "Быстрый старт" позволяет выбрать source-сезон из dropdown и клонировать одним нажатием.

Smart Quest Generation (Step 6)

Квесты генерируются автоматически из code-defined блюпринтов (29 шт.) с учётом контента сезона.

Алгоритм (5 фаз):

  1. Analyze Content — собрать cases, spins, items (с deduplication по dropChance), scrap analysis, quiz categories/subcategories/slugs/entityTypes
  2. Calculate Pool Sizes — DAILY/WEEKLY/PERMANENT целевые размеры (с 30% буфером)
  3. Instantiate Blueprints — создать кандидатов из блюпринтов × контент (content-aware: COLLECTION блюпринты проверяют наличие соответствующих предметов/скрапа)
  4. Balance Distribution — обрезать избыток, приоритизируя сложные квесты
  5. Save to Database — транзакция: удалить старые + создать новые

Генерация по режимам (7 режимов):

РежимВариантыПримеры
genericДо 3 (DAILY/WEEKLY), 1 (PERMANENT)"Охотник за удачей I/II/III"
per-case1 на каждый кейс сезона"Фанат «Discharge»"
per-item1 на high-tier предмет (max 3)"За «AK-47»!"
per-quiz-category1 на категорию квизов"Знаток «Оружие»"
per-quiz-subcategory1 на подкатегорию (min 3 квиза)"Учёный: Штурмовые"
per-quiz-slug1 на предмет/сущность (min 3 квиза)"Эксперт по AK-47"
per-quiz-entitytype1 на тип сущности (min 3 квиза)"Исследователь: Предметы"

Content-Aware COLLECTION: Генератор анализирует реальный контент сезона — предметы и скрап из кейсов и рулеток. COLLECTION блюпринты (12 шт.) генерируют квесты только если соответствующий контент существует (ресурсы, фрагменты, чертежи, скрап). Скрап-квесты динамически капируются на основе среднего дропа.

Quiz Quests = Generic Only

Quiz-квесты используют только generic режим с фиксированными таргетами (daily=1, weekly=5, permanent=15). Специфичные quiz-задания будут реализованы как Achievement templates. Подробнее: Quests ADR

Полная матрица блюпринтов: Quest Blueprints

Placeholder rewards

Генерируемые квесты получают placeholder SCRAP награды (Easy=50, Medium=100, Hard=150). Админ настраивает финальные награды после генерации.

Manual Quest Creation

Помимо автогенерации, админ может создать квест вручную прямо из визарда:

  • Кнопка «+» в заголовке каждой категории — создаёт квест с предзаполненной категорией
  • Кнопка «Создать квест» — создаёт без привязки к категории
  • Two-step flow: форма создания → автоматическая привязка к сезону
  • После создания запускается фоновая валидация сезона
  • Кнопки скрыты для завершённых сезонов (COMPLETED)

Achievement Generation (Step 7)

Достижения генерируются автоматически из шаблонов (blueprints).

Manual Achievement Creation

Аналогично квестам, админ может создать достижение вручную:

  • Кнопка «+» в заголовке каждой категории — создаёт с предзаполненной категорией
  • Кнопка «Создать достижение» — создаёт без привязки к категории
  • Two-step flow: форма создания → привязка к сезону через POST /:seasonId/achievements/add
  • Кнопки скрыты для завершённых сезонов (COMPLETED)

Quiz Generation (Step 5)

Квизы генерируются автоматически из шаблонов + SQLite базы данных Rust. Доступны два подхода: массовая генерация по категориям и точечная генерация по конкретным предметам.

Массовая генерация (Primary)

Входные данные:

  • categories — массив категорий: weapons, food, raid, attire
  • total — общее количество квизов (по умолчанию 270)
  • mode — режим генерации: regenerate (по умолчанию) или append

Режимы генерации:

РежимПоведениеБюджетUsed counters
regenerateУдаляет все существующие квизы, создаёт новыеSET (абсолютное значение)Сброс до 0
appendДобавляет новые к существующим без удаленияINCREMENT (прибавление)Без изменений
Догенерация (append)

Append-режим исключает дубли: перед генерацией собираются вопросы существующих квизов сезона, и новые генерируются только из оставшегося пула. Подтверждение не требуется — операция безопасна.

Алгоритм распределения:

  1. total / categories.length квизов на каждую категорию
  2. Внутри категории: 50% EASY, 30% MEDIUM, 20% HARD
  3. Для каждого слота: случайный предмет из SQLite × случайный шаблон

Точечная генерация (Targeted)

Позволяет генерировать квизы для конкретных предметов (shortnames) вместо целых категорий. Используется когда нужно добавить квизы для отдельных предметов — например, при создании slug-targeted достижений.

UI: Collapsible-секция «Точечная генерация» в Step 5 Wizard.

Flow:

  1. Админ выбирает категорию → загружаются предметы с количеством доступных шаблонов
  2. Опциональная фильтрация по подкатегории и поиску по имени
  3. Выбор конкретных предметов (чекбоксы, Select All / Deselect All)
  4. Опциональное ограничение количества (по умолчанию = максимум из шаблонов)
  5. Генерация → результат: +N квизов (Easy: X, Medium: Y, Hard: Z) + breakdown по предметам

Входные данные:

  • category — одна категория (например weapons)
  • shortnames — массив shortname-ов предметов (например ["rifle.ak", "smg.mp5"])
  • count — опциональный лимит (по умолчанию = все возможные шаблоны)

Особенности:

  • Всегда append-режим — добавляет квизы без удаления существующих
  • Исключает дубли по question текста (как и массовая генерация)
  • Бюджет SeasonContentBudget инкрементируется атомарно в транзакции

Общее

Источники данных:

  • backend/data/quiz-templates.json — шаблоны вопросов по категориям
  • backend/data/rust_data.db — SQLite база с данными Rust (предметы, рецепты, стоимости)

Архитектура "Fresh + Persist": Каждый сезон получает свежие квизы. После завершения сезона квизы остаются для аналитики (currentSeasonId = null, isActive = false).

Производительность

Все квизы записываются в БД одним createMany INSERT вместо индивидуальных создаёт.

Protection

ДействиеRate LimitAuthValidation
Получить текущий сезонgeneral (100/min)TelegramGetCurrentSeasonSchema
Получить статистикуgeneral (100/min)TelegramGetSeasonStatsSchema
Получить leaderboardgeneral (100/min)TelegramGetLeaderboardSchema
Завершить сезон (admin)mutations (5/min)Admin JWT + парольAdminForceEndSeasonSchema
Запустить сезон (admin)mutations (5/min)Admin JWT + парольAdminStartSeasonSchema
Детали реализации

См. Security Matrix для полного обзора защит.

Display Status (Frontend States)

Backend возвращает displayStatus — вычисляемое состояние, определяющее что показывать пользователю:

displayStatusКогдаUIОперации
activeACTIVE сезон, > INTER_SEASON_MINUTES до концаОбычный интерфейс✅ Все доступны
endingACTIVE сезон, ≤ INTER_SEASON_MINUTES до концаOverlay с обратным отсчётом MM:SS❌ Заблокированы
interSeasonНет ACTIVE, но есть SCHEDULEDOverlay: результаты прошлого сезона + countdown до следующего❌ Заблокированы
noSeasonНет ни ACTIVE, ни SCHEDULEDOverlay: результаты последнего сезона (если был)❌ Заблокированы
displayStatus — computed, not stored

displayStatus вычисляется в SeasonService.getSeasonInfo() на основе текущего состояния БД и времени. Не хранится в модели Season.

Season Timer Service

Точное планирование активации сезонов через setTimeout:

МетодОписание
scheduleActivation(seasonId, startDate)Установить таймер на конкретную дату
cancelActivation(seasonId)Отменить запланированную активацию
rescheduleAllOnBoot()При перезагрузке: восстановить таймеры для всех SCHEDULED сезонов
getActiveTimers()Мониторинг: список ID сезонов с активными таймерами

Обработка ограничения setTimeout:

  • JavaScript setTimeout ограничен ~24.8 днями (2^31 - 1 ms)
  • Для более далёких дат: рекурсивное перепланирование через MAX_TIMEOUT_MS
  • Если startDate уже в прошлом → немедленная активация

Интеграция: При Approve (Step 8) wizard устанавливает scheduledAt с датой и временем (HH:MM UTC). SeasonTimerService создаёт таймер. При ручном старте или удалении — таймер отменяется.

Season Status Middleware

Middleware requireActiveSeason блокирует операции при отсутствии или завершении сезона:

ОшибкаHTTPУсловиеPayload
NO_ACTIVE_SEASON503Нет сезона со status=ACTIVE{ error, message }
SEASON_ENDING503ACTIVE, но ≤ INTER_SEASON_MINUTES до endDate{ error, message, countdown: { totalSeconds } }

Применяется к: Операции с экономикой (открытие кейсов, спины, salvage, крафт). Не применяется к: Чтение данных (сезон, статистика, leaderboard).

// Использование в routes
fastify.post('/api/cases/open', {
preHandler: [authMiddleware, requireActiveSeason(prisma)]
}, handler);

Inter-Season Overlay (Frontend)

SeasonCountdownOverlay — fullscreen overlay, управляемый через useSeason() hook:

РежимКонтентОбновление
endingCountdown MM:SS до конца сезонаPolling каждые 30 сек
interSeasonРезультаты прошлого сезона + countdown DD:HH:MM:SS до следующегоPolling каждые 30 сек
noSeasonРезультаты последнего сезона (если был)Polling каждые 30 сек
activeOverlay скрытPolling отключён

useSeason() hook возвращает:

  • displayStatus — текущее состояние
  • season — DTO активного сезона (для active/ending)
  • lastSeason — результаты прошлого сезона (для interSeason/noSeason)
  • nextSeason — инфо о следующем сезоне (для ending/interSeason)
  • countdown — секунды до перехода
  • isOverlayVisibledisplayStatus !== 'active'
  • isActivedisplayStatus === 'active'

Frontend Integration (LoadingOrchestrator)

Early System Check — проверка доступности сезона выполняется вторым шагом (STEP 0.5) в LoadingOrchestrator.initialize(), после maintenance check, но до загрузки Bootstrap.

Зачем: Без активного сезона пользователь не может взаимодействовать с приложением (все критичные endpoints защищены requireActiveSeason middleware). Early check предотвращает бесполезную загрузку Bootstrap (~8 запросов к БД), показывает SeasonCountdownOverlay сразу.

Как работает:

  1. LoadingOrchestrator устанавливает loadingState = 'checking_season'
  2. Выполняется запрос к /api/seasons/current (без auth, всегда 200)
  3. Если displayStatus !== 'active' → гидрируется React Query cache, показывается SeasonCountdownOverlay, инициализация останавливается
  4. Если displayStatus === 'active' → продолжается дальнейшая загрузка (onboarding, bootstrap)

React Query Cache Hydration:

// Если сезон не активен — сохраняем данные в cache
if (seasonStatus.displayStatus !== 'active') {
if (seasonStatus.data) {
queryClient.setQueryData(['season'], seasonStatus.data);
}
// SeasonCountdownOverlay может сразу использовать эти данные
// без дополнительного запроса через useSeason()
}

Graceful Degradation:

try {
const status = await checkSeasonStatus();
if (status.displayStatus !== 'active') {
// Блокируем Bootstrap
return;
}
} catch (error) {
// При ошибке НЕ блокируем пользователя
console.warn('Season check failed, continuing');
// Возвращаем безопасный default: displayStatus = 'active'
// Продолжаем к следующему шагу
}
Принцип Graceful Degradation

Если проверка сезона упала с ошибкой (нет интернета, 500 от backend), пользователь не блокируется. Предполагается что сезон активен, и пользователь может продолжить. Это предотвращает ситуацию, когда баг в проверке сезона блокирует всех пользователей.

Файлы:

  • frontend/src/components/LoadingOrchestrator.tsx — функция checkSeasonStatus(), вызов в initialize(), cache hydration
  • frontend/src/types/loading.types.tschecking_season loading state
  • frontend/src/hooks/useSeason.ts — query key ['season'] для cache

Edge Cases

СитуацияUI поведение
Нет активного сезона, есть SCHEDULEDOverlay interSeason с countdown до startDate
Нет ни ACTIVE, ни SCHEDULEDOverlay noSeason с результатами последнего сезона
Пользователь не в топ-10Показ distanceToTop10 — сколько XP нужно
Сезон только завершилсяOverlay interSeason с finalResults победителей
LoadingOrchestrator при неактивном сезонеEarly check (STEP 0.5) блокирует Bootstrap → экономия ~8 DB запросов
Ошибка раздачи наградЛогирование, retry не реализован (ручное исправление)
Перезагрузка сервераrescheduleAllOnBoot() восстанавливает таймеры SCHEDULED сезонов
setTimeout > 24.8 днейРекурсивное перепланирование через MAX_TIMEOUT_MS

Admin Notifications

Уведомления для админов через Telegram при событиях сезона (топик SEASONS):

СобытиеТипКогда
Сезон стартовалSEASON_STARTEDПри активации (timer или ручной старт)
Сезон завершёнSEASON_ENDEDПосле автоматического/ручного завершения
Нужна настройкаSEASON_NEEDS_SETUPНет следующего SCHEDULED сезона после завершения
Скоро конецSEASON_ENDING_SOONЗа 7, 3, 1 день до endDate

Все события содержат seasonId для прямых ссылок на админ-панель.

Конфигурация
ADMIN_TELEGRAM_CHAT_ID=<chat_id>  # ID чата/группы для уведомлений
ADMIN_PANEL_URL=https://admin.goloot.online # URL для ссылок

При отсутствии ADMIN_TELEGRAM_CHAT_ID сервис работает в silent mode (логирует, не падает).

WARNING_DAYS: [7, 3, 1] — за сколько дней отправлять предупреждения. Cron каждый день в 10:00.


3. ADR (Architectural Decisions)

Почему Serializable транзакции для смены сезона?

Проблема: Смена сезона — критическая операция. Race condition между деактивацией старого и созданием нового сезона может привести к 0 или 2 активным сезонам.

Решение: Serializable transaction в atomicSeasonTransition:

  1. Проверка что сезон не изменился за время подготовки
  2. Деактивация старого + создание нового в одной транзакции
  3. Isolation level Serializable предотвращает phantom reads

Альтернативы (отклонены):

  • Distributed lock (Redis) — усложнение инфраструктуры
  • Optimistic locking — не гарантирует isolation

Последствия: Высокая надёжность, но блокировка таблицы Season на время транзакции (~100-500ms).

Почему длительность сезона 3 месяца?

Проблема: Без сброса игроки накапливают слишком много предметов, что приводит к массовым выводам и нагрузке на экономику.

Решение: 3-месячный цикл с полным сбросом:

  • Контроль объёма накопленных предметов
  • Предсказуемые пики вывода (конец сезона)
  • Справедливый старт для всех

Альтернативы (отклонены):

  • 1 месяц — слишком короткий, нет времени для гонки
  • 6 месяцев — переизбыток предметов, снижение ценности

Последствия: Регулярные сбросы, но управляемая экономика.

Почему inter-season overlay вместо COUNTDOWN статуса?

Проблема: Исходно был отдельный статус COUNTDOWN в enum, что создавало:

  • Дублирование isActive + status (риск рассинхронизации)
  • 5-state machine вместо 4 (переусложнение)
  • UI не мог однозначно определить что показывать

Решение: Удалить COUNTDOWN из enum, вычислять displayStatus на лету:

  • ending = ACTIVE сезон, но ≤ 10 мин до конца
  • interSeason = нет ACTIVE, но есть SCHEDULED (показ результатов + countdown)
  • noSeason = нет ни ACTIVE, ни SCHEDULED

Альтернативы (отклонены):

  • Хранить displayStatus в БД — двойной source of truth
  • Оставить COUNTDOWN — лишнее состояние, дублирование с isActive

Последствия: Чистый 4-state machine. UI-состояния определяются временем, а не хранятся.

Почему SeasonTimerService (setTimeout) вместо cron для активации?

Проблема: Cron с 5-минутным интервалом не даёт точности запуска. Если сезон должен стартовать в 18:00 — cron активирует его в промежутке 18:00-18:04.

Решение: SeasonTimerService с точным setTimeout на startDate:

  • Точность до миллисекунды
  • При перезагрузке: rescheduleAllOnBoot() восстанавливает таймеры
  • Обход лимита setTimeout (24.8 дня) через рекурсивное перепланирование

Альтернативы (отклонены):

  • Cron каждую минуту — избыточная нагрузка, всё равно ~1 мин задержки
  • Внешний scheduler (Bull/BullMQ) — лишняя зависимость для одной задачи

Последствия: Точный старт, но таймеры живут в памяти процесса. При crash — восстановление при следующем boot.

Почему денормализация imageUrl в rewards JSON?

Проблема: При каждом запросе сезона нужно обогащать награды данными из Item (imageUrl, tier).

Решение: Денормализация при сохранении через prepareRewardsForSave():

  • imageUrl и itemTier сохраняются в JSON rewards
  • Single Source of Truth остаётся в Item table
  • Обновление при изменении сезона

Альтернативы (отклонены):

  • Runtime enrichment — лишний JOIN на каждый запрос
  • Полное копирование Item данных — избыточно

Последствия: Быстрые чтения, но при изменении Item нужно помнить обновить сезоны.

Почему валидация наград через Steam бота?

Проблема: Админ может выбрать скин для награды, которого нет на Steam боте → победитель не получит приз.

Решение: Двухуровневая валидация в validateRewards():

  1. Проверка существования Item в БД с itemType = 'SKIN'
  2. Проверка наличия скина в инвентаре Steam бота через check-reward-availability

Особенности:

  • Один скин можно использовать для нескольких мест (top-1, top-3, top-10)
  • Graceful degradation при недоступности Steam API

Последствия: Гарантия выполнимости награды, но зависимость от Steam API.

Почему 3-шаговая защита завершения сезона?

Проблема: Случайное завершение сезона необратимо — сброс прогресса тысяч пользователей.

Решение: Multi-step confirmation flow:

  1. Предупреждение — список последствий (награды, сброс, новый сезон)
  2. Ввод текста — точное совпадение "ЗАВЕРШИТЬ СЕЗОН N" (case-sensitive)
  3. Пароль админа — финальное подтверждение через AuthService.validateCredentials()

Альтернативы (отклонены):

  • Одна кнопка с confirm — слишком просто для критической операции
  • 2FA — усложняет инфраструктуру

Последствия: Защита от случайных кликов, но медленнее для намеренного завершения.

Каскадный порядок удаления сезона

Проблема: При удалении сезона необходимо соблюдать порядок удаления связанных данных из-за FK constraints.

Решение: Строгий порядок в транзакции:

  1. UserSeasonStats — статистика пользователей
  2. CraftBudgetLog — логи крафт-бюджета (FK на BudgetPeriod)
  3. BudgetPeriod — периоды бюджета
  4. LuckPoolEntry — записи пула удачи
  5. Season — сам сезон

Критично: CraftBudgetLog должен удаляться до BudgetPeriod из-за craft_budget_logs_periodId_fkey.

Последствия: Надёжное каскадное удаление без FK violations.


4. Architecture

Services Overview

Cron Jobs

JobScheduleОписание
Season Lifecycle*/5 * * * * (каждые 5 мин)Проверка окончания ACTIVE сезона, 8-step transition
Rank Update*/10 * * * * (каждые 10 мин)Batch update рангов
Pool Update0 0,12 * * * (дважды в день)Luck Pool processing
Period Check5 0 * * * (ежедневно)Budget period transition
Warning0 10 * * * (ежедневно 10:00)Уведомления о скором конце сезона
Активация через SeasonTimerService

Активация SCHEDULED → ACTIVE выполняется не cron-ом, а точным setTimeout через SeasonTimerService. Это обеспечивает запуск сезона ровно в указанное время (HH:MM UTC), а не с задержкой до 5 мин.

Key Components

КомпонентПутьОписание
SeasonServiceseason.service.tsПолучение сезонов, getSeasonInfo() → displayStatus
SeasonTimerServiceseason-timer.service.tssetTimeout-based активация SCHEDULED сезонов
SeasonStatusMiddlewareseason-status.middleware.ts503 при NO_ACTIVE_SEASON / SEASON_ENDING
SeasonStatsServiceseason-stats.service.tsСтатистика пользователя, leaderboard
SeasonRewardServiceseason-reward.service.tsРаздача наград топ-10
SeasonResetServiceseason-reset.service.tsСброс прогресса между сезонами
SeasonRepositoryseason.repository.tsDB операции, batch rank update
SeasonLifecycleJobseason-lifecycle.job.tsCron orchestrator
RankUpdateJobrank-update.job.tsПериодическое обновление рангов
AdminNotificationServiceadmin-notification.service.tsTelegram уведомления для админов
User Routesseason.routes.ts/api/seasons/*
Admin Routesadmin-seasons.routes.ts/admin/seasons/*
SeasonQuizServiceseason-quiz.service.tsГенерация, импорт, валидация квизов сезона
QuizGeneratorServicequiz-generator.service.tsTemplate-based генерация квизов из SQLite данных
SqliteServicesqlite.service.tsRead-only доступ к Rust game data (rust_data.db)
SeasonQuestGeneratorServiceseason-quest-generator.service.tsSmart Quest Generation из блюпринтов
SeasonCloneServiceseason-clone.service.tsКлонирование контента между сезонами
SeasonSetupServiceseason-setup.service.tsУправление шагами Setup Wizard
SeasonCaseServiceseason-case.service.tsПривязка кейсов к сезону
SeasonSpinServiceseason-spin.service.tsПривязка рулеток к сезону
ContentAllocationServiceseason-content-allocation.service.tsФормирование пула контента сезона
ContentValidationServicecontent-validation.service.tsВалидация достижимости контента перед стартом
ExhaustionMonitorServiceexhaustion-monitor.service.tsМониторинг исчерпания контента и автосброс пула
Quest Blueprintsquest-blueprints.ts29 шаблонов генерации квестов (3 CASES, 3 QUIZ, 8 RECYCLE, 12 COLLECTION, 1 SOCIAL, 2 SPECIAL)
AchievementGeneratorServiceachievement-generator.service.tsSmart Achievement Generation (5-phase, blueprint-based)
Achievement Blueprintsachievement-blueprints.ts17 шаблонов генерации достижений (4 QUIZ, 3 CASES, 2 COLLECTION, 2 RECYCLE, 1 SOCIAL, 1 STREAK, 2 ECONOMY, 2 PROGRESSION)
Admin Setup Routesadmin-season-setup.routes.ts/admin/season-setup/* (wizard)

5. Database Schema

Models

МодельОписаниеКлючевые поля
SeasonСезонnumber, name, startDate, endDate, status, scheduledAt, actualStartAt, rewards, finalResults
UserSeasonStatsСтатистика за сезонuserId, seasonId, xp, scrap, level, rank, breakdown полей
SeasonRewardClaimПолученные наградыuserId, seasonId, position, rewardSnapshot

Relationships

Season Status Enum

enum SeasonStatus {
DRAFT = 'DRAFT', // Админ настраивает контент сезона
SCHEDULED = 'SCHEDULED', // Настройка завершена, ждёт активации
ACTIVE = 'ACTIVE', // Активный сезон
COMPLETED = 'COMPLETED' // Завершён
}

Shared Constants (SEASON_LIFECYCLE)

// shared/src/constants/seasonLifecycle.ts
export const SEASON_LIFECYCLE = {
INTER_SEASON_MINUTES: 10, // Минуты до endDate → "ending" state, операции блокируются
CRON_TRANSITION_INTERVAL: '*/5 * * * *', // Проверка завершения ACTIVE сезона
WARNING_DAYS: [7, 3, 1], // Дни до конца → Telegram уведомления админам
} as const;
UserSeasonStats — полная структура полей

Балансы:

  • scrap, scrapTotalEarned, xp, level

Scrap breakdown:

  • scrapFromQuizzes, scrapFromTasks, scrapFromAchievements
  • scrapFromCases, scrapFromSpins, scrapFromReferrals

XP breakdown:

  • xpFromTasks, xpFromAchievements, xpFromStreaks
  • xpFromReferrals, xpFromCases, xpFromSpins
  • xpFromSalvage, xpFromAdmin

Quiz stats:

  • quizzesCompleted, correctAnswers, incorrectAnswers
  • currentStreak, bestStreak

Activity:

  • casesOpened, dailyCasesOpened, itemsCrafted
  • itemsSalvaged, dailySpinsUsed, tasksCompleted
  • achievementsUnlocked, friendsInvited

Rank:

  • rank — текущая позиция
  • rankUpdAt — когда обновлялось

6. API Endpoints

МетодЭндпоинтОписаниеDocs
GET/api/seasons/currentТекущий сезон + displayStatus (всегда 200)
GET/api/seasons/statsСтатистика пользователя за сезон
GET/api/seasons/leaderboardТоп игроков + позиция пользователя

  • Cases — источник XP и Scrap
  • Daily Spins — источник XP и Scrap
  • Budget — Budget Periods привязаны к сезону
  • Quizzes — квизы генерируются для сезона (Step 5 Setup Wizard)
  • Quests — источник XP за выполнение заданий
  • Achievements — источник XP за разблокировку
  • Streaks — влияет на XP multiplier
  • Inventory — SEASON_REWARD items хранятся отдельно