From 623fbde38b4bb59f98e70d9f469760996592ae34 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Mon, 15 Jun 2026 11:36:56 +0300 Subject: [PATCH] =?UTF-8?q?feat(ct-math):=20=D1=83=D1=80=D0=BE=D0=BA=D0=B8?= =?UTF-8?q?=20=D1=81=D1=82=D0=B5=D1=80=D0=B5=D0=BE=D0=BC=D0=B5=D1=82=D1=80?= =?UTF-8?q?=D0=B8=D0=B8=20(44-47)=20+=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF?= =?UTF-8?q?=D1=82=20=D0=BC=D0=B8=D0=BD=D0=B8-=D1=84=D0=B8=D0=BA=D1=81?= =?UTF-8?q?=D0=B0=20866/1248?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backend/scripts/seed_ctmath_lessons_stereo.js — 4 урока блока «Стереометрия» по PILOT_STEREOMETRY (расположение/сечения, многогранники, тела вращения, координатный метод В20) в курс 13; применён (lessons.id=44-47, 60 блоков). - backend/scripts/fix_ctmath_misc.js — точечный фикс exam_tasks id=866 (варианты-прямые в норму) и id=1248 (битый источник → long); dry/--apply, идемпотентен. Запись блокируется авто-режимом — запускает пользователь. - README: статус (уроки стерео, сайдбар, остаток). Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/scripts/fix_ctmath_misc.js | 45 ++++++ backend/scripts/seed_ctmath_lessons_stereo.js | 128 ++++++++++++++++++ plans/ct-math/README.md | 12 +- 3 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 backend/scripts/fix_ctmath_misc.js create mode 100644 backend/scripts/seed_ctmath_lessons_stereo.js diff --git a/backend/scripts/fix_ctmath_misc.js b/backend/scripts/fix_ctmath_misc.js new file mode 100644 index 0000000..21055a5 --- /dev/null +++ b/backend/scripts/fix_ctmath_misc.js @@ -0,0 +1,45 @@ +'use strict'; +/* + * Точечная полировка 2 mc-задач ctmath: + * - id=866: варианты-прямые вшиты в середину текста, opts = цифры-указатели → + * нормальный opts_json + чистый текст (answer сохраняем = 4). + * - id=1248: битый источник (нет верного варианта, опции не сходятся) → 'long'. + * Идемпотентно (проверяет текущее состояние). dry по умолчанию, запись --apply. + */ +const db = require('../src/db/db'); +const APPLY = process.argv.includes('--apply'); + +const t866 = db.prepare('SELECT id,task_type,answer,opts_json FROM exam_tasks WHERE id=866').get(); +const t1248 = db.prepare('SELECT id,task_type FROM exam_tasks WHERE id=1248').get(); +const plan = []; + +if (t866 && t866.task_type === 'mc') { + // opts уже нормальные? (значения не цифры-указатели) + let o = []; try { o = JSON.parse(t866.opts_json); } catch {} + const isDigit = o.length && o.every(p => /^[1-9]$/.test(String(p[1]).trim())); + if (isDigit) { + plan.push({ + id: 866, + set: { + text_html: 'A16. Какая из прямых пересекает график функции $y=x^4-3x^2+11x$ в 11 добавочных точках?', + opts_json: JSON.stringify([['1', '$y=-3$'], ['2', '$y=-1{,}5$'], ['3', '$y=0$'], ['4', '$y=4k$'], ['5', '$y=2$']]), + answer: '4', + }, + }); + } else console.log('id=866 уже не цифровой — пропуск'); +} else console.log('id=866 нет или уже не mc — пропуск'); + +if (t1248 && t1248.task_type === 'mc') { + plan.push({ id: 1248, set: { task_type: 'long', answer: null } }); +} else console.log('id=1248 нет или уже не mc — пропуск'); + +console.log(APPLY ? '[APPLY]' : '[DRY-RUN]', 'к изменению:', plan.map(p => p.id).join(', ') || '(нет)'); +for (const p of plan) console.log(' id', p.id, '→', JSON.stringify(p.set).slice(0, 160)); + +if (!APPLY) { console.log('DRY-RUN: запись НЕ выполнялась. Запись: --apply'); process.exit(0); } +for (const p of plan) { + const cols = Object.keys(p.set); + const sql = `UPDATE exam_tasks SET ${cols.map(c => c + '=@' + c).join(', ')} WHERE id=@id`; + db.prepare(sql).run({ ...p.set, id: p.id }); +} +console.log('Обновлено:', plan.length); diff --git a/backend/scripts/seed_ctmath_lessons_stereo.js b/backend/scripts/seed_ctmath_lessons_stereo.js new file mode 100644 index 0000000..1d4fde4 --- /dev/null +++ b/backend/scripts/seed_ctmath_lessons_stereo.js @@ -0,0 +1,128 @@ +'use strict'; +/* + * Уроки блока «Стереометрия» курса «ЦЭ/ЦТ — Математика» (по PILOT_STEREOMETRY.md). + * 4 урока: расположение/сечения → многогранники → тела вращения → углы/расстояния. + * Форматы блоков — под рендер frontend/lesson.html (text/heading/callout esc-only; + * математика $…$/$$…$$; callout.style=info|warning|success|error). Идемпотентно. + * node backend/scripts/seed_ctmath_lessons_stereo.js [--dry] + */ +const db = require('../src/db/db'); +const DRY = process.argv.includes('--dry'); +const COURSE_TITLE = 'ЦЭ/ЦТ — Математика', SECTION_TITLE = 'Стереометрия'; + +const course = db.prepare("SELECT id FROM courses WHERE subject_slug='math' AND title=?").get(COURSE_TITLE); +if (!course) { console.error('Нет курса. Сначала seed_ctmath_course.js'); process.exit(1); } +const section = db.prepare('SELECT id FROM course_sections WHERE course_id=? AND title=?').get(course.id, SECTION_TITLE); +if (!section) { console.error('Нет секции «' + SECTION_TITLE + '»'); process.exit(1); } + +const H = (text, level = 2) => ['heading', { text, level }]; +const P = (text) => ['text', { text }]; +const F = (tex, label) => ['formula', label ? { label, tex } : { tex }]; +const CI = (text) => ['callout', { style: 'info', text }]; +const CW = (text) => ['callout', { style: 'warning', text }]; +const CS = (text) => ['callout', { style: 'success', text }]; +const SIM = (caption) => ['sim', { simId: 'stereo', caption }]; +const FC = (front, back) => ['flashcard', { front, back }]; +const ORD = (question, items) => ['ordering', { question, items }]; +const ACC = (title, content) => ['accordion', { title, content }]; + +// M26 — расположение, сечения (А2, В1) +const L1 = [ + H('Прямые и плоскости в пространстве'), + P('Две прямые в пространстве: пересекаются, параллельны или скрещиваются. Прямая и плоскость: прямая лежит в плоскости, параллельна ей или пересекает её. Две плоскости: параллельны или пересекаются по прямой.'), + SIM('Покрутите фигуру: найдите линию пересечения двух плоскостей и пары скрещивающихся прямых'), + CI('Линия пересечения двух плоскостей проходит через их общие точки. В правильной пирамиде плоскости, проходящие через вершину и центр основания, пересекаются по прямой через вершину (например, $SO$).'), + F('a\\parallel b,\\ b\\subset\\alpha,\\ a\\not\\subset\\alpha \\Rightarrow a\\parallel\\alpha', 'Признак параллельности прямой и плоскости'), + CW('В задании В1 (выбор верных утверждений о расстояниях) проверяйте каждое утверждение отдельно: расстояние между скрещивающимися прямыми — это длина их общего перпендикуляра, а не любого отрезка.'), + H('Разбор А2', 3), + P('Пример. В правильной четырёхугольной пирамиде $SABCD$ ($O$ — центр основания) найдите прямую пересечения плоскостей $DSO$ и $SCB$.'), + P('Решение. Обе плоскости проходят через вершину $S$, значит линия их пересечения проходит через $S$; анализом общих точек получаем прямую $SO$.'), + CS('Метод: ищем общие точки двух плоскостей — через них проходит линия пересечения.'), + FC('Расстояние между скрещивающимися прямыми', 'Длина их общего перпендикуляра'), + FC('Линия пересечения двух плоскостей', 'Проходит через все их общие точки'), + CI('Тренажёр: задания А2 и В1 по теме «Стереометрия» в практике модуля /exam-prep/ctmath. Цель: не менее 80%.'), +]; + +// M27 — многогранники (В13, В17) +const L2 = [ + H('Многогранники: объёмы, площади, подобие'), + F('V_{\\text{призмы}}=S_{\\text{осн}}\\cdot h,\\qquad V_{\\text{пирамиды}}=\\tfrac{1}{3}S_{\\text{осн}}\\cdot h', 'Объёмы'), + P('Сечение, параллельное основанию пирамиды, отсекает подобную фигуру. Если высота делится от вершины в отношении $k$, то линейные размеры сечения относятся к основанию как $k$, а площади — как $k^2$.'), + F('\\dfrac{S_{\\text{сеч}}}{S_{\\text{осн}}}=k^2,\\quad k=\\dfrac{\\text{высота до сечения}}{\\text{вся высота}}', 'Сечение, параллельное основанию'), + SIM('Сечение пирамиды плоскостью, параллельной основанию'), + CW('В задании В17 ловят на том, что как $k^2$ относятся именно площади, а не длины. Сначала найдите $k$ из отношения высот, затем возводите в квадрат.'), + H('Разбор В17', 3), + P('Пример. Плоскость, параллельная основанию треугольной пирамиды, делит высоту в отношении $5:3$ от вершины. Площадь сечения меньше площади основания на $39$. Найдите площадь сечения.'), + P('Решение. $k=\\dfrac{5}{5+3}=\\dfrac{5}{8}$, поэтому $\\dfrac{S_{\\text{сеч}}}{S_{\\text{осн}}}=\\dfrac{25}{64}$. Пусть $S_{\\text{осн}}=x$: $x-\\dfrac{25}{64}x=39\\Rightarrow\\dfrac{39}{64}x=39\\Rightarrow x=64$. Тогда $S_{\\text{сеч}}=25$.'), + CS('Ответ: $25$.'), + FC('$V$ пирамиды', '$\\tfrac{1}{3}S_{\\text{осн}}\\cdot h$'), + FC('Отношение площадей сечения и основания (сечение $\\parallel$ основанию)', '$k^2$, где $k$ — отношение высот от вершины'), + CI('Тренажёр: В13 и В17 по теме «Стереометрия». Цель: не менее 75%.'), +]; + +// M28 — тела вращения (А9, В13) +const L3 = [ + H('Тела вращения: цилиндр, конус, шар'), + F('S_{\\text{сферы}}=4\\pi R^2,\\qquad V_{\\text{шара}}=\\tfrac{4}{3}\\pi R^3', 'Шар и сфера'), + F('S_{\\text{бок}}=2\\pi R h,\\qquad V=\\pi R^2 h', 'Цилиндр'), + F('S_{\\text{бок}}=\\pi R l,\\qquad V=\\tfrac{1}{3}\\pi R^2 h', 'Конус'), + SIM('Сечение цилиндра плоскостью, параллельной оси'), + CI('Сфера, касающаяся плоскости: радиус в точку касания перпендикулярен плоскости. Расстояние от центра до точки плоскости и радиус образуют прямоугольный треугольник — работает теорема Пифагора.'), + H('Разбор А9', 3), + P('Пример. Квадрат с диагональю $8$ лежит в плоскости $\\alpha$; сфера касается $\\alpha$ в точке пересечения диагоналей; расстояние от центра сферы до вершины квадрата равно $4\\sqrt2$. Найдите площадь сферы.'), + P('Решение. Полудиагональ $=4$. $R^2=(4\\sqrt2)^2-4^2=32-16=16$, $R=4$. Площадь $=4\\pi R^2=64\\pi$.'), + CS('Ответ: $64\\pi$.'), + H('Разбор В13', 3), + P('Пример. Цилиндр рассечён плоскостью, параллельной оси; в сечении квадрат площади $100$; расстояние от оси до плоскости $\\sqrt{39}$. Найдите $\\dfrac{S_{\\text{бок}}}{\\pi}$.'), + P('Решение. Сторона квадрата $=10$ (это и высота, и хорда). $R^2=(\\sqrt{39})^2+5^2=39+25=64$, $R=8$. $S_{\\text{бок}}=2\\pi\\cdot8\\cdot10=160\\pi$.'), + CS('Ответ: $160$.'), + FC('$S$ сферы', '$4\\pi R^2$'), + FC('$V$ шара', '$\\tfrac{4}{3}\\pi R^3$'), + FC('$S_{\\text{бок}}$ конуса', '$\\pi R l$'), + CI('Тренажёр: А9 и В13 по теме «Стереометрия». Цель: не менее 80% (А9) и 70% (В13).'), +]; + +// M29 — углы и расстояния, координатный метод (В20) +const L4 = [ + H('Координатный метод: угол между прямыми'), + P('Универсальный приём для В20: ввести удобную систему координат (вершину фигуры в начало), выписать координаты нужных точек, составить направляющие векторы прямых и найти угол через косинус скалярного произведения. Если геометрия «не идёт» — считайте координатами.'), + F('\\cos\\varphi=\\dfrac{|\\vec a\\cdot\\vec b|}{|\\vec a|\\,|\\vec b|}', 'Угол между прямыми через векторы'), + F('\\vec a\\cdot\\vec b=a_xb_x+a_yb_y+a_zb_z,\\qquad |\\vec a|=\\sqrt{a_x^2+a_y^2+a_z^2}', 'Скалярное произведение и длина'), + SIM('Угол между скрещивающимися прямыми'), + ORD('Расставьте шаги решения В20 координатным методом', [ + 'Ввести систему координат', + 'Выписать координаты точек (учесть отношения деления рёбер)', + 'Составить направляющие векторы прямых', + 'Найти cos φ через скалярное произведение и длины', + ]), + CW('В числителе — модуль скалярного произведения (угол между прямыми не превосходит $90^\\circ$). Частые ошибки В20 — потеря модуля и неверные координаты точек деления рёбер.'), + ACC('Альтернативы (раскрыть)', 'Угол между прямой и плоскостью считают через нормаль плоскости; есть также теорема о трёх синусах. Но координатный метод универсален и почти всегда быстрее в задачах ЦТ.'), + H('Разбор В20', 3), + P('Пример. В кубе $ABCDA_1B_1C_1D_1$ с ребром $1$ найдите $8\\cos^2\\varphi$, где $\\varphi$ — угол между прямыми $AB_1$ и $BC_1$.'), + P('Решение. Координаты: $A(0;0;0)$, $B(1;0;0)$, $B_1(1;0;1)$, $C_1(1;1;1)$. Векторы $\\vec{AB_1}=(1;0;1)$, $\\vec{BC_1}=(0;1;1)$. $\\cos\\varphi=\\dfrac{|1|}{\\sqrt2\\cdot\\sqrt2}=\\dfrac{1}{2}$, поэтому $8\\cos^2\\varphi=8\\cdot\\dfrac14=2$.'), + CS('Ответ: $2$.'), + FC('Угол между прямыми (векторы)', '$\\cos\\varphi=\\dfrac{|\\vec a\\cdot\\vec b|}{|\\vec a||\\vec b|}$'), + FC('Скалярное произведение', '$a_xb_x+a_yb_y+a_zb_z$'), + FC('Длина вектора', '$\\sqrt{a_x^2+a_y^2+a_z^2}$'), + CI('Тренажёр: В20 по теме «Стереометрия» (координатный метод). Цель: не менее 60% — это самые «дорогие» баллы.'), +]; + +const LESSONS = [ + { title: 'Расположение прямых и плоскостей. Сечения', read: 9, blocks: L1 }, + { title: 'Многогранники: объёмы, площади, подобие', read: 11, blocks: L2 }, + { title: 'Тела вращения: цилиндр, конус, шар', read: 11, blocks: L3 }, + { title: 'Углы и расстояния: координатный метод', read: 12, blocks: L4 }, +]; + +console.log(DRY ? '[DRY-RUN]' : '[APPLY]', `курс id=${course.id}, секция «${SECTION_TITLE}» id=${section.id}`); +const insLesson = db.prepare('INSERT INTO lessons (course_id, title, order_index, is_published, section_id, read_time) VALUES (?,?,?,1,?,?)'); +const insBlock = db.prepare('INSERT INTO lesson_blocks (lesson_id, type, order_index, data) VALUES (?,?,?,?)'); +LESSONS.forEach((L, i) => { + const ex = db.prepare('SELECT id FROM lessons WHERE course_id=? AND title=?').get(course.id, L.title); + if (ex) { console.log(` есть урок: «${L.title}» (id ${ex.id}) — пропуск`); return; } + if (DRY) { console.log(` + урок «${L.title}» (${L.blocks.length} блоков)`); return; } + const lid = insLesson.run(course.id, L.title, 10 + i + 1, section.id, L.read).lastInsertRowid; + L.blocks.forEach(([type, data], bi) => insBlock.run(lid, type, bi, JSON.stringify(data))); + console.log(` + урок «${L.title}» (id ${lid}, ${L.blocks.length} блоков)`); +}); +console.log(DRY ? 'DRY-RUN: ничего не записано.' : 'Готово. Уроки стереометрии добавлены (черновик курса).'); diff --git a/plans/ct-math/README.md b/plans/ct-math/README.md index b61c709..26546c1 100644 --- a/plans/ct-math/README.md +++ b/plans/ct-math/README.md @@ -52,11 +52,11 @@ Работает на `/exam-prep/ctmath` (дашборд, темы, практика, слабые темы, пробники). Скрипт-конвертер: `backend/scripts/seed_ctmath_exam_tasks.js`. -Также (на общих подсистемах): теория-курс `courses.id=13` (черновик) + уроки тригонометрии `41–43` + -диагностика `tests.id=164` + новые темы. +Также (на общих подсистемах): теория-курс `courses.id=13` (черновик) + уроки **тригонометрии `41–43`** +и **стереометрии `44–47`** (по пилотам) + диагностика `tests.id=164` + новые темы. Осталось: -- выдать доступ ученикам: `content_access` (content_type='exam', content_ref='ctmath') классу/ученику; -- добавить пункт сайдбара на `/exam-prep/ctmath`; -- мелкий фикс задачи `exam_tasks.id=1248` (бракованный источник); -- (опц.) дотегировать вопросы под тонкие подтемы; дополнить уроки остальных блоков. +- ✅ пункт сайдбара на `/exam-prep/ctmath` — добавлен. +- выдать доступ ученикам: `content_access` (content_type='exam', content_ref='ctmath') классу/ученику; решить видимость пункта для учеников; +- мелкий фикс задач `exam_tasks.id=866, 1248` — скрипт `backend/scripts/fix_ctmath_misc.js --apply` (запускает пользователь); +- (опц.) уроки остальных 7 блоков; колоды карточек формул; дотегировать вопросы под тонкие подтемы.