Quizzes
1. Summary
Goal: Повысить вовлечённость чтения Telegram канала через автоматическую публикацию квизов сразу после выхода поста. Квиз живёт 24 часа, затем удаляется — создаёт срочность и мотивирует включить уведомления.
User Value: Возможность заработать Scrap за правильные ответы в комментариях к посту. Ограниченное время создаёт FOMO и причину следить за каналом.
Сейчас: только Scrap. Планируется: XP, SP (не реализовано в коде).
2. Business Logic
Question Types
- SINGLE
- MULTIPLE
Описание: Один правильный ответ из нескольких вариантов.
Требования:
- Ровно 1 правильный ответ (
isCorrect: true) - Минимум 3 неправильных ответа
correctAnswer— строка, совпадающая с текстом правильного варианта
Пример: "Сколько HP у металлической двери?" → "250 HP"
Описание: Несколько правильных ответов. Пользователь не знает, сколько ответов верно — нужно выбрать все правильные.
Требования:
- Минимум 2 правильных ответа (
isCorrect: true) - Минимум 1 неправильный ответ
correctAnswer— массив строк, все должны совпадать с текстами правильных вариантов
Примеры:
- 2/2 split: "Какие ресурсы нужны для Furnace?" → ["Stone", "Wood"] + 2 неверных
- 3/1 split: "Из чего крафтится Metal Door?" → ["Metal Fragments", "Gears", "Wood"] + 1 неверный
Категоризация
Иерархия: Category → Subcategory → Quiz
📁 Category: "Rust"
├── 📂 Subcategory: "HP строений"
│ ├── Quiz: "Сколько HP у металлической двери?"
│ └── Quiz: "Сколько HP у каменной стены?"
└── 📂 Subcategory: "Крафт"
└── Quiz: "Сколько металла нужно для двери?"
Квиз не может быть создан без привязки к подкатегории. Это обеспечивает структурированную организацию контента.
Core Mechanics
Validation Rules:
| Поле | Ограничения |
|---|---|
question | 5-500 символов |
explanation | 10-1000 символов |
options[].text | 1-200 символов |
options | JSON schema: minItems: 4 для всех типов; бизнес-правила: SINGLE — 1 верный + 3+ неверных; MULTIPLE — 2+ верных + 1+ неверный |
timeLimit | 10-600 секунд (опционально) |
rewardScrap | 0-10000 (по умолчанию 100) |
source | валидный HTTP/HTTPS URL |
imageUrl | HTTP/HTTPS URL или путь /images/quizzes/* |
Каскадная активация:
При изменении статуса isActive происходит каскадное обновление:
Category (isActive: false)
↳ Subcategory (isActive: false)
↳ Quiz (isActive: false)
При деактивации категории автоматически деактивируются все её подкатегории и квизы.
Auto-Generation (Season Quizzes)
Квизы для сезона генерируются автоматически через QuizGeneratorService — логика портирована из внешнего Node.js скрипта в TypeScript сервис. Поддерживаются два подхода: массовая генерация по категориям и точечная генерация по конкретным предметам.
Data Pipeline:
quizzes/ repo (отдельный git) backend/ (монорепо)
┌─────────────────────────┐ ┌──────────────────────────┐
│ JSON (crawl/parse) │ │ QuizGeneratorService │
│ ↓ │ │ ↓ │
│ migrate.js │ │ SELECT ... FROM items │
│ ├── schema.sql │ ──copy──▶ │ WHERE quiz_eligible=1 │
│ ├── items INSERT │ │ ↓ │
│ ├── QuizEligibility │ │ quiz-templates.json │
│ │ Enricher │ │ ↓ │
│ └── rust_data.db │ │ Generated quizzes → DB │
└─────────────────────────┘ └──────────────────────────┘
Источники данных:
- SQLite (
backend/data/rust_data.db, ~28MB) — read-only база с предметами Rust (HP, крафт, рейды) - Templates (
quiz-templates.json) — шаблоны вопросов по категориям - Eligibility config (
quiz-eligibility.json) — правила исключения NPC/admin/event предметов
Quiz Eligibility Filtering:
Поле quiz_eligible в таблице items определяет, можно ли генерировать квизы по предмету. Конфиг quizzes/config/quiz-eligibility.json задаёт правила исключения:
| Правило | Тип матчинга | Примеры |
|---|---|---|
| NPC outfits | exact shortname | scientistsuit_heavy, attire.banditguard |
| Frankenstein NPC parts | shortname LIKE | %franken% |
| Admin-only weapons | exact shortname | rocket.launcher.rpg7 |
| Items in development | name LIKE | %РАЗРАБОТКЕ% |
| Unobtainable attire | exact shortname | jumpsuit.suit, hat.gas.mask |
| Promo/streaming items | exact shortname | lumberjack.hoodie, twitchrivalsflag |
| Seasonal event items | exact shortname | gloweyes, gingerbreadsuit |
Enrichment выполняется при каждом migrate.js --force — поле вычисляется из конфига, не хранится в исходных JSON.
Для исключения предмета — добавить shortname в quizzes/config/quiz-eligibility.json и перегенерировать базу (node src/db/migrate.js --force). Код генератора менять не нужно.
Категории:
| Категория | SINGLE | MULTIPLE |
|---|---|---|
weapons | 38 | 7 |
attire | 31 | 8 |
food | 24 | 16 |
construction | 20 | 6 |
items | 21 | 5 |
electrical | 18 | 6 |
tools | 18 | 6 |
components | 18 | 7 |
loot_containers | 18 | 9 |
fun | 17 | 7 |
ammo | 16 | 6 |
resources | 13 | 7 |
misc | 13 | 5 |
traps | 12 | 6 |
scientists | 10 | 5 |
medical | 9 | 2 |
animals | 9 | 7 |
vehicles | 9 | 7 |
missions | 5 | 3 |
vendors | 5 | 3 |
raid | 3 | 1 |
Стратегии генерации неправильных ответов (SINGLE):
| Стратегия | Описание |
|---|---|
close_values | Рядом с правильным: offset_percentage (±%), offset_fixed (±N), offset_small |
workbench_levels | Уровни верстаков (0-3) |
text_options | Выбор из текстового пула (контейнеры, локации) |
range_values | Диапазон значений в строковом формате |
range_offset | Смещение диапазонов (для числовых интервалов) |
item_text_options | Выбор из текстовых значений других предметов категории |
MULTIPLE grouping стратегии:
| Стратегия | Описание |
|---|---|
workbench_level | По уровню верстака крафта |
craft_ingredient | По общему ингредиенту крафта |
has_field | По наличию поля (значение > 0 или непустая строка) |
threshold | По числовому порогу (field >= value) |
loot_only | Предметы без крафта (с опциональным whitelist) |
raid_quantity | По количеству рейд-инструмента |
vendor_offer_threshold | Per-vendor квиз: товары дороже N металлолома у конкретного вендора |
loot_chance_threshold | Per-entity квиз: предметы с шансом выпадения выше/ниже порога из loot-таблицы |
item_locations | Per-item квиз: локации, в которых можно получить предмет (из obtained_from_all) |
raid_cheapest | Цели, для которых один рейд-инструмент дешевле другого по sulfur_cost |
raid_sulfur_threshold | Цели, рейдуемые данным инструментом за ≤ N серы |
shop_location_match | Предметы, доступные для покупки в конкретной локации (из shop_locations_all) |
Расчёт сложности: Задаётся в шаблоне (difficulty: easy|medium|hard). Награда: EASY=50, MEDIUM=100, HARD=150 scrap.
Cross-Season Deduplication
Дедупликация квизов основана на контентных fingerprints, а не на ID или тексте вопроса. Это гарантирует, что один и тот же вопрос (например, "сколько патронов в AK-47?") никогда не повторится между сезонами.
Fingerprint Formula (backend/src/domains/quizzes/utils/fingerprint.util.ts):
| Тип | Формула | Пример |
|---|---|---|
| SINGLE | sha256(category:questionFieldId:slug) | sha256("weapons:magazine_capacity:rifle.ak") |
| MULTIPLE | sha256(category:templateId:sortedCorrectItems) | sha256("food:high_calories:apple,cooked_pork,granola") |
Ключевые свойства:
- Детерминистичность — одна и та же комбинация предмет + шаблон всегда даёт одинаковый fingerprint
- Order-independence — для MULTIPLE массив correct items сортируется перед хешированием
- Collision resistance — SHA256, вероятность коллизии при 100k квизах < 10⁻⁶⁰
Алгоритм при генерации сезона (SeasonQuizService):
1. Собрать contentFingerprint ВСЕХ квизов из ВСЕХ сезонов (включая архивные)
→ SELECT contentFingerprint FROM Quiz (без фильтра по isActive)
2. Создать Set<string> из собранных fingerprints
3. Сгенерировать пул кандидатов (SINGLE + MULTIPLE)
4. Отфильтровать: pool.filter(q => !fpSet.has(q.contentFingerprint))
5. Записать только уникальные квизы в БД
Два шаблона про один предмет ("Сколько урона у AK-47?" и "Какой магазин у AK-47?") имеют разные fingerprints, потому что questionFieldId отличается (base_damage vs magazine_capacity). А один шаблон про один предмет в разных сезонах — одинаковый fingerprint, дубль отсечён.
Fingerprints хранятся в БД вместе с квизами. Если квизы физически удалить (purge), fingerprints пропадут и вопросы смогут сгенерироваться повторно. Архивные квизы сохраняются именно для поддержания дедупликации.
Targeted Generation (Item-Level)
Точечная генерация позволяет создавать квизы для конкретных предметов (по shortname) вместо целых категорий. Используется для добавления квизов под конкретные slug-targeted достижения.
Flow (Admin UI — collapsible-секция в Step 5 Wizard):
- Выбор категории → загрузка предметов с подсчётом доступных шаблонов на каждый предмет
- Опциональная фильтрация по подкатегории / поиск по имени
- Выбор предметов (чекбоксы) → отображение максимального количества шаблонов
- Генерация → всегда append-режим (без удаления существующих)
Input: { category, shortnames[], count? }
Алгоритм:
- Загрузить предметы из SQLite по shortnames
- Для каждого предмета проверить все шаблоны категории (SINGLE + MULTIPLE)
- Исключить дубли по
contentFingerprint(cross-season, все существующие квизы) - Записать в БД + инкрементировать
SeasonContentBudget
Result: { generatedCount, byItem: Record<slug, count>, byDifficulty, errors[], skipped[] }
Skip Diagnostics:
При targeted-генерации для каждого пропущенного шаблона собирается детальная диагностика — почему квиз не удалось сгенерировать. В UI это блок "N шаблонов пропущено" с раскрываемыми деталями.
| Поле | Описание | Пример |
|---|---|---|
reason | Причина пропуска с контекстом данных | "Недостаточно несовместимых оружий (compatible: 24, incompatible: 2)" |
detail.sourceValue | Состояние данных при отказе | "compatible: 24, incompatible: 2" |
detail.correctAnswer | Правильный ответ (если удалось определить) | "Штурмовая винтовка" |
detail.wrongAnswers | Неправильные варианты (если частично сгенерированы) | ["Дробовик", "Пистолет"] |
detail.strategy | Стратегия генерации неправильных ответов | "close_values" |
Skip diagnostics доступна только при точечной генерации (generateForItems). При массовой генерации для сезона (generateForSeason) пропущенные шаблоны не трекаются — система просто генерирует всё, что может.
Quiz Availability Check
Endpoint для проверки наличия квизов по slug в активном сезоне. Используется в Achievement Editor (ConditionsEditor) для отображения предупреждения, когда slug-targeted достижение (категория QUIZ) ссылается на предмет, для которого недостаточно квизов.
UI: Жёлтый warning badge в ConditionsEditor при available < targetProgress, зелёный check при достаточном количестве.
Формат шаблона (quiz-templates.json)
{
"id": "base_damage",
"source_field": "base_damage",
"answer_type": "direct_fact",
"condition": "item.base_damage !== null && item.base_damage > 0",
"templates": [
"Сколько урона наносит '{item_name}'?",
"Какой базовый урон у '{item_name}'?"
],
"wrong_answer_strategy": "close_values",
"wrong_answer_config": {
"method": "offset_percentage",
"percentage": 20,
"count": 3
},
"difficulty": "easy",
"tags": ["weapon", "damage", "stats"],
"answer_suffix": "HP"
}
id— уникальный идентификатор шаблонаsource_field— поле из SQLite записи предмета (колонка в fetchItems)answer_type— тип ответа (direct_fact,modifier_percent,direct_fact_time,direct_fact_range)condition— строковое выражение для фильтрации подходящих предметовtemplates— массив вариантов текста вопроса (случайный выбор при генерации)wrong_answer_strategy— стратегия генерации неправильных ответовwrong_answer_config— параметры стратегии (method, offsets/percentage, count)difficulty— сложность:"easy","medium","hard"tags— теги для группировки и фильтрацииanswer_suffix— суффикс ответа (опционально):"HP","%","сек","ккал"
Protection
| Действие | Rate Limit | Auth | Validation |
|---|---|---|---|
| CRUD квизов | adminApiLimiter (50/min) | Admin JWT | CreateQuizSchema, UpdateQuizSchema |
| Get stats | adminApiLimiter (50/min) | Admin JWT | GetQuizStatsSchema |
См. Security Matrix для полного обзора защит.
Edge Cases
| Ситуация | UI/Backend поведение |
|---|---|
| ❌ Некорректный questionType | QuizValidationError с указанием поля |
| ❌ correctAnswer не совпадает с options | QuizValidationError: "correctAnswer must match the text of the correct option" |
| ❌ Недостаточно вариантов (SINGLE) | QuizValidationError: "Minimum 4 options required" |
| ❌ Недостаточно вариантов (MULTIPLE) | QuizValidationError: "MULTIPLE type requires at least 2 correct answers" / "at least 1 incorrect answer" |
| ❌ Категория не найдена | HTTP 400: "Category not found" |
| ❌ Подкатегория не указана | QuizValidationError: "Подкатегория обязательна для создания квиза" |
| 🔄 Удаление категории с квизами | HTTP 400: "Невозможно удалить категорию: есть связанные квизы" |
| 📊 Запрос статистики | Кеш 5 минут, fallback на пересчёт |
| ✅ Review checkbox | localStorage-чекбокс в Season Wizard Step 5. Проверенные квизы: opacity-60, таб-бейдж {reviewed}/{total}. Состояние через useSeasonReview хук |
| ✏️ Save из EditQuizModal | НЕ ставит review автоматически — редактирование !== ревью |
3. ADR (Architectural Decisions)
Почему два типа вопросов (SINGLE/MULTIPLE)?
Проблема: Нужна гибкость для разных типов контента — простые факты (один ответ) и комплексные знания (несколько ответов).
Решение: Enum QuizQuestionType с жёсткой валидацией соответствия correctAnswer типу вопроса.
Альтернативы (отклонены):
- Только SINGLE — ограничивает типы вопросов
- Свободный ввод — сложная валидация, плохой UX
Последствия: Код валидации дублируется для каждого типа, но обеспечивает строгую консистентность данных.
Почему обязательная подкатегория?
Проблема: Без структуры сложно фильтровать и организовывать большое количество квизов.
Решение: Subcategory обязательна. При генерации квизов подкатегория назначается автоматически на основе данных предмета.
Альтернативы (отклонены):
- Теги вместо иерархии — сложнее управлять, нет чёткой структуры
- Опциональная подкатегория — приведёт к "свалке" квизов без категоризации
Последствия: Больше работы при создании контента, но лучшая организация.
Почему template-based генерация вместо ручного создания?
Проблема: Каждый сезон требует ~270 квизов (90 дней × 3 сложности). Ручное создание — это 270 × 5 мин = 22.5 часов работы контент-менеджера.
Решение: QuizGeneratorService генерирует квизы из шаблонов + SQLite базы данных Rust. Админ выбирает категории и количество, генерация занимает ~2 секунды.
Альтернативы (отклонены):
- AI генерация (GPT) — непредсказуемое качество, стоимость API, latency
- Переиспользование квизов между сезонами — пользователи запомнят ответы
- Внешний Node.js скрипт (
child_process.exec) — хрупкий, сложно отлаживать, нет per-season конфигурации
Последствия: Быстрая генерация свежего контента, но ограничена данными в SQLite и набором шаблонов.
Почему review checkboxes в localStorage, а не в БД?
Проблема: При генерации контента в Season Wizard администратор проверяет квизы (Step 5), квесты (Step 6) и достижения (Step 7) вручную. Нужен механизм отслеживания прогресса проверки.
Решение: Хук useSeasonReview хранит состояние проверки в localStorage (admin-season-review-{seasonId}). Покрывает все 3 типа контента: quizzes, quests, achievements. Approval Step (Step 8) показывает сводку проверки как UI-предупреждение (не блокирует одобрение).
Предыдущее решение (отклонено): Поле isReviewed Boolean в модели Quiz + endpoint PATCH /admin/quizzes/:id/toggle-reviewed. Работало только для квизов, требовало API и миграций для каждого нового типа контента.
Почему localStorage:
- Один администратор — мультиустройственная синхронизация не нужна
- Review — это workflow-инструмент, не бизнес-данные (не влияет на публикацию)
- Потеря при очистке кэша допустима (просто перепроверить)
- Единый механизм для всех типов контента без backend-изменений
Последствия: Упрощённая архитектура — нет эндпоинтов, миграций, серверной логики. Масштабируется на новые типы контента без backend-изменений.
Почему кеширование статистики на 5 минут?
Статистика квизов (totalAttempts, avgScore, recentActivity) кешируется в памяти на 5 минут для снижения нагрузки на БД.
Проблема: Агрегация из QuizResult для каждого запроса — дорогая операция.
Решение: In-memory кеш с TTL 5 минут, автоочистка просроченных записей.
Альтернативы (отклонены):
- Redis кеш — overkill для админских запросов
- Материализованные вью — сложнее поддерживать
Последствия: Статистика может отставать до 5 минут, что допустимо для админки.
Почему копируем contentFingerprint при клонировании сезона?
При копировании контента сезона квизы клонируются с сохранением contentFingerprint для поддержания дедупликации.
Проблема: Через несколько сезонов игроки забывают вопросы. Нужен способ переиспользовать старый контент без создания дубликатов.
Решение: При клонировании сезона (SeasonCloneService.cloneFromSeason) поле contentFingerprint копируется из source квизов.
Альтернативы (отклонены):
-
Автоматический "сброс" fingerprints через N сезонов — отклонено потому что:
- Переиспользование контента должно быть явным решением администратора, а не автоматическим
- Риск случайных дубликатов при генерации нового контента
- Сложность в определении "правильного" значения N (6 сезонов? 12?)
- Нет гарантии что игроки действительно забыли вопросы
-
Сброс fingerprint в NULL при копировании — отклонено потому что:
- Нарушает дедупликацию: система не сможет определить что квиз уже существует
- При следующей генерации может создаться точная копия того же вопроса
- Нарушается инвариант "один контент = один fingerprint"
-
Ручное удаление старых квизов перед генерацией — отклонено потому что:
- Потеря аналитики и истории (totalAttempts, avgScore)
- Ломаются связи с QuizResult
- Необходимость cascade delete
Почему текущее решение лучше:
- ✅ Явный контроль: Админ выбирает КОГДА переиспользовать старый контент через клонирование
- ✅ Защита от дубликатов: Если после клонирования нужно добавить новые квизы — система не создаст копии существующих
- ✅ Гибкость: Клонирование сезона (с fingerprint) — для полного переиспользования контента
- ✅ Простота: "Clone = exact copy with full metadata" — предсказуемое поведение
- ✅ Консистентность: Дедупликация работает одинаково для сгенерированных и клонированных квизов
Последствия:
- Админ явно управляет жизненным циклом контента
- Cross-season дедупликация работает корректно для всех типов квизов
- Нет риска случайных дубликатов при миксе клонирования + генерации
- Аналитика и история сохраняются в исходных записях
Workflow для переиспользования контента через N сезонов:
1. Сезон 1 (2024-01) → 270 квизов сгенерированы → каждый получил contentFingerprint
2. Сезон 2-5 (2024-02 до 2024-06) → новые квизы, дедупликация работает
3. Сезон 6 (2024-07) → Админ клонирует Сезон 1 → квизы копируются с fingerprints
4. Система видит эти fingerprints как "existing content" → не создаёт дубликаты при генерации
5. Игроки получают знакомые вопросы, но уже не помнят ответы (прошло 6 месяцев)
4. Architecture
Services Overview
Key Components
| Компонент | Путь | Описание |
|---|---|---|
| ContentFingerprint | backend/src/domains/quizzes/utils/fingerprint.util.ts | SHA256 fingerprint для cross-season дедупликации |
| QuizGeneratorService | backend/src/domains/quizzes/services/quiz-generator.service.ts | SINGLE quiz generation из SQLite + templates |
| QuizGeneratorMultipleService | backend/src/domains/quizzes/services/quiz-generator-multiple.service.ts | MULTIPLE quiz generation (grouping strategies) |
| QuizSqlBuilders | backend/src/domains/quizzes/services/quiz-sql-builders.ts | SQL builder-функции для SQLite subquery фрагментов |
| SqliteService | backend/src/common/sqlite/sqlite.service.ts | Read-only доступ к rust_data.db |
| QuizService | backend/src/domains/quizzes/services/quiz.service.ts | Валидация и CRUD квизов |
| QuizRepository | backend/src/domains/quizzes/repositories/quiz.repository.ts | Data access layer для квизов (Prisma) |
| CategoryService | backend/src/domains/quizzes/services/category.service.ts | Управление категориями с каскадной активацией |
| SubcategoryService | backend/src/domains/quizzes/services/subcategory.service.ts | Управление подкатегориями |
| QuizStatsService | backend/src/domains/quizzes/services/quiz-stats.service.ts | Статистика с кешированием |
| SeasonQuizService | backend/src/domains/seasons/services/season-quiz.service.ts | Генерация и импорт квизов для сезона |
| Admin Quizzes Routes | backend/src/domains/quizzes/routes/admin-quizzes.routes.ts | CRUD эндпоинты квизов |
| Admin Categories Routes | backend/src/domains/quizzes/routes/admin-categories.routes.ts | CRUD эндпоинты категорий |
| Admin Subcategories Routes | backend/src/domains/quizzes/routes/admin-subcategories.routes.ts | CRUD эндпоинты подкатегорий |
| Admin Quiz Metadata Routes | backend/src/domains/quizzes/routes/admin-quiz-metadata.routes.ts | Метаданные для каскадных селектов |
| Admin Upload Routes | backend/src/domains/quizzes/routes/admin-quizzes-upload.routes.ts | Загрузка изображений квизов |
| Quiz Schemas | backend/src/domains/quizzes/schemas/quiz.schemas.ts | Валидация запросов квизов |
| Admin Quiz Schemas | backend/src/domains/quizzes/schemas/admin-quiz.schemas.ts | Схемы для metadata и upload |
| Category Schemas | backend/src/domains/quizzes/schemas/category.schemas.ts | Валидация запросов категорий |
| Subcategory Schemas | backend/src/domains/quizzes/schemas/subcategory.schemas.ts | Валидация запросов подкатегорий |
| Eligibility Config | quizzes/config/quiz-eligibility.json | Правила исключения предметов из квизов |
| DB Schema | quizzes/src/db/schema.sql | Схема SQLite (items.quiz_eligible) |
| Migration | quizzes/src/db/migrate.js | Pipeline: JSON → SQLite + eligibility enrichment |
5. Database Schema
Models
| Модель | Описание | Ключевые поля |
|---|---|---|
| Category | Категория квизов | name, displayName, icon, color, isActive |
| Subcategory | Подкатегория | categoryId, name, isActive |
| Quiz | Вопрос квиза | question, questionType, options, correctAnswer, explanation, difficulty, rewardScrap, isActive, publishedInTelegram |
| QuizResult | Результат прохождения | userId, quizId, score, percentage, correctAnswers, totalQuestions, timeSpent, earnedScrap |
| PublishedQuiz | Публикация квиза в Telegram канал (авто-удаление через 24ч) | quizId, postId, messageId, difficulty, publishedAt, expiresAt, isDeleted, totalAnswers, correctAnswers, incorrectAnswers, shuffledOrder, deletedAt |
Relationships
Key Fields
Quiz model details
| Поле | Тип | Описание |
|---|---|---|
questionType | SINGLE | MULTIPLE | Тип вопроса |
options | Json | [{text: string, isCorrect: boolean}] |
correctAnswer | Json | Строка (SINGLE) или массив строк (MULTIPLE) |
slug | String? | Идентификатор сущности: "mp5a4", "launch-site" |
entityType | String? | Тип: "item", "monument", "mechanic", "npc", "vehicle" |
difficulty | EASY | MEDIUM | HARD | Сложность |
timeLimit | Int? | Лимит времени в секундах |
rewardScrap | Int | Награда за правильный ответ |
contentFingerprint | String? | SHA256 fingerprint для cross-season дедупликации (см. Cross-Season Deduplication) |
currentSeasonId | String? | Привязка к сезону (null = не привязан) |
totalAttempts | Int | Счётчик попыток (денормализация) |
avgScore | Float | Средний балл (денормализация) |
6. API Endpoints
User API для квизов отсутствует — пользователи проходят квизы в комментариях к постам Telegram канала через бота.
- Quizzes
- Categories
- Subcategories
- Quiz Metadata
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /admin/quizzes | Список квизов с фильтрами | → |
| GET | /admin/quizzes/:id | Детали квиза | → |
| POST | /admin/quizzes | Создание квиза | → |
| PUT | /admin/quizzes/:id | Обновление квиза | → |
| DELETE | /admin/quizzes/:id | Удаление квиза | → |
| PATCH | /admin/quizzes/:id/toggle-active | Переключение активности | → |
| POST | /admin/quizzes/bulk-action | Массовые операции | → |
| GET | /admin/quizzes/:id/stats | Статистика квиза | → |
| GET | /admin/quizzes/availability?seasonId&slug&categoryId? | Проверка наличия квизов по slug в сезоне (для Achievement Editor) | — |
| GET | /admin/quizzes/telegram-publishable | Квизы, доступные для ручной публикации в Telegram | — |
| POST | /admin/quizzes/:id/publish-telegram | Ручная публикация квиза в Telegram канал | — |
| POST | /admin/quizzes/upload-image | Загрузка изображения для квиза (multipart) | — |
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /admin/categories | Список категорий | → |
| GET | /admin/categories/:id | Получить категорию по ID | — |
| POST | /admin/categories | Создание категории | → |
| PUT | /admin/categories/:id | Обновление категории | → |
| DELETE | /admin/categories/:id | Удаление категории | → |
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /admin/subcategories | Список подкатегорий | → |
| GET | /admin/subcategories/:id | Детали подкатегории | — |
| GET | /admin/subcategories/categories/:categoryId/subcategories | Подкатегории категории | → |
| POST | /admin/subcategories | Создание подкатегории | → |
| PUT | /admin/subcategories/:id | Обновление подкатегории | → |
| DELETE | /admin/subcategories/:id | Удаление подкатегории | → |
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /admin/quiz-metadata | Метаданные квизов (категории, подкатегории, entityTypes, slugs) для каскадных селектов в форме достижений | — |
7. Related
- Seasons — квизы генерируются для сезона (Step 5 Setup Wizard)
- Achievements — ачивки за прохождение квизов (по
slugиentityType) - Quests — квесты "пройди N квизов"
- Security Matrix — обзор защит Admin API