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 (10 шагов)
Доступные действия:
- Настройка названия, дат, наград
- Привязка кейсов, рулеток
- Генерация квизов, квестов, достижений
- Клонирование контента из другого сезона
Переход в SCHEDULED: При утверждении через Approve (Step 10)
Период: Сезон настроен и ожидает активации по startDate
Механика активации:
SeasonTimerServiceустанавливает точныйsetTimeoutнаstartDate- При срабатывании таймера:
SCHEDULED → ACTIVE,actualStartAt = now() - Альтернатива: ручной старт админом (multi-step confirmation)
- При перезагрузке сервера:
rescheduleAllOnBoot()восстанавливает все таймеры - При активации инвалидируются кэши квестов (
activeQuests,seasonQuestPool), чтобы квесты нового сезона были доступны сразу
Переход в ACTIVE: Автоматически по startDate или ручной старт
Период: Активный сезон (3 месяца)
Механики:
- Накопление XP из всех источников
- Ранжирование в реальном времени (обновление каждые 10 мин)
- Полный доступ ко всем механикам (кейсы, спины, квесты)
Ending phase: За INTER_SEASON_MINUTES (10 мин) до endDate операции блокируются middleware, показывается overlay с обратным отсчётом.
Переход в COMPLETED: Автоматически при достижении endDate (cron каждые 5 мин).
Lifecycle cron использует прямой DB-запрос (getActiveSeasonDirect) вместо Redis-кэша для проверки endDate. Дополнительный safety net: авто-завершение блокируется если сезон активен менее MIN_ACTIVE_DAYS_FOR_AUTO_TRANSITION (7 дней). Админский force-end не затронут.
Период: Завершённый сезон (архив)
9-шаговая атомарная транзиция:
- Финальное обновление рангов
- Раздача наград топ-10 по XP, рефереров и бустеров (идемпотентно через
SeasonRewardClaim) - Сбор
finalResultsJSON (до сброса!) - Архивация квестов сезона
- Сброс прогресса всех пользователей
- Завершение content budget
- Serializable TX:
ACTIVE → COMPLETED(без авто-активации следующего сезона) - Поиск следующего SCHEDULED сезона → планирование таймера через
SeasonTimerService - 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 | Ручное начисление |
| Raffle | xpFromRaffle | Участие в раффлах |
| Promo Codes | xpFromPromoCodes | Активация промокодов |
| Boosts | xpFromBoosts | Награды Boost Pass |
Scrap Sources (детализация)
Все 11 источников скрапа учитываются в UserSeasonStats (10 через addScrap() + admin-грант через отдельный путь):
| Источник | Поле | Описание |
|---|---|---|
| Quizzes | scrapFromQuizzes | Правильные ответы в квизах |
| Quests | scrapFromTasks | Выполнение квестов |
| Achievements | scrapFromAchievements | Разблокировка достижений |
| Cases | scrapFromCases | Открытие кейсов |
| Spins | scrapFromSpins | Daily Spins |
| Referrals | scrapFromReferrals | Реферальный доход |
| Admin | scrapFromAdmin | Ручное начисление |
| Promo Codes | scrapFromPromoCodes | Активация промокодов |
| Raffle | scrapFromRaffle | Выигрыш в розыгрышах |
| Consolation | scrapFromConsolation | Утешительные призы |
| Boosts | scrapFromBoosts | Награды Boost Pass |
SCRAP_SOURCE_TO_SEASON — полный Record (не Partial). Каждый ScrapSource гарантированно обновляет UserSeasonStats. Admin grants проходят через updateSeasonStats() в interactive transaction.
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): 10 шагов с последовательным прохождением и валидацией.
Edit Mode (SCHEDULED/ACTIVE): 9 шагов со свободной навигацией, per-step save.
| Шаг | Название | Описание |
|---|---|---|
| 1 | Basic Info | Название, описание, даты, время старта (HH:MM UTC) |
| 2 | Budget | Конфигурация бюджета сезона (periods, total, carryover). В ACTIVE — операционный Budget Control |
| 3 | Rewards | Награды для топ-1, топ-3, топ-10 (скины) — сезонный XP лидерборд |
| 4 | Cases | Привязка кейсов к сезону |
| 5 | Spins | Привязка рулеток к сезону |
| 6 | Referral Rewards | Награды для топ-1, топ-3, топ-10 рефереров (скины) — реферальный лидерборд |
| 7 | Quizzes | Массовая генерация, точечная генерация или импорт квизов |
| 8 | Quests | Smart Quest Generation из блюпринтов |
| 9 | Achievements | Smart Achievement Generation из блюпринтов |
| 10 | Approval | Утверждение и планирование старта (только Setup Mode) |
Шаг адаптируется в зависимости от статуса сезона:
- DRAFT/SCHEDULED → Конфигурация: количество периодов (1-30), общий бюджет (опционально), переключатель carryover + превью
- ACTIVE → Config (readonly) + полный операционный Budget Control (статистика, increment/decrement, lock, операции)
- COMPLETED → Всё readonly
isStepCompleted всегда возвращает true — шаг нельзя заблокировать (defaults валидны).
isStepCompleted всегда возвращает true для REFERRAL_REWARDS — шаг нельзя заблокировать.
Статусы шага:
IN_PROGRESS— находимся на шаге прямо сейчасCOMPLETED—referralRewards !== null(награды настроены)WARNING—referralRewards === null && setupStep > 6(явный пропуск через кнопку "Без наград")
Кнопка "Без наград" (только в Setup Mode): вызывает DELETE /referral-rewards → referralRewards = null → статус шага становится WARNING → лидерборд будет работать без предметных наград.
canApprove (Step 10) принимает WARNING как эквивалент COMPLETED — сезон можно утвердить без настройки referral наград.
Если referralRewards не настроены (null), реферальный лидерборд скрыт в UI. Добавить награды можно в любой момент через Edit Mode.
В Edit Mode (SCHEDULED/ACTIVE) навигация между шагами свободная — можно переходить к любому шагу без валидации предыдущих. Шаг Approve отсутствует.
На каждом шаге визарда отображается persistent ValidationBanner — баннер здоровья сезона, который заменил прежнюю ephemeral toast-валидацию при создании/редактировании квестов. Баннер показывает:
- Ошибки (красный), предупреждения (жёлтый) или успешный статус (зелёный)
- Топ-3 проблемы, отсортированные по severity (ошибки первыми)
- Валидация запрашивается через
GET /admin/season-setup/:seasonId/validateсstaleTime: 30s
Компонент: admin/src/components/season-setup/ValidationBanner.tsx
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 8)
Квесты генерируются автоматически из code-defined блюпринтов (38 шт.) с учётом контента сезона.
Алгоритм (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 — транзакция: удалить старые auto-generated + создать новые (manual квесты сохраняются при регенерации)
Генерация по режимам (3 режима):
| Режим | Варианты | Примеры |
|---|---|---|
generic | До 3 (DAILY/WEEKLY), 1 (PERMANENT) | "Охотник за удачей I/II/III" |
per-case | 1 на каждый кейс сезона | "Фанат «Discharge»" |
per-item | 1 на high-tier предмет (max 3) | "За «AK-47»!" |
Специфичные quiz-режимы (per-quiz-category, per-quiz-subcategory, per-quiz-slug, per-quiz-entitytype) реализованы как Achievement blueprints, а не Quest blueprints. Причина: публикация квизов (3/день через Telegram, случайная выборка) не координируется с ротацией квестов, поэтому специфичные quiz-квесты часто были бы невыполнимы в рамках своего временного окна. Достижения -- долгосрочные цели, и эта проблема для них не актуальна.
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)
Удаление квеста из сезона (через «Clear quests» или поштучно) всегда выполняет hard delete — квест, награда, связанные UserQuest, ClaimedUniqueQuest и UserQuestExclusion полностью удаляются из БД. Это касается всех квестов — и auto-generated, и manual/imported. Квесты существуют только в контексте сезона.
Исключение: регенерация (Save to Database) удаляет только auto-generated квесты и сохраняет manual.
Achievement Generation (Step 9)
Достижения генерируются автоматически из шаблонов (blueprints).
Аналогично квестам, админ может создать достижение вручную:
- Кнопка «+» в заголовке каждой категории — создаёт с предзаполненной категорией
- Кнопка «Создать достижение» — создаёт без привязки к категории
- Two-step flow: форма создания → привязка к сезону через
POST /:seasonId/achievements/add - Кнопки скрыты для завершённых сезонов (
COMPLETED)
Quiz Generation (Step 7)
Квизы генерируются автоматически из шаблонов + 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 7 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 вместо индивидуальных создаёт.
Activity Counters (глобальные счётчики)
Публичный виджет на экранах Кейсов и Рулеток, показывающий общее количество открытий/прокрутов за текущий сезон всеми пользователями.
Формулы:
- Кейсы:
SUM(casesOpened)по всемUserSeasonStatsтекущего сезона - Рулетки:
SUM(dailySpinsUsed)по всемUserSeasonStatsтекущего сезона ratePerHour = total / max(1, hoursElapsedSinceSeasonStart)— средняя скорость за сезон
Кеширование: Redis, key goloot:v1:season:{seasonId}:activity-counters, TTL 60s (в Repository layer).
Frontend (live counter):
- Polling каждые 60s (совпадает с Redis TTL)
useLiveCounterhook интерполирует между поллами: тикает +1 с интервалом3600s / ratePerHour- Optimistic +1 при открытии кейса/спина пользователем (через
window.dispatchEvent) Math.max(displayValue, serverValue)при синке — счётчик никогда не уменьшается- OdometerCounter: flip-clock стиль, 8 цифр с leading zeros (opacity-30),
translateYв em-юнитах
Protection
| Действие | Rate Limit | Auth | Validation |
|---|---|---|---|
| Получить текущий сезон | general (100/min) | Telegram | GetCurrentSeasonSchema |
| Получить статистику | general (100/min) | Telegram | GetSeasonStatsSchema |
| Получить leaderboard | general (100/min) | Telegram | GetLeaderboardSchema |
| Получить activity counters | general (100/min) | Нет (публичный) | GetActivityCountersSchema |
| Завершить сезон (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 10) 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 — проверка maintenance и доступности сезона выполняется параллельно (ШАГ 0) в LoadingOrchestrator.initialize() через Promise.all([checkMaintenanceStatus(), checkSeasonStatus()]), до загрузки Bootstrap.
Зачем: Без активного сезона пользователь не может взаимодействовать с приложением (все критичные endpoints защищены requireActiveSeason middleware). Early check предотвращает бесполезную загрузку Bootstrap (~8 запросов к БД), показывает SeasonCountdownOverlay сразу.
Как работает:
LoadingOrchestratorустанавливаетloadingState = 'checking_maintenance'и запускаетPromise.allдля параллельной проверки maintenance и сезона- Выполняется запрос к
/api/seasons/current(требуетtelegramAuth— заголовокX-Telegram-Init-Data) - Если
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 state (определён в типах, но в runtime устанавливаетсяchecking_maintenanceпередPromise.all)frontend/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 (ШАГ 0) блокирует Bootstrap → экономия ~8 DB запросов |
| Ошибка раздачи наград | Логирование, retry не реализован (ручное исправление) |
| Пользователь в топ-10 XP, рефереров и/или бустеров | Создаются отдельные SeasonRewardClaim для каждого типа (XP, REFERRAL, BOOST_LEADERBOARD) — все вознаграждения выдаются независимо |
| Admin scrap grant без season stats | Невозможно — addScrap использует interactive transaction с updateSeasonStats(). Все 10 scrap sources через addScrap() гарантированно обновляют USS; admin-грант проходит через updateSeasonStats() напрямую |
| Перезагрузка сервера | rescheduleAllOnBoot() восстанавливает таймеры SCHEDULED сезонов |
| Referral награды добавлены в уже активный сезон | ReferralRewardsSeasonModal показывается пользователю один раз (localStorage referral_rewards_seen_{seasonId}). Хук: useReferralRewardsNotification, компонент: ReferralRewardsSeasonModal.tsx |
| Шаг 6 пропущен через "Без наград" | Статус шага = WARNING в Stepper (жёлтый ⚠). Баннер в Step 6 и Step 10 (Approval) информирует что лидерборд работает без наград |
| setTimeout > 24.8 дней | Рекурсивное перепланирование через MAX_TIMEOUT_MS |
| Activity counter: нет активного сезона | SeasonCounterBanner возвращает null — баннер не рендерится |
| Activity counter: optimistic +1 обгоняет сервер | Math.max(displayValue, serverValue) — число никогда не уменьшается, сервер подтянет при следующем poll |
Admin Notifications
Уведомления для админов через Telegram при событиях сезона (топик SEASONS):
| Событие | Тип | Когда |
|---|---|---|
| Сезон стартовал | SEASON_STARTED | При активации (timer или ручной старт) |
| Сезон завершён | SEASON_ENDED | После автоматического/ручного завершения |
| Нужна настройка | SEASON_NEEDS_SETUP | Нет следующего SCHEDULED сезона после завершения |
| Скоро конец | SEASON_ENDING_SOON | За 7, 3, 1 день до endDate |
Все события содержат seasonId для прямых ссылок на админ-панель.
TELEGRAM_GROUP_ID=<group_id> # ID группы для уведомлений (используется TopicsService)
ADMIN_URL=https://admin.goloot.online # URL для ссылок в уведомлениях
При отсутствии TELEGRAM_GROUP_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).
Почему lifecycle job обходит Redis-кэш (defense in depth)?
Проблема: Cache poisoning инцидент 16.03.2026. RustQuestProgressService.isSeasonActive() записывал частичный объект (select: { id: true }) в общий кэш-ключ activeSeason. Lifecycle cron читал этот объект, endDate был undefined, now < undefined → false — сезон завершился на 2.5 месяца раньше.
Решение (3 уровня):
- Корень: Rust сервис переведён на
SeasonRepository.getActiveSeason()(полный объект) - Архитектура: Lifecycle job использует
getActiveSeasonDirect()— прямой DB-запрос, кэш полностью обходится. Стоимость: 1 запрос каждые 5 мин (ничтожно) - Safety net:
MIN_ACTIVE_DAYS_FOR_AUTO_TRANSITION = 7— cron автоматически блокирует transition для сезонов активных менее 7 дней. Админский force-end не затронут
Принцип: Необратимые операции (transition уничтожает данные) никогда не должны зависеть от кэша. Defense in depth: даже при неизвестном баге safety net предотвращает катастрофу.
Почему длительность сезона 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 — усложняет инфраструктуру
Последствия: Защита от случайных кликов, но медленнее для намеренного завершения.
Почему hard delete всех квестов при удалении из сезона?
Проблема: При удалении квестов через wizard (кнопка «Clear quests» или поштучное удаление) manual/imported квесты лишь отвязывались от сезона (SeasonQuest удалялся), но сам Quest оставался в БД. Это приводило к накоплению orphaned квестов в админ-реестре без возможности их очистки.
Решение: Hard delete для всех квестов при удалении из сезона. Порядок в транзакции:
UserQuestExclusion— нет CASCADE на FK, удалять вручнуюSeasonQuest— связь сезон↔квестQuest— CASCADE автоматически удаляетUserQuest,ClaimedUniqueQuestReward— нет CASCADE от Quest, удалять вручную
Альтернативы (отклонены):
- Soft delete (deletedAt) — квесты всё равно засоряют реестр, нужен отдельный cleanup
- Hard delete только auto-generated — manual квесты становятся orphaned
Исключение: При регенерации (generateQuests) удаляются только auto-generated квесты — manual квесты сохраняются, чтобы не потерять ручную работу админа.
Последствия: Чистый реестр квестов, но удаление необратимо (восстановление только через re-import).
Каскадный порядок удаления сезона
Проблема: При удалении сезона необходимо соблюдать порядок удаления связанных данных из-за FK constraints.
Решение: Строгий порядок в транзакции:
UserSeasonStats— статистика пользователейCraftBudgetLog— логи крафт-бюджета (FK на BudgetPeriod)BudgetPeriod— периоды бюджетаLuckPoolEntry— записи пула удачиSeasonRewardClaim— клеймы наград сезонаSeason— сам сезон
Критично: CraftBudgetLog должен удаляться до BudgetPeriod из-за craft_budget_logs_periodId_fkey.
Последствия: Надёжное каскадное удаление без FK violations.
Почему SeasonRewardClaim имеет поле type?
Проблема: Исходный unique constraint @@unique([userId, seasonId]) допускал только одну запись на пользователя за сезон. Если пользователь попадал в топ-10 по XP и в топ-10 рефереров — второй upsert (с пустым update: {}) молча проигнорировал бы создание второй записи, и реферальная награда была бы потеряна без каких-либо ошибок.
Решение: Добавить поле type RewardClaimType @default(XP) и изменить unique constraint на @@unique([userId, seasonId, type]):
type: "XP"— награда XP leaderboard (топ-10 по XP)type: "REFERRAL"— награда referral leaderboard (топ-10 рефереров)type: "BOOST_LEADERBOARD"— награда boost leaderboard (топ-10 бустеров)
Альтернативы (отклонены):
- Отдельная таблица
SeasonReferralRewardClaim— дублирование схемы без пользы - Хранить оба вознаграждения в одной записи (JSON) — усложняет идемпотентность
Последствия: Пользователь, попавший в несколько топ-10, получает отдельные SeasonRewardClaim для каждого типа. Каждый тип распределяется идемпотентно через upsert с update: {}.
Известное ограничение: частичный сбой при раздаче наград
Проблема: SeasonLifecycleJob использует идемпотентную проверку seasonRewardClaim.count > 0 перед запуском дистрибуции. Если XP-награды были распределены успешно, а referral-распределение завершилось с ошибкой — повторный запуск lifecycle job пропустит оба этапа (check count > 0 будет истинным из-за уже созданных XP-записей).
Текущий статус: Не исправляется в рамках данного изменения. Требует отдельного рефакторинга SeasonLifecycleJob с раздельными флагами идемпотентности для XP, REFERRAL и BOOST_LEADERBOARD распределений.
Последствия: При частичном сбое необходимо ручное вмешательство (удалить XP-записи из SeasonRewardClaim и перезапустить lifecycle job).
Почему scrapTotalEarned переименован в scrapEarned?
Проблема: scrapTotalEarned — некорректное имя в per-season контексте. «Total» подразумевает сумму за всё время, но поле хранит earned за конкретный сезон. Фронтенд для Season использовал scrap (баланс earned−spent), а для All Time — scrapTotalEarned (только earned). Несогласованность приводила к отображению отрицательных значений при тратах скрапа, полученного через admin grant.
Корень: 4 из 10 источников скрапа (admin, promo_code, raffle, consolation) не обновляли UserSeasonStats. Маппинг SCRAP_SOURCE_TO_SEASON был Partial<Record> — для отсутствующих источников updateSeasonScrap() не вызывался. Admin grant вообще обходил scrap.service.ts. В результате: трата скрапа (через spendScrap) декрементила USS.scrap, но доход не инкрементил → баланс уходил в минус.
Решение:
- Rename
scrapTotalEarned→scrapEarned(убрать confusing «Total») - Добавить 4 недостающих
scrapFrom*поля в схему - Маппинг
SCRAP_SOURCE_TO_SEASON→ полныйRecord(неPartial) - Admin
addScrap/removeScrap— interactive transaction сupdateSeasonStats() - Фронтенд: единое поле
scrapEarnedдля обоих периодов (Season и All Time)
Последствия: Все 10 scrap sources гарантированно обновляют UserSeasonStats. Отрицательные значения в статистике невозможны.
Почему Budget Control перенесён в Season Wizard?
Проблема: Budget Control жил как отдельная страница /budget, показывая данные активного сезона без идентификации какому сезону они принадлежат. Бюджетные параметры (333₽/день, 10-дневные периоды, 9 периодов) были глобальными константами — одинаковыми для всех сезонов.
Решение: Перенести Budget Control в Season Setup Wizard как Step 2. Шаг адаптируется по статусу:
- DRAFT/SCHEDULED — конфигурация per-season параметров
- ACTIVE — readonly config + операционный Budget Control
- COMPLETED — всё readonly
Альтернативы (отклонены):
- Оставить отдельной страницей с dropdown сезона — теряется контекст wizard flow
- Дублировать на обеих страницах — DRY violation
Последствия: Бюджет конфигурируется в контексте конкретного сезона. Standalone /budget страница и sidebar entry полностью удалены.
Почему activity-counters endpoint без авторизации?
Проблема: Глобальные счётчики (всего кейсов открыто, спинов использовано) не привязаны к конкретному пользователю. Добавление Telegram auth увеличивало бы нагрузку на валидацию без пользы.
Решение: GET /api/seasons/activity-counters — публичный endpoint, без telegramAuth middleware. Защита: rateLimitConfigs.general (100/min).
Альтернативы (отклонены):
- Telegram auth как у остальных season endpoints — избыточно для агрегатных данных
- CDN caching — усложнение инфраструктуры, Redis TTL 60s достаточен
Последствия: Минимальная нагрузка, простое кеширование. Единственный публичный endpoint в season routes.
Почему OdometerCounter, а не AnimatedValue?
Проблема: В проекте есть AnimatedValue.tsx + useCountUp.ts — count-up анимация (плавный счёт от A до B одним числом) с shimmer/shrink эффектами и звуком. Нужен другой визуальный эффект для фонового счётчика.
Решение: Отдельный компонент OdometerCounter — каждая цифра анимируется независимо через CSS translateY (как счётчик пробега). Нет shimmer/звука — тихий, фоновый элемент. Flip-clock стиль: каждая цифра в тёмной rounded-ячейке.
Альтернативы (отклонены):
- Переиспользование
useCountUpс кастомным рендером — разная визуальная механика, костыльная адаптация - CSS-only анимация (
@keyframes) — нет контроля над per-digit анимацией
Последствия: Два компонента для двух разных паттернов. AnimatedValue — для наград (яркий, со звуком). OdometerCounter — для фоновых счётчиков (тихий, постоянный).
4. Architecture
Services Overview
Cron Jobs
| Job | Schedule | Описание |
|---|---|---|
| Season Lifecycle | */5 * * * * (каждые 5 мин) | Проверка окончания ACTIVE сезона, 9-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 по XP (season.rewards) + топ-10 рефереров (season.referralRewards) + топ-10 бустеров (season.boostLeaderboardRewards) |
| 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 | user-seasons.routes.ts | /api/seasons/* |
| Admin Routes | admin-seasons.routes.ts | /admin/seasons/* |
| Admin Content Budget Routes | admin-content-budget.routes.ts | /admin/content-budget/* |
| 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 | Клонирование контента между сезонами (включая boostLeaderboardRewards) |
| 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 | Валидация достижимости контента перед стартом. Сообщения review-валидации показывают прогресс: «проверено N из M» |
| ExhaustionMonitorService | exhaustion-monitor.service.ts | Мониторинг исчерпания контента и автосброс пула |
| Quest Blueprints | quest-blueprints.ts | 38 шаблонов генерации квестов (3 CASES, 3 QUIZ, 8 RECYCLE, 10 COLLECTION, 2 SOCIAL, 3 SPECIAL, 3 PROGRESSION, 6 ECONOMY) |
| AchievementGeneratorService | achievement-generator.service.ts | Smart Achievement Generation (5-phase, blueprint-based) |
| Achievement Blueprints | achievement-blueprints.ts | 18 шаблонов генерации достижений (4 QUIZ, 3 CASES, 2 COLLECTION, 2 RECYCLE, 1 SOCIAL, 1 STREAK, 2 ECONOMY, 2 PROGRESSION, 1 SPECIAL) |
| Admin Setup Routes | admin-season-setup.routes.ts | /admin/season-setup/* (wizard) |
| OdometerCounter | OdometerCounter.tsx | Flip-clock цифровой счётчик с per-digit CSS анимацией |
| SeasonCounterBanner | SeasonCounterBanner.tsx | Баннер глобальной активности на CasesScreen/DailySpinScreen |
| useLiveCounter | useLiveCounter.ts | Клиентская интерполяция: ticking + optimistic increment |
| useSeasonCounters | useSeasonCounters.ts | React Query hook для activity-counters с 60s polling |
| ValidationBanner | ValidationBanner.tsx | Persistent баннер валидации на каждом шаге визарда |
| ReferralRewardsStep | ReferralRewardsStep.tsx | Step 6 визарда: настройка наград реферального лидерборда (top1/top3/top10); переиспользует SeasonRewardsForm |
| BudgetStep | BudgetStep.tsx | Step 2 визарда: конфигурация бюджета (DRAFT) / операционный Budget Control (ACTIVE) |
5. Database Schema
Models
| Модель | Описание | Ключевые поля |
|---|---|---|
| Season | Сезон | number, name, startDate, endDate, status, scheduledAt, actualStartAt, rewards, referralRewards, boostLeaderboardRewards, finalResults, budgetPeriodsCount, budgetTotalRub, budgetCarryoverEnabled |
| UserSeasonStats | Статистика за сезон | userId, seasonId, xp, scrap, level, rank, breakdown полей |
| SeasonRewardClaim | Полученные награды | userId, seasonId, type ("XP"|"REFERRAL"|"BOOST_LEADERBOARD"), position, rewardSnapshot; @@unique([userId, seasonId, type]) |
| SeasonContentBudget | Бюджет контента сезона (квизы) | seasonId, quizTotalBudget, quizEasy/Medium/HardBudget, quizEasy/Medium/HardUsed, status |
| SeasonAchievement | Связь достижения с сезоном | seasonId, achievementId, isAutoGenerated, rewardStatus |
| SeasonCase | Связь сезона с кейсом | seasonId, caseId, sortOrder, isActive |
| SeasonSpin | Связь сезона с рулеткой | seasonId, spinId, sortOrder, isActive |
| SeasonQuest | Связь сезона с квестом | seasonId, questId, generatedFrom |
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 уведомления админам
MIN_ACTIVE_DAYS_FOR_AUTO_TRANSITION: 7, // Safety net: минимум дней для авто-завершения
} as const;
UserSeasonStats — полная структура полей
Балансы:
scrap,scrapEarned,xp,level
Scrap breakdown:
scrapFromQuizzes,scrapFromTasks,scrapFromAchievementsscrapFromCases,scrapFromSpins,scrapFromReferralsscrapFromAdmin,scrapFromPromoCodes,scrapFromRaffle,scrapFromConsolation,scrapFromBoosts
Scrap расходы:
scrapSpent,scrapSpentOnCases,scrapSpentOnCraft,scrapSpentOnSpins
XP breakdown:
xpFromTasks,xpFromAchievementsxpFromReferrals,xpFromCases,xpFromSpinsxpFromSalvage,xpFromAdminxpFromRaffle,xpFromPromoCodes,xpFromBoosts
Quiz stats:
quizzesCompleted,correctAnswers,incorrectAnswers
Streaks:
bestDailyLoginStreak
Streak Points:
streakPointsEarned,streakPointsSpent
Activity:
casesOpened,dailyCasesOpened,itemsCrafteditemsSalvaged,dailySpinsUsed,tasksCompletedachievementsUnlocked,friendsInvited
Rank:
rank— текущая позицияrankUpdAt— когда обновлялось
6. API Endpoints
- User API
- Admin: CRUD
- Admin: Lifecycle
- Admin: Setup Wizard
- Admin: Content Budget
- Admin: Rewards
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /api/seasons/current | Текущий сезон + displayStatus (требует Telegram auth) | → |
| GET | /api/seasons/stats | Статистика пользователя за сезон | → |
| GET | /api/seasons/leaderboard | Топ игроков + позиция пользователя | → |
| GET | /api/seasons/activity-counters | Глобальные счётчики активности (без auth) | — |
| Метод | Эндпоинт | Описание | 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) | → |
Quick Create & Status
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /admin/season-setup/suggested-start-date | Рекомендуемая дата старта нового сезона |
| POST | /admin/season-setup/quick-create | Быстрое создание сезона (автоматический setup) |
| GET | /admin/season-setup/:seasonId/status | Статус настройки сезона |
| GET | /admin/season-setup/:seasonId/current-step | Текущий шаг настройки |
Step 1 — Basic Info
| Метод | Эндпоинт | Описание |
|---|---|---|
| PUT | /admin/season-setup/:seasonId/basic-info | Обновить название, даты |
Step 2 — Budget Config
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /admin/season-setup/:seasonId/budget-config | Получить конфигурацию бюджета (+ computed preview) |
| PUT | /admin/season-setup/:seasonId/budget-config | Обновить конфигурацию бюджета (periodsCount, totalBudgetRub, carryoverEnabled) |
Step 3 — Rewards
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /admin/season-setup/:seasonId/rewards | Получить награды сезона (XP leaderboard) |
| PUT | /admin/season-setup/:seasonId/rewards | Настроить награды (top1/3/10) |
| POST | /admin/season-setup/:seasonId/rewards/copy-from-last | Скопировать награды из предыдущего сезона |
Step 6 — Referral Rewards
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /admin/season-setup/:seasonId/referral-rewards | Получить награды реферального лидерборда |
| PUT | /admin/season-setup/:seasonId/referral-rewards | Настроить награды рефереров (top1/3/10) |
| POST | /admin/season-setup/:seasonId/referral-rewards/copy-from-last | Скопировать referral rewards из предыдущего сезона |
| DELETE | /admin/season-setup/:seasonId/referral-rewards | Убрать награды рефлидерборда (шаг 6 → статус WARNING) |
Boost Leaderboard Rewards (внутри BoostPassStep):
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /admin/season-setup/:seasonId/boost-leaderboard-rewards | Получить награды лидерборда бустеров |
| PUT | /admin/season-setup/:seasonId/boost-leaderboard-rewards | Настроить награды бустеров (top1/3/10) |
| POST | /admin/season-setup/:seasonId/boost-leaderboard-rewards/copy-from-last | Скопировать из предыдущего сезона |
| DELETE | /admin/season-setup/:seasonId/boost-leaderboard-rewards | Убрать награды лидерборда бустеров |
Step 4 — Cases
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /admin/season-setup/cases/available | Доступные кейсы для добавления |
| GET | /admin/season-setup/:seasonId/cases | Кейсы текущего сезона |
| POST | /admin/season-setup/:seasonId/cases | Добавить кейсы в сезон |
| GET | /admin/season-setup/:seasonId/cases/:caseId/cascade-preview | Превью каскадного удаления (связанные квесты) |
| DELETE | /admin/season-setup/:seasonId/cases/:caseId | Удалить кейс из сезона |
Step 5 — Spins
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /admin/season-setup/spins/available | Доступные рулетки для добавления |
| GET | /admin/season-setup/:seasonId/spins | Рулетки текущего сезона |
| POST | /admin/season-setup/:seasonId/spins | Добавить рулетки в сезон |
| DELETE | /admin/season-setup/:seasonId/spins/:spinId | Удалить рулетку из сезона |
Step 7 — Quizzes
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /admin/season-setup/:seasonId/quizzes | Квизы сезона |
| GET | /admin/season-setup/:seasonId/quizzes/stats | Статистика квизов сезона |
| GET | /admin/season-setup/quizzes/categories | Доступные категории для генерации |
| POST | /admin/season-setup/:seasonId/quizzes/generate | Массовая генерация квизов (mode: regenerate/append) |
| DELETE | /admin/season-setup/:seasonId/quizzes/clear | Очистить все квизы сезона |
| DELETE | /admin/season-setup/:seasonId/quizzes/by-category | Удалить квизы по категории |
| POST | /admin/season-setup/:seasonId/quizzes/generate-one | Сгенерировать один квиз |
| GET | /admin/season-setup/quizzes/items?category&subcategory? | Предметы категории с количеством шаблонов |
| POST | /admin/season-setup/:seasonId/quizzes/generate-targeted | Точечная генерация квизов по конкретным предметам |
| POST | /admin/season-setup/:seasonId/quizzes/preview-targeted-deletion | Превью точечного удаления квизов |
| DELETE | /admin/season-setup/:seasonId/quizzes/delete-targeted | Точечное удаление квизов по фильтрам |
| DELETE | /admin/season-setup/:seasonId/quizzes/:quizId | Удалить конкретный квиз |
| POST | /admin/season-setup/:seasonId/quizzes/:quizId/regenerate | Перегенерировать конкретный квиз |
Step 8 — Quests
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /admin/season-setup/:seasonId/quests | Квесты сезона |
| POST | /admin/season-setup/:seasonId/quests/generate | Smart Quest Generation |
| POST | /admin/season-setup/:seasonId/quests | Добавить квест в сезон вручную |
| DELETE | /admin/season-setup/:seasonId/quests/clear | Очистить все квесты сезона |
| DELETE | /admin/season-setup/:seasonId/quests/:questId | Удалить квест из сезона |
| PUT | /admin/season-setup/:seasonId/quests/:questId/reward | Обновить награду квеста |
Navigation, Validation & Approval
| Метод | Эндпоинт | Описание |
|---|---|---|
| POST | /admin/season-setup/:seasonId/complete-step | Завершить шаг настройки (step в body) |
| GET | /admin/season-setup/:seasonId/validate | Валидация сезона перед утверждением |
| POST | /admin/season-setup/:seasonId/approve | Утвердить сезон |
| POST | /admin/season-setup/:seasonId/clone-from | Клонировать контент из другого сезона |
Review & Rollback
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /admin/season-setup/:seasonId/review | Получить review state сезона |
| PATCH | /admin/season-setup/:seasonId/review | Обновить review state |
| POST | /admin/season-setup/:seasonId/rollback | Откатить к предыдущему шагу настройки |
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /admin/content-budget/allocation/preview | Превью аллокации бюджета без создания |
| POST | /admin/content-budget/:seasonId/budget | Создать бюджет контента сезона |
| GET | /admin/content-budget/:seasonId/budget | Статус бюджета сезона |
| POST | /admin/content-budget/:seasonId/achievements/generate | Сгенерировать достижения для сезона |
| POST | /admin/content-budget/:seasonId/achievements/add | Добавить существующее достижение к сезону |
| GET | /admin/content-budget/:seasonId/achievements | Достижения сезона |
| DELETE | /admin/content-budget/:seasonId/achievements/clear | Очистить все достижения сезона |
| DELETE | /admin/content-budget/:seasonId/achievements/:achievementId | Удалить достижение из сезона |
| PUT | /admin/achievements/:achievementId/reward | Обновить награду достижения |
| GET | /admin/content-budget/:seasonId/placeholder-count | Количество placeholder наград |
| GET | /admin/content-budget/:seasonId/validate | Валидация перед стартом |
| POST | /admin/content-budget/:seasonId/approve | Утвердить бюджет для старта |
| GET | /admin/content-budget/:seasonId/exhaustion | Статус исчерпания бюджета |
| GET | /admin/content-budget/:seasonId/exhaustion/stats | Историческая статистика исчерпания |
| Метод | Эндпоинт | Описание | 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 7 Setup Wizard)
- Quests — источник XP за выполнение заданий
- Achievements — источник XP за разблокировку
- Streaks — влияет на XP multiplier
- Inventory — SEASON_REWARD items хранятся отдельно