25 KiB
Модуль «Подготовка к экзамену» — план разработки
Цель: превратить пассивный браузер 80 вариантов ЦТ-стиля по математике 9 в полноценный модуль подготовки к экзамену с интерактивным тренажёром, аналитикой и пробниками. Архитектура — generic под несколько экзаменов (математика 9, физика 9, химия 9, ЦТ 11 и т.д.).
Старт: 2026-05-29.
Текущая база: /exam9 — 80 вариантов × 30 заданий, KaTeX + SVG, чтение + просмотр решения, учительский «назначить как ДЗ» (только нечётные).
Совместимость: старая страница exam9.html сохраняется как «Браузер вариантов» внутри нового модуля (вкладка), URL /exam9 переадресуется на /exam-prep/math9/variants.
0. Generic-архитектура: что значит «несколько экзаменов»
Модуль параметризуется ключом экзамена examKey:
| examKey | Заголовок | Предмет | Класс | Длит. | Вар. | Зад/вар. |
|---|---|---|---|---|---|---|
math9 |
Экзамен 9 класс — Математика | math | 9 | 180 мин | 80 | 30 |
phys9 |
Экзамен 9 класс — Физика | phys | 9 | 180 мин | — | — |
chem9 |
Экзамен 9 класс — Химия | chem | 9 | 180 мин | — | — |
math11ce |
ЦТ — Математика | math | 11 | 180 мин | — | 30 |
На старте делаем math9. Регистрация остальных — это только заполнение exam_tracks + добавление сидов задач. Никаких кодовых изменений.
1. Структура URL и страниц
Routes (Express)
GET /exam-prep → редирект на /exam-prep/math9 (или список, когда экзаменов >1)
GET /exam-prep/:examKey → exam-prep.html (дашборд)
GET /exam-prep/:examKey/variants → exam-prep-variants.html (старый /exam9 view)
GET /exam-prep/:examKey/variants/:n → exam-prep-variants.html?v=:n
GET /exam-prep/:examKey/practice → exam-prep-practice.html (тренажёр случайных задач)
GET /exam-prep/:examKey/topics → exam-prep-topics.html (карта тем)
GET /exam-prep/:examKey/topics/:slug → exam-prep-topics.html?t=:slug
GET /exam-prep/:examKey/mock → exam-prep-mock.html (стартовый экран)
GET /exam-prep/:examKey/mock/:mid → exam-prep-mock.html?m=:mid (в процессе/результат)
GET /exam9 → 301 redirect /exam-prep/math9/variants (back-compat)
В backend/src/server.js добавляются 5-6 кастомных роутов, отдающих соответствующий HTML (паттерн как у /exam9).
Файлы фронта
frontend/
exam-prep.html ← дашборд
exam-prep-variants.html ← старый exam9.html, перенесённый сюда
exam-prep-practice.html ← тренажёр (random)
exam-prep-topics.html ← темы (список + по теме)
exam-prep-mock.html ← пробный (старт + run + результат)
css/exam-prep.css ← общий CSS модуля
js/exam-prep/
common.js ← общая загрузка examKey, sidebar, helpers
api.js ← обёртки LS.api для /api/exam-prep/*
task-card.js ← рендер задания с проверкой ответа
answer-input.js ← компонент ввода (radio для MC, text для open)
katex.js ← обёртка над KaTeX bootstrap
progress.js ← вычисление прогресса/streak
dashboard.js ← логика exam-prep.html
practice.js ← логика exam-prep-practice.html
topics.js ← логика exam-prep-topics.html
mock.js ← логика exam-prep-mock.html
variants.js ← логика exam-prep-variants.html (по сути старый app.js)
2. База данных (новые таблицы)
Все таблицы создаются миграцией 004_exam_prep.sql. Существующие exam9_variant_tests и feature_exam9_enabled остаются для совместимости с учительским «назначить как ДЗ».
-- Реестр экзаменов
CREATE TABLE exam_tracks (
exam_key TEXT PRIMARY KEY, -- 'math9'
title TEXT NOT NULL, -- 'Экзамен 9 класс — Математика'
subject_slug TEXT NOT NULL, -- 'math'
grade INTEGER NOT NULL, -- 9
duration_min INTEGER NOT NULL, -- 180
tasks_per_variant INTEGER NOT NULL, -- 30
variants_count INTEGER NOT NULL, -- 80
scoring_json TEXT, -- JSON: [{correct:30, score:100}, ...] сетка
intro_html TEXT, -- описание для лендинга
enabled INTEGER NOT NULL DEFAULT 1,
sort_order INTEGER NOT NULL DEFAULT 0
);
-- Банк задач (импорт из v*.js)
CREATE TABLE exam_tasks (
id INTEGER PRIMARY KEY,
exam_key TEXT NOT NULL REFERENCES exam_tracks(exam_key) ON DELETE CASCADE,
variant INTEGER NOT NULL, -- 1..80
task_idx INTEGER NOT NULL, -- 1..30
task_type TEXT NOT NULL, -- 'mc' | 'open' | 'long'
text_html TEXT NOT NULL, -- условие (HTML с $...$ для KaTeX)
figure_html TEXT, -- опциональный SVG/HTML
opts_json TEXT, -- JSON: [["а","..."],...] для mc, NULL для остальных
answer TEXT, -- 'г' для mc, '-2' / '7500' для open, NULL для long
solution_html TEXT NOT NULL, -- полное решение (HTML)
topic TEXT, -- 'algebra/powers' (заполнится в фазе 4)
subtopic TEXT,
difficulty INTEGER, -- 1-5
UNIQUE(exam_key, variant, task_idx)
);
CREATE INDEX idx_exam_tasks_topic ON exam_tasks(exam_key, topic);
CREATE INDEX idx_exam_tasks_variant ON exam_tasks(exam_key, variant);
-- Попытки пользователя
CREATE TABLE exam_attempts (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
exam_task_id INTEGER NOT NULL REFERENCES exam_tasks(id) ON DELETE CASCADE,
user_answer TEXT,
is_correct INTEGER, -- 0/1, NULL = долгое задание без автопроверки
time_ms INTEGER, -- сколько потратил на эту задачу
mode TEXT NOT NULL, -- 'practice'|'variant'|'topic'|'mock'
session_id INTEGER, -- группировка попыток в пробнике/тем.сессии
hint_used INTEGER NOT NULL DEFAULT 0, -- 0/1, открывал ли подсказку
solution_viewed INTEGER NOT NULL DEFAULT 0,-- 0/1, открывал ли решение
created_at INTEGER NOT NULL -- unix ms
);
CREATE INDEX idx_exam_attempts_user_time ON exam_attempts(user_id, created_at DESC);
CREATE INDEX idx_exam_attempts_task ON exam_attempts(exam_task_id);
CREATE INDEX idx_exam_attempts_session ON exam_attempts(session_id) WHERE session_id IS NOT NULL;
-- Пробные экзамены
CREATE TABLE exam_mock_sessions (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
exam_key TEXT NOT NULL,
variant INTEGER, -- если из готового варианта
source TEXT NOT NULL, -- 'variant' | 'random' | 'weak-topics'
task_ids_json TEXT NOT NULL, -- JSON [id,id,...] — порядок задач
started_at INTEGER NOT NULL,
finished_at INTEGER,
duration_planned_min INTEGER NOT NULL, -- 180
score INTEGER, -- балл по сетке
total_correct INTEGER,
total_tasks INTEGER,
status TEXT NOT NULL DEFAULT 'active' -- 'active'|'finished'|'abandoned'
);
CREATE INDEX idx_mock_user ON exam_mock_sessions(user_id, started_at DESC);
-- План подготовки (по дате экзамена)
CREATE TABLE exam_user_plan (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
exam_key TEXT NOT NULL,
exam_date TEXT NOT NULL, -- YYYY-MM-DD
daily_target INTEGER NOT NULL DEFAULT 10, -- задач в день
weak_focus INTEGER NOT NULL DEFAULT 1, -- приоритезировать слабые темы
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
PRIMARY KEY (user_id, exam_key)
);
-- Темы (справочник, заполняется в фазе 4)
CREATE TABLE exam_topics (
slug TEXT PRIMARY KEY, -- 'algebra-powers'
exam_key TEXT NOT NULL,
parent_slug TEXT, -- 'algebra' для вложенности
title TEXT NOT NULL, -- 'Степени и корни'
description TEXT,
sort_order INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_topics_exam ON exam_topics(exam_key, parent_slug);
Seed: exam_tracks
INSERT INTO exam_tracks (exam_key,title,subject_slug,grade,duration_min,tasks_per_variant,variants_count,scoring_json,sort_order)
VALUES ('math9','Экзамен 9 класс — Математика','math',9,180,30,80,
'[{"correct":30,"score":100},{"correct":28,"score":95},...]', -- сетка перевода
10);
Импорт-скрипт
backend/scripts/import-exam-tasks.js — переносит frontend/js/exam9/variants/v*.js в таблицу exam_tasks:
- Загружает каждый
v*.jsв изолированный VM-контекст с подставнымVARIANTS = {}. - Для каждой задачи парсит:
text_html,figure_html,solution_html,opts_json— напрямую.task_type— из наличияopts: если есть →mc, иначе если вsolесть<div class="sol-ans">Ответ: $число$→open, иначе →long.answer— регулярка по<div class="sol-ans">Ответ:\s*([абвгд])для MC;Ответ:\s*\$?(-?[\d,\.]+)\$?для open; NULL для long.
- Идемпотентно:
DELETE FROM exam_tasks WHERE exam_key='math9'перед вставкой. - Запуск:
node backend/scripts/import-exam-tasks.js math9. Без аргумента — все ключи.
Качество автопарса: ожидаемо ~80-90% задач получат корректный answer. Остальные требуют:
- ручной правки
v*.js(добавитьanswer: "г"явным полем в task → парсер берёт его в первую очередь), - либо помечаем
task_type='long'(не идут в тренажёр, только в просмотре варианта).
Поле answer в v*.js — необязательное расширение текущего формата. Старый код exam9.html его игнорирует.
3. Backend API
Новый роут: backend/src/routes/exam-prep.js, монтируется как /api/exam-prep.
| Метод | Путь | Что делает |
|---|---|---|
| GET | /tracks |
список включённых треков (для лендинга) |
| GET | /:examKey/info |
трек + агрегаты пользователя (всего задач, решено, точность) |
| GET | /:examKey/variants |
список вариантов с прогрессом пользователя (для browse-вкладки) |
| GET | /:examKey/variants/:n/tasks |
задачи варианта N (text/figure/opts/answer-shape) |
| GET | /:examKey/topics |
дерево тем + кол-во задач + точность пользователя по теме |
| GET | /:examKey/topics/:slug/tasks?limit=20&exclude_solved=1 |
задачи по теме для тренажёра |
| GET | /:examKey/practice/next?count=10&strategy=random|weak|unsolved |
выборка задач для тренажёра |
| GET | /:examKey/dashboard |
агрегат: точность, последние попытки, streak, top-3 слабых тем, прогресс по плану |
| GET | /:examKey/plan |
план пользователя |
| PUT | /:examKey/plan |
сохранить план (exam_date, daily_target, weak_focus) |
| POST | /attempts |
сохранить попытку {exam_task_id, user_answer, is_correct, time_ms, mode, session_id, hint_used, solution_viewed} |
| GET | /attempts?task_id=X |
история попыток по задаче (для повторного открытия) |
| POST | /:examKey/mock/start |
создать пробник {source, variant?, count?, weak_topics?} → {id, task_ids, duration_min} |
| GET | /mock/:id |
состояние пробника + задачи (без answer/solution пока active) |
| POST | /mock/:id/answer |
сохранить ответ (без проверки) {task_id, user_answer, time_ms} |
| POST | /mock/:id/finish |
завершить → вернуть результат с is_correct/solution |
| GET | /mock/:id/result |
результат завершённого пробника (балл + разбор) |
| GET | /admin/tasks/:id (admin) |
редактор задачи (для исправлений) |
| PUT | /admin/tasks/:id (admin) |
сохранить правки задачи |
Ответы безопасны: при mode=mock и статусе active — answer и solution_html не отдаются. После finish отдаются.
Производительность: агрегаты дашборда (точность, слабые темы) считаются одним запросом с GROUP BY, без N+1.
4. Расчёты и формулы
Точность
accuracy = correct_attempts / total_attempts
Где correct_attempts — последняя попытка по каждой задаче (не «все попытки за всё время»). Это даёт реальный текущий уровень: если ученик ошибся, потом решил — считаем решённой.
Слабая тема
Тема со ≥3 попыток и точностью < 60%, отсортированы по (accuracy ASC, attempts DESC). Top-3 показываем на дашборде.
Streak
Подряд идущие календарные дни с ≥1 правильной попыткой. Сегодня обнуляет/сохраняет.
Готовность к экзамену (gauge 0-100)
readiness = 0.6 * (coverage) + 0.4 * (accuracy_weighted)
coverage = topics_with_3plus_attempts / total_topics
accuracy_weighted = avg(accuracy_per_topic) только по тем с ≥3 попытками
Метрика — для дашборда (виджет «Готовность: 73%»). Грубая, мотивационная.
План «Сегодня»
days_left = max(1, exam_date - today)
tasks_left = total_tasks - distinct_solved_tasks
daily_target = clamp(ceil(tasks_left / days_left), 5, 50)
weights = если weak_focus=1: 70% задач из top-3 слабых тем, 30% случайных нерешённых
Балл пробника
По scoring_json из трека (сетка перевода кол-ва правильных в балл). Если NULL — просто процент.
5. Фазы реализации
| # | Что | Фронт | Бэк | Оценка |
|---|---|---|---|---|
| F0 | Миграция 004_exam_prep.sql + seed math9 + импорт-скрипт + проверка качества автопарса ответов |
— | DB schema, import script | 0.5 дня |
| F1 | Скелет страниц exam-prep.html + sidebar (замена /exam9), общий CSS, common.js, базовый дашборд (статичный) |
5 HTML, общий CSS, sidebar | route handlers (отдача HTML), /api/exam-prep/tracks, /info |
1 день |
| F2 | Браузер вариантов (перенос текущего /exam9 в exam-prep-variants.html, читает из API а не из v*.js) |
variants.js + рефактор |
/variants, /variants/:n/tasks |
0.5 дня |
| F3 | Компонент task-card с вводом ответа + проверкой + POST /attempts + просмотром решения после ответа |
task-card.js, answer-input.js |
/attempts (POST/GET) |
1 день |
| F4 | Дашборд live: точность, последние попытки, streak, прогресс-бар. Реальные данные. | dashboard.js, progress.js |
/dashboard |
0.5 дня |
| F5 | Тренажёр «случайные задачи» — выборка через /practice/next?strategy=random|unsolved |
practice.js |
/practice/next |
0.5 дня |
| F6 | Тегирование тем: LLM-скрипт классификации + ручная проверка + exam_topics справочник |
— | tag-tasks.js скрипт |
1 день (включая ручную верификацию) |
| F7 | Карта тем + тренажёр по теме | topics.js, topic-card.js |
/topics, /topics/:slug/tasks |
0.5 дня |
| F8 | Слабые места на дашборде + кнопка «Прокачать тему» + strategy=weak |
+ изменения в dashboard.js/practice.js | расширение /dashboard, /practice/next |
0.5 дня |
| F9 | Пробный экзамен: старт, таймер, ответы без проверки, финиш, результат с разбором | mock.js (3 экрана) |
/mock/start, /mock/:id/answer, /mock/:id/finish, /mock/:id/result |
1.5 дня |
| F10 | План по дате экзамена: форма даты + виджет «Сегодня N задач» на дашборде | дополнение dashboard.js |
/plan (GET/PUT), интеграция в /dashboard |
0.5 дня |
| F11 | Учительская часть: расширить import-exam9.js → все 80 вариантов; отчёт класса по темам |
таб «Класс» на learners-странице | расширение /api/classes/:id/exam-stats |
1 день |
| F12 | Полировка: skeleton-loaders, empty states, mobile UI, achievements (интеграция с gamification) | косметика | — | 0.5 дня |
Итого: ~10 дней работы (одна большая сессия = 1 фаза). Опционально F11+F12 после внутренней приёмки.
Параллелизация
- F0 → F1 → F2 — последовательно (база).
- F3 — после F0.
- F4 — после F3.
- F5 — после F3.
- F6 — независим, можно параллельно с F1-F5.
- F7-F8 — после F6.
- F9 — после F3.
- F10 — после F4.
6. Sidebar — точечные изменения
В js/sidebar.js, группа content:
- ${L('/exam9', 'clipboard-check', 'Экзамен 9 класс')}
+ ${L('/exam-prep/math9', 'clipboard-check', 'Подготовка к экзамену 9')}
Внутри страницы /exam-prep/math9 — табы или подменю на 4 вкладки: Дашборд · Варианты · Тренажёр · Темы · Пробник.
Когда будут другие экзамены — родительский пункт Подготовка к экзаменам (расскрывающаяся группа) с детьми.
7. Подсчёт ответа: алгоритм проверки
В task-card.js при клике «Проверить»:
task_type |
Ввод | Эталон | Проверка |
|---|---|---|---|
mc |
radio выбран 'г' |
answer='г' |
строгое равенство |
open |
text input '7500' |
answer='7500' |
нормализация: запятая→точка, обрезка пробелов, парсинг как Number, сравнение с 1e-6 погрешностью |
long |
нет ввода (только «Я решил / Показать решение») | NULL | пользователь сам отмечает правильность |
Ответы хранятся в user_answer строкой как ввёл, is_correct — 0/1/NULL.
8. Совместимость и миграция
- URL
/exam9→ 301 на/exam-prep/math9/variants(server.js). - localStorage
exam9_progress_v1— оставляем, не мигрируем. После запуска прогресс пишется в БД, старый кэш не мешает. - localStorage
exam9_last_variant— переименовываем вexam_prep_math9_last_variant. exam9_variant_tests— не трогаем, учительский «назначить как ДЗ» продолжает работать. В F11 расширяем импорт на 80 вариантов.v*.jsфайлы — остаются как авторский источник. Импорт-скрипт перегружает таблицуexam_tasks. Любые правки в v55.js →npm run import-exam-tasks math9.- KaTeX — общий bootstrap через
js/exam-prep/katex.js.
9. Что не входит в этот план (future)
- Видео-разборы решений.
- Конструктор кастомного варианта учителем.
- Лидерборд между учениками.
- Adaptive difficulty (карта Лейтнера для задач).
- Push в Telegram по плану.
- Экспорт пробника в PDF.
10. Зафиксированные решения (2026-05-29)
| Вопрос | Решение |
|---|---|
| Откуда брать правильный ответ | Гибрид: если в task есть явное поле answer: — берём его; иначе парсим из <div class="sol-ans"> в sol регуляркой. Неуспешные парсы логируются — фиксим явным полем. |
| Long-задачи (без автопроверки) | В режимах practice/topic — фильтр task_type != 'long'. В variant/mock — присутствуют, ученик после решения сам отмечает «решил/не решил». |
| Когда задача «решена» | ≥1 правильная попытка за всю историю. Прогресс не откатывается. Для long — если пользователь отметил «решил». |
| История попыток | Все. Accuracy считаем по последней попытке на задачу, история — для виджета «Последние попытки». |
| Иерархия тем | Двухуровневая: algebra → algebra-powers. На UI группируем по родителю, можно выбрать раздел целиком или конкретную подтему. ~6-8 разделов, ~25-30 подтем. |
Acceptance criteria (для всего модуля)
- Ученик может выбрать тему и прорешать 10 задач с проверкой ответа.
- Точность пользователя обновляется в реальном времени на дашборде.
- Пробник на 180 минут с таймером, балл по сетке ЦТ.
- Top-3 слабых тем на дашборде с кнопкой «Прокачать».
- План по дате экзамена показывает «Сегодня N задач».
- Старая страница вариантов работает без изменений (через новый URL).
- Учитель может назначить любой из 80 вариантов как ДЗ (не только нечётные).
- Архитектура позволяет добавить
phys9без изменений кода — только сид + импорт.