Seasons
1. Summary
Goal: Временные соревновательные периоды (3 месяца) с рейтингом по XP. Контролирует жизненный цикл игровой экономики: обнуление прогресса, награды для топ-10, обратный отсчёт до старта нового сезона. Периодический сброс предотвращает переизбыток накопленных предметов и контролирует объём выводов скинов.
User Value: Соревновательная мотивация. Попадание в топ-10 гарантирует реальный скин в Steam — уникальная награда за активность без денежных вложений. Игроки вне топ-10 не получают сезонных наград — это элитарная система.
2. Business Logic
Season Lifecycle
4-state machine: DRAFT → SCHEDULED → ACTIVE → COMPLETED
- DRAFT
- SCHEDULED
- ACTIVE
- COMPLETED
Период: Настройка контента через Setup Wizard (8 шагов)
Доступные действия:
- Настройка названия, дат, наград
- Привязка кейсов, рулеток
- Генерация квизов, квестов, достижений
- Клонирование контента из другого сезона
Переход в SCHEDULED: При утверждении через Approve (Step 8)
Период: Сезон настроен и ожидает активации по startDate
Механика активации:
SeasonTimerServiceустанавливает точныйsetTimeoutнаstartDate- При срабатывании таймера:
SCHEDULED → ACTIVE,actualStartAt = now() - Альтернатива: ручной старт админом (multi-step confirmation)
- При перезагрузке сервера:
rescheduleAllOnBoot()восстанавливает все таймеры
Переход в ACTIVE: Автоматически по startDate или ручной старт
Период: Активный сезон (3 месяца)
Механики:
- Накопление XP из всех источников
- Ранжирование в реальном времени (обновление каждые 10 мин)
- Полный доступ ко всем механикам (кейсы, спины, квесты)
Ending phase: За INTER_SEASON_MINUTES (10 мин) до endDate операции блокируются middleware, показывается overlay с обратным отсчётом.
Переход в COMPLETED: Автоматически при достижении endDate (cron каждые 5 мин)
Период: Завершённый сезон (архив)
8-шаговая атомарная транзиция:
- Финальное обновление рангов
- Раздача наград топ-10 (идемпотентно через
SeasonRewardClaim) - Сбор
finalResultsJSON (до сброса!) - Архивация квестов сезона
- Сброс прогресса всех пользователей
- Завершение content budget
- Serializable TX:
ACTIVE → COMPLETED, следующийSCHEDULED → ACTIVE - Telegram уведомления админам
Хранение: finalResults JSON с топ-игроками и статистикой
Reward Tiers
| Позиция | Tier | Типичная награда |
|---|---|---|
| #1 | legendary | Самый ценный скин сезона |
| #2-3 | mythical | Скин высокой ценности |
| #4-10 | epic | Скин средней ценности |
| #11+ | — | Без награды |
imageUrl и itemTier сохраняются в JSON rewards при создании сезона для быстрого отображения без JOIN на таблицу Item.
XP Sources (детализация)
Все источники XP учитываются в UserSeasonStats:
| Источник | Поле | Описание |
|---|---|---|
| Quests | xpFromTasks | Выполнение квестов |
| Achievements | xpFromAchievements | Разблокировка достижений |
| Cases | xpFromCases | Открытие кейсов |
| Spins | xpFromSpins | Daily Spins |
| Referrals | xpFromReferrals | Приглашение друзей |
| Salvage | xpFromSalvage | Разбор предметов |
| Admin | xpFromAdmin | Ручное начисление |
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.
| Шаг | Название | Описание |
|---|---|---|
| 1 | Basic Info | Название, описание, даты, время старта (HH:MM UTC) |
| 2 | Rewards | Награды для топ-1, топ-3, топ-10 (скины) |
| 3 | Cases | Привязка кейсов к сезону |
| 4 | Spins | Привязка рулеток к сезону |
| 5 | Quizzes | Массовая генерация, точечная генерация или импорт квизов |
| 6 | Quests | Smart Quest Generation из блюпринтов |
| 7 | Achievements | Smart Achievement Generation из блюпринтов |
| 8 | Approve | Утверждение и планирование старта (только Setup 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
В шаге Basic Info (Step 1) секция "Быстрый старт" позволяет выбрать source-сезон из dropdown и клонировать одним нажатием.
Smart Quest Generation (Step 6)
Квесты генерируются автоматически из code-defined блюпринтов (29 шт.) с учётом контента сезона.
Алгоритм (5 фаз):
- Analyze Content — собрать cases, spins, items (с deduplication по dropChance), scrap analysis, quiz categories/subcategories/slugs/entityTypes
- Calculate Pool Sizes — DAILY/WEEKLY/PERMANENT целевые размеры (с 30% буфером)
- Instantiate Blueprints — создать кандидатов из блюпринтов × контент (content-aware: COLLECTION блюпринты проверяют наличие соответствующих предметов/скрапа)
- Balance Distribution — обрезать избыток, приоритизируя сложные квесты
- Save to Database — транзакция: удалить старые + создать новые
Генерация по режимам (7 режимов):
| Режим | Варианты | Примеры |
|---|---|---|
generic | До 3 (DAILY/WEEKLY), 1 (PERMANENT) | "Охотник за удачей I/II/III" |
per-case | 1 на каждый кейс сезона | "Фанат «Discharge»" |
per-item | 1 на high-tier предмет (max 3) | "За «AK-47»!" |
per-quiz-category | 1 на категорию квизов | "Знаток «Оружие»" |
per-quiz-subcategory | 1 на подкатегорию (min 3 квиза) | "Учёный: Штурмовые" |
per-quiz-slug | 1 на предмет/сущность (min 3 квиза) | "Эксперт по AK-47" |
per-quiz-entitytype | 1 на тип сущности (min 3 квиза) | "Исследователь: Предметы" |
Content-Aware COLLECTION: Генератор анализирует реальный контент сезона — предметы и скрап из кейсов и рулеток. COLLECTION блюпринты (12 шт.) генерируют квесты только если соответствующий контент существует (ресурсы, фрагменты, чертежи, скрап). Скрап-квесты динамически капируются на основе среднего дропа.
Quiz-квесты используют только generic режим с фиксированными таргетами (daily=1, weekly=5, permanent=15). Специфичные quiz-задания будут реализованы как Achievement templates. Подробнее: Quests ADR
Полная матрица блюпринтов: Quest Blueprints
Генерируемые квесты получают placeholder SCRAP награды (Easy=50, Medium=100, Hard=150). Админ настраивает финальные награды после генерации.
Помимо автогенерации, админ может создать квест вручную прямо из визарда:
- Кнопка «+» в заголовке каждой категории — создаёт квест с предзаполненной категорией
- Кнопка «Создать квест» — создаёт без привязки к категории
- Two-step flow: форма создания → автоматическая привязка к сезону
- После создания запускается фоновая валидация сезона
- Кнопки скрыты для завершённых сезонов (
COMPLETED)
Achievement Generation (Step 7)
Достижения генерируются автоматически из шаблонов (blueprints).
Аналогично квестам, админ может создать достижение вручную:
- Кнопка «+» в заголовке каждой категории — создаёт с предзаполненной категорией
- Кнопка «Создать достижение» — создаёт без привязки к категории
- Two-step flow: форма создания → привязка к сезону через
POST /:seasonId/achievements/add - Кнопки скрыты для завершённых сезонов (
COMPLETED)
Quiz Generation (Step 5)
Квизы генерируются автоматически из шаблонов + SQLite базы данных Rust. Доступны два подхода: массовая генерация по категориям и точечная генерация по конкретным предметам.
Массовая генерация (Primary)
Входные данные:
categories— массив категорий:weapons,food,raid,attiretotal— общее количество квизов (по умолчанию 270)mode— режим генерации:regenerate(по умолчанию) илиappend
Режимы генерации:
| Режим | Поведение | Бюджет | Used counters |
|---|---|---|---|
regenerate | Удаляет все существующие квизы, создаёт новые | SET (абсолютное значение) | Сброс до 0 |
append | Добавляет новые к существующим без удаления | INCREMENT (прибавление) | Без изменений |
Append-режим исключает дубли: перед генерацией собираются вопросы существующих квизов сезона, и новые генерируются только из оставшегося пула. Подтверждение не требуется — операция безопасна.
Алгоритм распределения:
total / categories.lengthквизов на каждую категорию- Внутри категории: 50% EASY, 30% MEDIUM, 20% HARD
- Для каждого слота: случайный предмет из SQLite × случайный шаблон
Точечная генерация (Targeted)
Позволяет генерировать квизы для конкретных предметов (shortnames) вместо целых категорий. Используется когда нужно добавить квизы для отдельных предметов — например, при создании slug-targeted достижений.
UI: Collapsible-секция «Точечная генерация» в Step 5 Wizard.
Flow:
- Админ выбирает категорию → загружаются предметы с количеством доступных шаблонов
- Опциональная фильтрация по подкатегории и поиску по имени
- Выбор конкретных предметов (чекбоксы, Select All / Deselect All)
- Опциональное ограничение количества (по умолчанию = максимум из шаблонов)
- Генерация → результат:
+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 Limit | Auth | Validation |
|---|---|---|---|
| Получить текущий сезон | general (100/min) | Telegram | GetCurrentSeasonSchema |
| Получить статистику | general (100/min) | Telegram | GetSeasonStatsSchema |
| Получить leaderboard | general (100/min) | Telegram | GetLeaderboardSchema |
| Завершить сезон (admin) | mutations (5/min) | Admin JWT + пароль | AdminForceEndSeasonSchema |
| Запустить сезон (admin) | mutations (5/min) | Admin JWT + пароль | AdminStartSeasonSchema |
См. Security Matrix для полного обзора защит.
Display Status (Frontend States)
Backend возвращает displayStatus — вычисляемое состояние, определяющее что показывать пользователю:
| displayStatus | Когда | UI | Операции |
|---|---|---|---|
active | ACTIVE сезон, > INTER_SEASON_MINUTES до конца | Обычный интерфейс | ✅ Все доступны |
ending | ACTIVE сезон, ≤ INTER_SEASON_MINUTES до конца | Overlay с обратным отсчётом MM:SS | ❌ Заблокированы |
interSeason | Нет ACTIVE, но есть SCHEDULED | Overlay: результаты прошлого сезона + countdown до следующего | ❌ Заблокированы |
noSeason | Нет ни ACTIVE, ни SCHEDULED | Overlay: результаты последнего сезона (если был) | ❌ Заблокированы |
displayStatus вычисляется в SeasonService.getSeasonInfo() на основе текущего состояния БД и времени. Не хранится в модели Season.
Season Timer Service
Точное планирование активации сезонов через setTimeout:
| Метод | Описание |
|---|---|
scheduleActivation(seasonId, startDate) | Установить таймер на конкретную дату |
cancelActivation(seasonId) | Отменить запланированную активацию |
rescheduleAllOnBoot() | При перезагрузке: восстановить таймеры для всех SCHEDULED сезонов |
getActiveTimers() | Мониторинг: список ID сезонов с активными таймерами |
Обработка ограничения setTimeout:
- JavaScript
setTimeoutограничен ~24.8 днями (2^31 - 1ms) - Для более далёких дат: рекурсивное перепланирование через
MAX_TIMEOUT_MS - Если
startDateуже в прошлом → немедленная активация
Интеграция: При Approve (Step 8) wizard устанавливает scheduledAt с датой и временем (HH:MM UTC). SeasonTimerService создаёт таймер. При ручном старте или удалении — таймер отменяется.
Season Status Middleware
Middleware requireActiveSeason блокирует операции при отсутствии или завершении сезона:
| Ошибка | HTTP | Условие | Payload |
|---|---|---|---|
NO_ACTIVE_SEASON | 503 | Нет сезона со status=ACTIVE | { error, message } |
SEASON_ENDING | 503 | ACTIVE, но ≤ 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:
| Режим | Контент | Обновление |
|---|---|---|
ending | Countdown MM:SS до конца сезона | Polling каждые 30 сек |
interSeason | Результаты прошлого сезона + countdown DD:HH:MM:SS до следующего | Polling каждые 30 сек |
noSeason | Результаты последнего сезона (если был) | Polling каждые 30 сек |
active | Overlay скрыт | Polling отключён |
useSeason() hook возвращает:
displayStatus— текущее состояниеseason— DTO активного сезона (дляactive/ending)lastSeason— результаты прошлого сезона (дляinterSeason/noSeason)nextSeason— инфо о следующем сезоне (дляending/interSeason)countdown— секунды до переходаisOverlayVisible—displayStatus !== 'active'isActive—displayStatus === 'active'
Frontend Integration (LoadingOrchestrator)
Early System Check — проверка доступности сезона выполняется вторым шагом (STEP 0.5) в LoadingOrchestrator.initialize(), после maintenance check, но до загрузки Bootstrap.
Зачем: Без активного сезона пользователь не может взаимодействовать с приложением (все критичные endpoints защищены requireActiveSeason middleware). Early check предотвращает бесполезную загрузку Bootstrap (~8 запросов к БД), показывает SeasonCountdownOverlay сразу.
Как работает:
LoadingOrchestratorустанавливаетloadingState = 'checking_season'- Выполняется запрос к
/api/seasons/current(без auth, всегда 200) - Если
displayStatus !== 'active'→ гидрируется React Query cache, показываетсяSeasonCountdownOverlay, инициализация останавливается - Если
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'
// Продолжаем к следующему шагу
}
Если проверка сезона упала с ошибкой (нет интернета, 500 от backend), пользователь не блокируется. Предполагается что сезон активен, и пользователь может продолжить. Это предотвращает ситуацию, когда баг в проверке сезона блокирует всех пользователей.
Файлы:
frontend/src/components/LoadingOrchestrator.tsx— функцияcheckSeasonStatus(), вызов вinitialize(), cache hydrationfrontend/src/types/loading.types.ts—checking_seasonloading statefrontend/src/hooks/useSeason.ts— query key['season']для cache
Edge Cases
| Ситуация | UI поведение |
|---|---|
| Нет активного сезона, есть SCHEDULED | Overlay interSeason с countdown до startDate |
| Нет ни ACTIVE, ни SCHEDULED | Overlay 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:
- Проверка что сезон не изменился за время подготовки
- Деактивация старого + создание нового в одной транзакции
- 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():
- Проверка существования Item в БД с
itemType = 'SKIN' - Проверка наличия скина в инвентаре Steam бота через
check-reward-availability
Особенности:
- Один скин можно использовать для нескольких мест (top-1, top-3, top-10)
- Graceful degradation при недоступности Steam API
Последствия: Гарантия выполнимости награды, но зависимость от Steam API.
Почему 3-шаговая защита завершения сезона?
Проблема: Случайное завершение сезона необратимо — сброс прогресса тысяч пользователей.
Решение: Multi-step confirmation flow:
- Предупреждение — список последствий (награды, сброс, новый сезон)
- Ввод текста — точное совпадение "ЗАВЕРШИТЬ СЕЗОН N" (case-sensitive)
- Пароль админа — финальное подтверждение через
AuthService.validateCredentials()
Альтернативы (отклонены):
- Одна кнопка с confirm — слишком просто для критической операции
- 2FA — усложняет инфраструктуру
Последствия: Защита от случайных кликов, но медленнее для намеренного завершения.
Каскадный порядок удаления сезона
Проблема: При удалении сезона необходимо соблюдать порядок удаления связанных данных из-за FK constraints.
Решение: Строгий порядок в транзакции:
UserSeasonStats— статистика пользователейCraftBudgetLog— логи крафт-бюджета (FK на BudgetPeriod)BudgetPeriod— периоды бюджетаLuckPoolEntry— записи пула удачиSeason— сам сезон
Критично: CraftBudgetLog должен удаляться до BudgetPeriod из-за craft_budget_logs_periodId_fkey.
Последствия: Надёжное каскадное удаление без FK violations.
4. Architecture
Services Overview
Cron Jobs
| Job | Schedule | Описание |
|---|---|---|
| Season Lifecycle | */5 * * * * (каждые 5 мин) | Проверка окончания ACTIVE сезона, 8-step transition |
| Rank Update | */10 * * * * (каждые 10 мин) | Batch update рангов |
| Pool Update | 0 0,12 * * * (дважды в день) | Luck Pool processing |
| Period Check | 5 0 * * * (ежедневно) | Budget period transition |
| Warning | 0 10 * * * (ежедневно 10:00) | Уведомления о скором конце сезона |
Активация SCHEDULED → ACTIVE выполняется не cron-ом, а точным setTimeout через SeasonTimerService. Это обеспечивает запуск сезона ровно в указанное время (HH:MM UTC), а не с задержкой до 5 мин.
Key Components
| Компонент | Путь | Описание |
|---|---|---|
| SeasonService | season.service.ts | Получение сезонов, getSeasonInfo() → displayStatus |
| SeasonTimerService | season-timer.service.ts | setTimeout-based активация SCHEDULED сезонов |
| SeasonStatusMiddleware | season-status.middleware.ts | 503 при NO_ACTIVE_SEASON / SEASON_ENDING |
| SeasonStatsService | season-stats.service.ts | Статистика пользователя, leaderboard |
| SeasonRewardService | season-reward.service.ts | Раздача наград топ-10 |
| SeasonResetService | season-reset.service.ts | Сброс прогресса между сезонами |
| SeasonRepository | season.repository.ts | DB операции, batch rank update |
| SeasonLifecycleJob | season-lifecycle.job.ts | Cron orchestrator |
| RankUpdateJob | rank-update.job.ts | Периодическое обновление рангов |
| AdminNotificationService | admin-notification.service.ts | Telegram уведомления для админов |
| User Routes | season.routes.ts | /api/seasons/* |
| Admin Routes | admin-seasons.routes.ts | /admin/seasons/* |
| SeasonQuizService | season-quiz.service.ts | Генерация, импорт, валидация квизов сезона |
| QuizGeneratorService | quiz-generator.service.ts | Template-based генерация квизов из SQLite данных |
| SqliteService | sqlite.service.ts | Read-only доступ к Rust game data (rust_data.db) |
| SeasonQuestGeneratorService | season-quest-generator.service.ts | Smart Quest Generation из блюпринтов |
| SeasonCloneService | season-clone.service.ts | Клонирование контента между сезонами |
| SeasonSetupService | season-setup.service.ts | Управление шагами Setup Wizard |
| SeasonCaseService | season-case.service.ts | Привязка кейсов к сезону |
| SeasonSpinService | season-spin.service.ts | Привязка рулеток к сезону |
| ContentAllocationService | season-content-allocation.service.ts | Формирование пула контента сезона |
| ContentValidationService | content-validation.service.ts | Валидация достижимости контента перед стартом |
| ExhaustionMonitorService | exhaustion-monitor.service.ts | Мониторинг исчерпания контента и автосброс пула |
| Quest Blueprints | quest-blueprints.ts | 29 шаблонов генерации квестов (3 CASES, 3 QUIZ, 8 RECYCLE, 12 COLLECTION, 1 SOCIAL, 2 SPECIAL) |
| AchievementGeneratorService | achievement-generator.service.ts | Smart Achievement Generation (5-phase, blueprint-based) |
| Achievement Blueprints | achievement-blueprints.ts | 17 шаблонов генерации достижений (4 QUIZ, 3 CASES, 2 COLLECTION, 2 RECYCLE, 1 SOCIAL, 1 STREAK, 2 ECONOMY, 2 PROGRESSION) |
| Admin Setup Routes | admin-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,scrapFromAchievementsscrapFromCases,scrapFromSpins,scrapFromReferrals
XP breakdown:
xpFromTasks,xpFromAchievements,xpFromStreaksxpFromReferrals,xpFromCases,xpFromSpinsxpFromSalvage,xpFromAdmin
Quiz stats:
quizzesCompleted,correctAnswers,incorrectAnswerscurrentStreak,bestStreak
Activity:
casesOpened,dailyCasesOpened,itemsCrafteditemsSalvaged,dailySpinsUsed,tasksCompletedachievementsUnlocked,friendsInvited
Rank:
rank— текущая позицияrankUpdAt— когда обновлялось
6. API Endpoints
- User API
- Admin: CRUD
- Admin: Lifecycle
- Admin: Setup Wizard
- Admin: Rewards
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /api/seasons/current | Текущий сезон + displayStatus (всегда 200) | → |
| GET | /api/seasons/stats | Статистика пользователя за сезон | → |
| GET | /api/seasons/leaderboard | Топ игроков + позиция пользователя | → |
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /admin/seasons | Все сезоны с пагинацией | → |
| GET | /admin/seasons/:id | Детали сезона | → |
| POST | /admin/seasons | Создать сезон | → |
| PUT | /admin/seasons/:id | Обновить сезон | → |
| POST | /admin/seasons/:id/delete | Удалить (с подтверждением) | → |
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /admin/seasons/lifecycle/status | Статус lifecycle job | → |
| GET | /admin/seasons/:id/preview-end | Превью завершения сезона | → |
| POST | /admin/seasons/:id/end | Принудительное завершение | → |
| POST | /admin/seasons/:id/start | Ручной старт из SCHEDULED (multi-step confirmation) | → |
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /admin/season-setup/:seasonId/status | Статус настройки сезона |
| PUT | /admin/season-setup/:seasonId/basic-info | Обновить название, даты |
| PUT | /admin/season-setup/:seasonId/rewards | Настроить награды (top1/3/10) |
| POST | /admin/season-setup/:seasonId/cases | Добавить кейсы |
| POST | /admin/season-setup/:seasonId/spins | Добавить рулетки |
| GET | /admin/season-setup/:seasonId/quizzes/categories | Доступные категории для генерации |
| POST | /admin/season-setup/:seasonId/quizzes/generate | Массовая генерация квизов (mode: regenerate/append) |
| GET | /admin/season-setup/quizzes/items?category&subcategory? | Предметы категории с количеством шаблонов (для точечной генерации) |
| POST | /admin/season-setup/:seasonId/quizzes/targeted | Точечная генерация квизов по конкретным предметам |
| POST | /admin/season-setup/:seasonId/quizzes/import | Импорт квизов |
| GET | /admin/season-setup/:seasonId/quizzes/stats | Статистика квизов сезона |
| POST | /admin/season-setup/:seasonId/quests/generate | Smart Quest Generation |
| POST | /admin/season-setup/:seasonId/clone-from | Клонировать контент из другого сезона |
| POST | /admin/season-setup/:seasonId/complete-step/:step | Завершить шаг настройки |
| POST | /admin/season-setup/:seasonId/approve | Утвердить сезон |
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /admin/seasons/available-rewards | Доступные скины для наград | → |
| GET | /admin/seasons/check-reward-availability | Проверка наличия на Steam боте | → |
| GET | /admin/seasons/next | Черновик следующего сезона | → |
| POST | /admin/seasons/next | Создать/обновить черновик | → |
| GET | /admin/seasons/:id/history | История сезона (топ + статистика) | → |
7. Related
- 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 хранятся отдельно