全链路死角审查与验收清单
范围:/home/zyw/Downloads/ai-lesson-prep(Next.js 14)。已实证跑通:教案生成 → plan_confirmed → PPT 生成 → ppt_confirmed → 导出 plan_docx / ppt。
本审查只覆盖剩余步骤:材料生成/确认/completed、其余导出类型、教研样例发布、学习通五步、三类课时(无主案例讲授课 / 案例课 / 实验课)。
证据均为代码行号;判定口径:可行 = 按 UI 正常点能走通;有风险 = 能走通但会空文件 / 与 UI 不符 / 需绕路;死路 = 按 UI 操作会 400/按钮永灰,该步骤不可达。
状态机(单 RANK,不是真并行):
not_started → plan_draft → plan_confirmed → ppt_draft → ppt_confirmed → materials_draft → completed
src/lib/prepare/state.ts:3-4, 22-30, 50-81。generate_ppt / generate_materials 用 maxStatus 只升不降;completed 除「重生成教案」外锁死。
0. 总表(先看结论)
| # | 步骤 | 无主案例讲授课 | 案例课 | 实验课 | 判定 |
| 1 | 材料生成 → 确认 → completed | 死路(卡在生成教案),绕开 G2 后材料段可行 | 可行(有主案例) | 死路(同 G2),绕开后材料段可行/有风险 | 见 §1 |
| 2a | 导出 question_bank | 有材料才行;无习题则空表 | 同左 | 同左;实验更易空 | 见 §2 |
| 2b | 导出 question_bank_xlsx | 同上 | 同上 | 同上 | 见 §2 |
| 2c | 导出 materials_teacher / materials_student | 有材料草稿即可(未确认会警告) | 同左 | 同左 | 见 §2 |
| 2d | 导出 activity_list | 可行(只要求已确认教案) | 同左 | 同左 | 见 §2、§4 |
| 2e | 导出 toolkit | 同题库,须有材料 | 同左 | 同左 | 见 §2 |
| 3 | publish-sample | 须先 completed;脱敏有 422 边角 | 同左 | 同左 | 见 §3 |
| 4 | 学习通五步 ↔ activity_list | 无硬依赖 | 同左 | 同左 | 见 §4 |
| 5 | 实验课教案/PPT/材料/题库 | G1 已不再因无目标卡死;G2/课型硬编码仍卡 | — | 见左 | 见 §5 |
1. 材料生成 → 确认(step=materials)→ completed
1.1 主路径可达性
| 字段 | 内容 |
| 判定 | 可行(前提:已 plan_confirmed,且本课能生成出非空 exercises 或 cases) |
| 证据 | 状态机 confirm_materials:src/lib/prepare/state.ts:73-75(当前 RANK ≥ materials_draft 才进 completed)。后端确认:src/app/api/lesson-plans/[id]/confirm/route.ts:97-115(无 draft.materials → 400 need_materials;RANK < materials_draft → 400 need_materials_draft;G1 失败 → 400 g1_no_objectives,同文件 67-74)。生成材料门槛:src/lib/ai/generate.ts:536-544 + src/lib/prepare/state.ts:42-44(须 plan_confirmed 及以上)。落库升态:src/lib/prepare/persist.ts:71-81(generate_materials → maxStatus(..., materials_draft))。废弃一键完成:confirm/route.ts:116-124(step=complete 恒 400 complete_removed)。 |
| UI 确认按钮 | src/app/prepare/[classId]/lesson-dev-panel.tsx:954-977。disabled 条件:busy 或 !bundle.materials 或 status !== "materials_draft" 或 g1 不通过。按钮文案「确认材料(完成备课)」,只在「材料」页签出现。 |
| 修复建议 | 不改主路径。把 status !== "materials_draft" 的 title 写全(G1 / 空材料)。completed 后若允许补确认材料,按钮应在 completed && draft.materials 新于 confirmed 时重新点亮,或提供「更新确认稿」。 |
| 验证方式 | plan_confirmed 后 POST /api/lesson-plans/generate {step:"materials",run:true} → 任务 done → GET 教案 status=materials_draft → PUT .../confirm {step:"materials"} → status=completed。UI:材料页按钮从灰→可点→完成后变灰。 |
1.2 UI 与后端对「空材料」不一致
| 字段 | 内容 |
| 判定 | 有风险(UI 更严;直打 API 可把空材料标成 completed) |
| 证据 | UI 点击处理 lesson-dev-panel.tsx:415-425:exercises.length===0 && cases.length===0 则前端报 need_materials,不发请求。后端 confirm/route.ts:99-104 只判断 !draft.materials(空对象 {} 或 {exercises:[]} 也算有)。Schema src/lib/ai/schema.ts:429-432:exercises 必须是数组,允许空数组。材料页列表 lesson-dev-panel.tsx:982-1003:无习题时显示「尚未生成材料」,但只要 materials 对象在且 status=materials_draft,确认按钮仍可点(空习题+空案例时点下去才报错)。 |
| 修复建议 | 后端与 UI 对齐:确认时要求 exercises.length+cases.length≥1(或实验课允许 worksheets 等字段)。Schema 对空 exercises 标 error,避免 flagged 仍落库升态。 |
| 验证方式 | 手工把 draft.materials 写成 {exercises:[],cases:[]},status=materials_draft:UI 点确认应失败;curl PUT confirm step=materials 今日会 201/200 且 status=completed(这就是不一致)。 |
1.3 确认按钮对状态名过严(重生成教案后的假死)
| 字段 | 内容 |
| 判定 | 有风险(材料还在草稿里,按钮永久灰,直到再点一次「生成材料草稿」) |
| 证据 | `nextStatus("completed" |
| 修复建议 | 确认材料的准入改为「draft.materials 非空 且 RANK ≥ plan_confirmed」,升态用 maxStatus(cur, completed);或重生成教案时若已有材料则 nextStatus 回到 materials_draft 而非 plan_draft 一条路走到底。completed 后更新材料:允许 confirm_materials 覆盖 confirmed.materials。 |
| 验证方式 | completed → 再生成教案 → plan_draft → 确认教案 → plan_confirmed → 材料页确认按钮应为可点(今日不可点)→ 再点「生成材料草稿」后应变为可点。 |
1.4 无主案例讲授课 / 实验课:卡在「生成教案」,材料段根本进不去
| 字段 | 内容 |
| 判定 | 死路(UI 全链路;与「材料」步骤的关系是前置被切) |
| 证据 | UI 生成一律 lessonType: "案例讨论":lesson-dev-panel.tsx:224-243(plan/materials/ppt 三次调用同一函数)。生成 API 的 G2 默认值也是 "案例讨论":src/app/api/lesson-plans/generate/route.ts:66-78。G2:src/lib/prepare/g2.ts:114-126——只要请求课型是案例型,无主案例就 400 g2_no_main_case,不管模板偏好是不是「讲授/实验」。UI 前置拦截只用 lesson.requiresMainCase(wizard-data.ts:128 ← needsMainCase(preferred)),讲授课/实验课 requiresMainCase=false,按钮可点,请求照样被 G2 打回。课型下拉默认同样是「案例讨论」:lesson-dev-panel.tsx:1216-1217。 |
| 三类课时 | 案例课:向导会要主案例(lesson-dev-panel.tsx:226-228, 608),选了就能过 G2,材料段可达。无主案例讲授课 / 实验课:UI 点「生成教案」→ 400,到不了 plan_confirmed,后续材料/完成/发布全断。直打 API 并显式传 lessonType:"讲授" 或 "实验" 可绕开(g2.ts:116-118:requested 非案例型则 mustBeCase=false)。 |
| 修复建议 | runGenerate 传 lesson.preferredLessonTypes[0](或用户在表单选的 plan.lesson_type),禁止写死「案例讨论」。generate/route.ts:71 去掉 ?? "案例讨论",无课型时走模板偏好(G2 的 uniqueTypes.every(isCaseLessonType) 分支)。 |
| 验证方式 | 理工班/未选主案例的讲授课、实验1:UI 点生成教案,今日必 g2_no_main_case。改 body lessonType:"讲授" 后应 201 并落到 plan_draft。 |
1.5 并行 PPT 后仍能确认材料(不是死路)
| 字段 | 内容 |
| 判定 | 可行 |
| 证据 | plan_confirmed → 先生成并确认 PPT → ppt_confirmed(RANK 4)→ 再生成材料 → maxStatus(ppt_confirmed, materials_draft)=materials_draft(state.ts:70-72, 36-39)。此时确认按钮条件满足。先材料后 PPT:generate_ppt 从 materials_draft 不降级(state.ts:64-66);confirm_ppt 的 maxStatus(materials_draft, ppt_confirmed) 仍是 materials_draft(state.ts:67-69),与 UI 提示「PPT 确认不回退」一致(lesson-dev-panel.tsx:389-391, 979-980)。可以不确认 PPT 就完成备课:completed 不要求 ppt_confirmed。 |
| 修复建议 | 无需改可达性。若产品要求「完成=教案+材料+PPT 都确认」,应在 confirm_materials 增加 generation_meta.ppt_confirmed===true 闸门(今日没有)。 |
| 验证方式 | 两条序:A. 教案确认→材料→确认材料→completed(无 PPT)。B. 教案确认→PPT 确认→材料→确认材料→completed。均应成功。 |
2. 导出类型前置校验(src/lib/exporters/run.ts)
公共入口:runLessonExport。弱教案闸:run.ts:136-142——confirmed 里既没有 lesson_title 也没有 objectives 数组 → 400 need_plan_confirmed。空数组 objectives:[] 也算「有数组」,会放行。 PPT 导出读 仅 confirmed(run.ts:144, 281-286);材料读 confirmed 否则 draft(run.ts:145-149)。
needMaterials():run.ts:95-103(materials_teacher|materials_student|question_bank|question_bank_xlsx|toolkit)。activity_list 不在其中。requireMaterials():run.ts:271-278(无材料 → 400 need_materials,文案写的是「须先生成材料再导出题库」,材料 docx 另有文案 run.ts:350-354)。
导出按钮 UI 只看 busy 0 1 !planId(lesson-dev-panel.tsx:904-927),不看状态、不看是否已确认,点下去才 400。与文案「导出基于已确认内容」(900-901 行)不符。
2.1 ppt(已实证,对照用)
| 字段 | 内容 |
| 判定 | 可行(须 PPT 已写入 confirmed_content.ppt.slides) |
| 证据 | run.ts:281-295:无 slides → 400 need_ppt_confirmed;再跑 validateGenerationPayload("ppt"),失败 → 400 ppt_hard_constraint(页级:要点≤5 / 单条≤20 字 / 讨论课争议页+记录页,schema.ts:274-396,上限 PPT_MAX_SLIDES=16 见 src/lib/ai/types.ts:53)。不接受 draft PPT。 不要求 status===ppt_confirmed(材料先行导致状态停在 materials_draft/completed 时,只要确认过 PPT 仍能导出——与面板 979-980 行说明一致)。 |
| 缺了会怎样 | 未确认 PPT:400 need_ppt_confirmed。硬约束失败:400,带 issues。 |
| 修复建议 | 导出按钮在 !confirmed.ppt.slides 时禁用,避免空点。 |
| 验证方式 | ppt_confirmed 或 materials_draft 且 meta ppt_confirmed=true 时导出 201;仅 draft PPT 应 400。 |
2.2 plan_docx(已实证)
| 字段 | 内容 |
| 判定 | 可行 |
| 证据 | 只走弱教案闸 run.ts:140-142, 322-348。用 confirmed plan。不要求材料/PPT。 |
| 缺了会怎样 | 从未确认教案:400 need_plan_confirmed。 |
| 修复建议 | 弱闸改为「checkG1(plan) 通过」或 statusRank>=plan_confirmed,避免空 objectives:[] 漏网。 |
| 验证方式 | plan_confirmed 后导出 201(已实证)。 |
2.3 materials_teacher / materials_student
| 字段 | 内容 |
| 判定 | 有风险(不强制「已确认材料」;未确认按草稿导出并警告) |
| 证据 | needMaterials 含二者:run.ts:95-103。无任何材料:run.ts:349-354 400 need_materials。有 draft 无 confirmed:run.ts:147-148 warnings.push("材料尚未确认,本次按草稿导出"),仍写 docx。教师版含答案、学生版隐藏:materials-docx.ts:59-70。不要求 completed。 |
| 缺了会怎样 | 无材料:400。未确认教案:若 confirmed 无 plan,先被 need_plan_confirmed 拦住。未确认材料:成功 + warning。 |
| 三类课时 | 讲授课/实验课只要材料生成成功即可导出,不必先完成备课。案例课同。 |
| 修复建议 | 若产品要求「确认后才导出」,在 needMaterials 分支改为「无 confirmed 材料则 400」,draft 仅警告不够。UI 按钮按 bundle.materials 禁用。 |
| 验证方式 | materials_draft 导出教师版 → 201 且 warnings 含「尚未确认」。completed 后再导出 → 无该警告。无材料 → 400 need_materials。 |
2.4 question_bank
| 字段 | 内容 |
| 判定 | 有风险(有材料就成功;无习题也成功,得到空表) |
| 证据 | run.ts:372-398:requireMaterials() 后写 4 个文件:CSV(P7 列)、xlsx、导入说明、顺带一份 activity_list。行数据只来自 materials.exercises:question-bank.ts:18-37, 75-107。cases 不进题库。不要求材料已确认(draft 回落 + 警告,同 147-148)。不要求 PPT。 |
| 缺了会怎样 | 无材料对象:400 need_materials。有材料无习题:201,CSV 仅表头、xlsx「题库」表只有表头。未确认教案:可能先 400 need_plan_confirmed。 |
| 修复建议 | exercises.length===0 时 400 empty_question_bank 或强制 warning。UI 提示「题库来自材料习题,案例讨论题不入库」。 |
| 验证方式 | 有习题:xlsx 行数 = 1+N。无习题有案例:201 但行数=1(仅表头)。 |
2.5 question_bank_xlsx
| 字段 | 内容 |
| 判定 | 有风险(同 2.4) |
| 证据 | run.ts:399-419:xlsx + 学习通 CSV + 导入说明。不附带 activity_list(与 question_bank 不同)。同样 requireMaterials(),同样不查习题是否为空。 |
| 缺了会怎样 | 同 2.4。 |
| 修复建议 | 同 2.4。 |
| 验证方式 | 与 2.4 对照文件集合:本类型无 activity_list_txt。 |
2.6 activity_list
| 字段 | 内容 |
| 判定 | 可行(只要求已确认教案;与五步勾选无关) |
| 证据 | 不在 needMaterials():run.ts:95-103, 420-432。写活动清单 + 课件包清单。清单正文 question-bank.ts:253-320:目标来自 confirmed plan;讨论/作业/测验可来自 lesson_events 与 可选 materials。无材料时各类可出现「本课教案未单独列出,可按资料上传或跳过」(310-311)。不读 chaoxing_five_steps。 |
| 缺了会怎样 | 未确认教案:400 need_plan_confirmed。未生成材料:仍成功,活动建议偏空。未勾五步:仍成功。 |
| 修复建议 | 保持「照做清单不依赖勾选」。UI 标明「不要求材料/五步」。若希望清单饱满,在无材料时 warning。 |
| 验证方式 | plan_confirmed 且无材料:201,正文含章节目录与占位活动。completed 且有习题:测验/作业条数随 exercises 增加。 |
2.7 toolkit(一键交换物包)
| 字段 | 内容 |
| 判定 | 有风险(须材料;内含空题库仍打包成功;含五步记录快照) |
| 证据 | run.ts:433-466:requireMaterials() 后逐个 save xlsx/csv/学习通 csv/guide/activity_list/课件包清单/chaoxing_checklist,再 zip。checklist 来自 parseChaoxingSteps(gen.chaoxing_five_steps)(run.ts:222-233, 441),全未勾也能导出(记录里写「未勾选」)。不要求 PPT、不要求 completed、不要求材料已确认。 |
| 缺了会怎样 | 无材料:400 need_materials。未勾五步:zip 内仍有「-五步勾选记录.txt」。无习题:题库文件空表但仍进包。 |
| 修复建议 | 与题库同样处理空习题。产品若要求「完成备课才出交换物」,加 status===completed。 |
| 验证方式 | materials_draft:201 + warning「尚未确认」。无材料:400。zip 内应有 7 个成员文件 + toolkit 自身记录。 |
2.8 前置校验对照表
| 导出 type | 须已确认教案 | 须已有材料 | 须已确认材料 | 须已确认 PPT | 缺前置 |
ppt | 弱闸 | 否 | 否 | 是(confirmed slides + schema) | need_plan_confirmed / need_ppt_confirmed / ppt_hard_constraint |
plan_docx | 弱闸 | 否 | 否 | 否 | need_plan_confirmed |
materials_teacher | 弱闸 | 是(draft 可) | 否(警告) | 否 | need_materials |
materials_student | 弱闸 | 是(draft 可) | 否(警告) | 否 | need_materials |
question_bank | 弱闸 | 是 | 否(警告) | 否 | need_materials;空习题仍 201 |
question_bank_xlsx | 弱闸 | 是 | 否(警告) | 否 | 同上 |
activity_list | 弱闸 | 否 | 否 | 否 | 仅 need_plan_confirmed |
toolkit | 弱闸 | 是 | 否(警告) | 否 | need_materials |
3. 教研样例 publish-sample / research_samples / 脱敏
3.1 发布闸门与落库
| 字段 | 内容 |
| 判定 | 可行(须 completed);若干 边角失败 见下 |
| 证据 | API:src/app/api/lesson-plans/[id]/publish-sample/route.ts:38-48。核心:src/lib/samples/publish.ts:47-163。非 completed → 400「仅已完成(completed)教案可发布为教研样例」(73-75)。非本人/非 admin → 403(70-72)。samplePublishOptOut → 403(76-78)。个人偏好关闭且非 admin → 403(79-82)。已 revoked → 409,教师不能自行再发布(83-85)。快照:confirmed 优先、plan/ppt/materials 可回落 draft(31-44)。source_lesson_plan_id 唯一(prisma/schema.prisma ResearchSample),再发布走 update 不是 create(134-144)。UI:sample-publish-card.tsx:32, 106-116 按钮 disabled={!completed || optOut || !allowPublishSample}。 |
| 修复建议 | 409 文案在 UI 展示「联系管理员恢复」。completed 提示旁加「须先确认材料」。撤销后若允许教师覆盖,去掉 83-85 或改为 admin-only restore 入口(已有 reviewResearchSample restore,publish.ts:199-215)。 |
| 验证方式 | ppt_confirmed/materials_draft 点发布:按钮灰,API 400。completed 且允许发布:201,research_samples 一行,sanitizedContent 无班级名。再 POST:200 created=false。admin revoke 后再 POST:409。 |
3.2 脱敏 sanitize 边角
| 字段 | 内容 |
| 判定 | 有风险(命中则 422,发布失败;内容含教师短名/用户名/残留 URL 时) |
| 证据 | sanitizeValue + assertNoPersonalInfo:publish.ts:95-104,实现 src/lib/samples/sanitize.ts:294-354。班级名/教师名/学号/画像桶/URL 会替换;替换后若 findPersonalInfo 仍命中则抛错 → 422。风险点:① buildTeacherAliases 把 username 长度≥4 且非 admin 加入别名(sanitize.ts:107-108),教案正文若出现该登录名(如 test、demo)会 422。② 教师 displayName 两字且与知识点常用词撞车。③ strip_personal_links:false 时 citations 的 https:// 会被 findPersonalInfo 当成「个人链接」(336-341);UI 固定传 true(sample-publish-card.tsx:63),直打 API 传 false 会踩。④ 质量分只写入字段,不是闸门(publish.ts:110, 124-125 + quality.ts)。 |
| 修复建议 | username 别名加最小长度/中文名限制;422 响应把 hits 带回 UI。发布闸不要用「全文 includes(username)」。 |
| 验证方式 | 已实证脚本:scripts/test-s3c.ts / demo-s3c.ts。补测:把教师 username 写进 class_profile_summary 再发布,预期今日 422。 |
4. 「去学习通五步」与导出 activity_list
| 字段 | 内容 |
| 判定 | 可行(两者解耦,不是死路);有风险(教师会以为勾选/导出互为前置) |
| 证据 | 勾选持久化:generation_meta.chaoxing_five_steps,src/lib/prepare/chaoxing-steps.ts:1-4, 38-61;API src/app/api/lesson-plans/[id]/chaoxing-steps/route.ts:35-72。确认/再生成用 parseGenMeta 整包写回(persist.ts:53-79, bumpMetaOnConfirm 111-124),不会抹掉五步。activity_list 渲染不读勾选(question-bank.ts:253-320),只在文末打印五步标题常量(317 行,来自 status.ts:73-114)。toolkit 才把勾选快照写成 chaoxing_checklist(run.ts:441, 449)。UI 卡片出现条件:① 任意页签顶部,仅 status==="completed"(lesson-dev-panel.tsx:655-661);② 导出页、材料页,仅 status==="materials_draft"(929-935, 1004-1010)。ppt_confirmed 且尚未生成材料时,界面上没有五步卡片——此时仍可导出 activity_list。勾选不是导出前置,导出也不是勾选前置。 |
| 修复建议 | 卡片改为 statusRank>=plan_confirmed 即显示(与「对照活动清单去勾」一致)。导出页用一句话写清:「活动清单=照做文本;五步=教师核对;互不阻断。」 |
| 验证方式 | plan_confirmed 导出 activity_list 应 201。completed 勾选 ①② → GET chaoxing-steps 为 done;再导出 toolkit,checklist 对应行「已勾选」。全不勾导出 toolkit 仍 201。 |
5. 实验课时(现已有 objectives)与题库是否为空
5.1 教案 / PPT / 材料导出是否顺畅
| 字段 | 内容 |
| 判定 | G1:可行(课时目标已录入)。生成教案:死路(UI 硬编码案例课型,见 1.4)。绕开 G2 后教案确认/PPT/材料:可行,PPT 有讨论课硬约束 有风险。 |
| 证据 | G1 只看 objectives.length≥1:src/lib/prepare/g1.ts:18-32,确认教案/材料都走(confirm/route.ts:67-74)。整学期内容已声明实验课 3–5 条目标(整学期备课-验收示范说明.md)。实验课 isCaseLessonType("实验")===false(g2.ts:14-27),本不应要主案例。材料生成提示词仍写「习题与案例讨论」(generate.ts:523-524),实验课也会被要求出 exercises。PPT:若模型把 lesson_type 写成含「讨论/案例」(UI 又在请求里写死「案例讨论」),schema.ts:274-277, 384-395 强制争议页+记录页,实验课 PPT 可能 ppt_hard_constraint 不得确认。导出教案/材料不依赖课型。学时 2,PPT 上限 16 页(types.ts:53)一般够。 |
| 修复建议 | 同 1.4 去掉课型硬编码。材料 userExtra 对 lesson_type 含「实验」改为「实验步骤/记录表/思考题,exercises 至少 2 道(含 1 道 recall 以便题库)」。PPT isDiscussionLesson 不要被请求参数污染。 |
| 验证方式 | 实验1:lessonType:"实验" 生成教案 → 确认(G1 通过)→ 生成 PPT/材料 → 导出 plan_docx/ppt/materials_student。用 UI 默认「案例讨论」应 G2 失败(对照)。 |
5.2 question_bank 必须材料里有习题才不空
| 字段 | 内容 |
| 判定 | 有风险(导出不失败,但是空题库;实验课若只有 cases/操作步骤则必空) |
| 证据 | question-bank.ts:18-20, 75-76 只 map exercises。确认材料 UI 允许「仅 cases 无 exercises」(lesson-dev-panel.tsx:419-423)——此时可 completed,题库仍空。实验课 authentic_task 无 options 会进简答题列(question-bank.ts:57-61),学习通仍能导入,但 recall 单选可能为 0。 |
| 修复建议 | 材料 schema:exercises.length≥1。实验课生成至少 1 道 recall。空题库导出 400 或显著 warning。 |
| 验证方式 | 导出后数 xlsx「题库」数据行;实验1 若 N=0 记失败(产品口径),今日代码会当成功。 |
6. 额外发现(会使某步不可行)
6.1 生成任务 persist 失败仍标 done
| 字段 | 内容 |
| 判定 | 有风险(偶发假成功:UI 说已生成,草稿没有,确认按钮一直灰) |
| 证据 | src/lib/ai/tasks.ts:106-128:persist catch 只 console.error,随后仍把 task 写成 done。 |
| 修复建议 | persist 失败则 task=failed,不要 done。 |
| 验证方式 | 单测 mock persist throw,期望 task.status=failed。 |
6.2 确认 PPT 没有「半执行」保护(材料确认有)
| 字段 | 内容 |
| 判定 | 有风险 |
| 证据 | 材料确认专门拒绝 RANK < materials_draft,避免「写入 confirmed 但状态不前进」(confirm/route.ts:105-114 注释)。PPT 确认 没有 对等闸:status=plan_draft 但 draft 里还有旧 ppt 时,UI 按钮只看 bundle.ppt 与 schema(lesson-dev-panel.tsx:808-813),API 会把 ppt 写入 confirmed,nextStatus 因 RANK < ppt_draft 保持 plan_draft(state.ts:67-68)。 |
| 修复建议 | 与材料对称:RANK < ppt_draft 时 400 need_ppt_draft。 |
| 验证方式 | completed→重生成教案=plan_draft→不确认教案直接点「确认 PPT」:今日可能改 confirmed.ppt 而 status 仍 plan_draft。 |
6.3 flagged 材料/教案不阻断确认
| 字段 | 内容 |
| 判定 | 有风险(与文案「不得直接确认」不符) |
| 证据 | 生成成功文案:lesson-dev-panel.tsx:253-256「已生成但存在标红,不得直接确认」。确认教案只拦 G1(311-316);确认材料只拦 G1+空材料;都不看 flagged / 材料 schema。后端确认材料不调用 validateGenerationPayload("materials")。PPT 确认则拦 schema(confirm/route.ts:85-95,lesson-dev-panel.tsx:372-377)。 |
| 修复建议 | 材料确认复用 PPT 模式:schema invalid → 400。教案确认至少把 flagged 当强警告(产品已用质量六条作非硬阻断,需在文案上统一)。 |
| 验证方式 | 生成材料缺 objective_ids(schema 429-454 行会标红)→ 任务 flagged → 今日仍可确认到 completed。 |
6.4 导出按钮与校验不同步
| 字段 | 内容 |
| 判定 | 有风险(不是死路,是 UI 撒谎) |
| 证据 | lesson-dev-panel.tsx:904-927 vs run.ts 各 need_*。 |
| 修复建议 | 按 §2.8 表禁用并 title 写 code。 |
| 验证方式 | plan_draft 点「教师版材料」「题库」「toolkit」应灰;今日可点,返回 400。 |
6.5 无「跳过材料完成备课」通道
| 字段 | 内容 |
| 判定 | 有风险(讲授课若认为不需要材料,则永远不能 completed,也就不能发布样例) |
| 证据 | complete_removed:confirm/route.ts:116-124。publish.ts:73-75 只认 completed。 |
| 修复建议 | 若讲授课允许无材料完成:confirm 增加 step=complete 且课型非案例,或发布闸改为 plan_confirmed && (materials 已确认 || 非案例型)。若产品坚持「完成=有材料」,保持现状并在 UI 写死「三类课时都必须生成材料」。 |
| 验证方式 | 讲授课只确认教案+PPT:发布按钮保持灰(今日如此)。 |
6.6 占位章 G3(当前学期内容已灌满则不是死路)
| 字段 | 内容 |
| 判定 | 当前 29 课时内容库:可行。若有人重跑旧 seed/course.ts 占位章:死路 |
| 证据 | G3:g3.ts:27-56 + teachable.ts:9-35(description < 40 字拒生成)。generate/route.ts:81-95 对 plan/ppt/materials 都查。旧占位:seed/course.ts:173-218 知识点 description 空。当前验收说明:29 课时 G1/G3 全绿。 |
| 修复建议 | 禁止对占位章点生成;知识点页面对短 description 给同一句 G3 文案。 |
| 验证方式 | 抽一课时 POST generate,期望不再 g3_short_description。 |
《逐课时验收脚本》
环境:演示班「2024级演示班(通识)」或「2024级法学1班」;Cookie 登录 admin。课时 id 不要写死(seed/current-structure.json 的 id 会随导入变化)。先解析:
# 0) 登录拿 Cookie(按项目实际 login API)
# 1) GET /api/classes → 记 CLASS_ID
# 2) GET /api/lesson-plans?classId=$CLASS_ID (已有 plan 时带 status)
# 3) 备课页 SSR 的 wizard.lessons 或:GET /api/chapters?courseId=… 再 GET /api/lessons?chapterId=…
# 按 title 匹配下表代表课时,记下 LESSON_ID;若尚无 plan,生成后用返回/列表拿 PLAN_ID
代表课时(8 章各 1 + 实验 1;第 8 章第 1 课时 plan_docx/ppt 已实证,本脚本从材料段接上):
| 代号 | 章 | 代表课时 title 含 | 课型口径 | 主案例 |
| L0 | 第0章 计算思维与计算机基础 | 计算思维(0.1–0.3) | 讲授 | 无 |
| L1 | 第1章 人工智能概述 | 人工智能概述(1/2) | 案例 | DeepSeek-R1 |
| L2 | 第2章 Python程序设计基础 | Python程序设计基础(1/4) | 讲授 | 无 |
| L5 | 第5章 计算机视觉 | 计算机视觉技术与应用(1/3) | 案例 | 香港换脸诈骗 |
| L6 | 第6章 智能语音 | 智能语音处理与应用(1/3) | 案例 | AI 声音人格权 |
| L7 | 第7章 NLP | 自然语言处理与应用(1/2) | 案例 | 流浪狗舆情 |
| L8 | 第8章 生成式大模型 | 生成式大模型简介 | 案例 | 文生图著作权(已实证到 ppt/plan_docx) |
| E1 | 实验篇 | 实验1 Python程序设计基础实验 | 实验 | 无 |
课型参数(绕开 1.4 死路): 讲授传 "讲授",案例传 "案例讨论" 且 body 带已选 caseId,实验传 "实验"。对照坑:UI 默认 "案例讨论" 且无 caseId 时 L0/L2/E1 应 400 g2_no_main_case(记为已知死路,不挡案例课验收)。
公共 API 序列(每个代表课时)
设 $C=CLASS_ID $L=LESSON_ID $T=课型 $CASE=主案例id或省略。
# A. 教案(L8 若已 plan_confirmed / ppt_confirmed 则从 C 接)
POST /api/lesson-plans/generate
{classId:$C, lessonId:$L, step:"plan", lessonType:$T, caseId:$CASE, run:true}
→ 201 task.id ;轮询 GET /api/tasks/:id 至 done|failed(≤90×2s,与面板 pollTask 一致)
期望:done 且 result.payload.objectives.length≥1;failed+g2_no_main_case 仅允许出现在「未传正确课型/无主案例」对照。
GET /api/lesson-plans?classId=$C → 该课 status=plan_draft,记下 planId=$P
PUT /api/lesson-plans/$P/confirm {step:"plan"}
期望:200,plan.status=plan_confirmed;400 g1_no_objectives 视为该课内容库失败(当前学期不应出现)。
# B. PPT(可与 C 调换顺序)
POST /api/lesson-plans/generate {classId:$C, lessonId:$L, step:"ppt", run:true}
→ 轮询 done
PUT /api/lesson-plans/$P/confirm {step:"ppt"}
期望:schema.valid 时 200;status 为 ppt_confirmed,或已是 materials_draft/completed 则不降级。
schema invalid → 400 ppt_hard_constraint,不得进入「已确认 PPT」。
POST /api/lesson-plans/$P/export {type:"ppt"} 期望 201(已实证)
POST /api/lesson-plans/$P/export {type:"plan_docx"} 期望 201(已实证)
# C. 材料 → completed(本审查重点)
POST /api/lesson-plans/generate {classId:$C, lessonId:$L, step:"materials", run:true}
→ 轮询 done
期望:status=materials_draft(若已 completed 则仍 completed,见 1.3);
draft.materials.exercises 为数组。E1 额外断言 exercises.length≥1(否则题库空,记有风险)。
PUT /api/lesson-plans/$P/confirm {step:"materials"}
期望:200,status=completed。
对照:status=ppt_confirmed 且从未 generate materials → 400 need_materials_draft。
对照:{step:"complete"} → 400 complete_removed。
# D. 其余导出(均 POST /api/lesson-plans/$P/export {type})
type=materials_teacher 期望 201;若确认前导出,warnings 含「材料尚未确认」
type=materials_student 期望 201
type=question_bank 期望 201;下载 csv/xlsx,数据行 = exercises.length(E1 行数=0 则记缺陷)
type=question_bank_xlsx 期望 201;文件集合无 activity_list(与 question_bank 区分)
type=activity_list 期望 201(即使未勾五步);正文含本章课时目录与「照做顺序」
type=toolkit 期望 201;含 chaoxing_checklist;无材料时应 400 need_materials(本步已有材料)
# E. 学习通五步(不阻断 D)
PUT /api/lesson-plans/$P/chaoxing-steps {step:"align_chapter", done:true}
… 五步均可
GET 同上,期望 done/at
再 export toolkit,checklist 对应行「已勾选」
对照:五步全 false 时 activity_list 仍 201
# F. 发布样例
POST /api/lesson-plans/$P/publish-sample {strip_personal_links:true}
期望:201/200,sample.status=published;sanitizedContent 无真实班级名/教师名。
对照:在 materials_draft 时调用 → 400。
对照:opt-out 后 → 403。
分课时期望状态
| 课时 | 应用课型 $T | 生成教案 | 确认教案 | PPT 确认 | 材料确认 | 导出题库 | 发布样例 |
| L0 讲授 | 讲授 | plan_draft | plan_confirmed | 可 | completed | 行数≥1 为佳 | 201 |
| L1 案例 | 案例讨论+case | 同上 | 同上 | 讨论课须有争议/记录页 | 同上 | 习题+案例都有 | 201 |
| L2 讲授 | 讲授 | 同上 | 同上 | 可无争议页 | 同上 | 同 L0 | 201 |
| L5 案例 | 案例讨论+case | 同上 | 同上 | 同 L1 | 同上 | 同上 | 201 |
| L6 案例 | 案例讨论+case | 同上 | 同上 | 同 L1 | 同上 | 同上 | 201 |
| L7 案例 | 案例讨论+case | 同上 | 同上 | 同 L1 | 同上 | 同上 | 201 |
| L8 案例 | 已实证到 ppt/plan_docx | 跳到 C | 已 plan_confirmed | 已 ppt_confirmed | 本步必须新跑 | 新跑 | 新跑 |
| E1 实验 | 实验 | 绕开 G2 后 plan_draft | G1 应过 | 勿被「案例讨论」schema 误杀 | completed | 断言习题非空 | 201 |
UI 对照(每课至少抽 1 次,建议 L0 + L1 + E1):
- 材料页「确认材料(完成备课)」仅在
materials_draft 且有 bundle.materials 时可点。
completed 后页顶出现五步卡片;ppt_confirmed 无材料时不应依赖五步才能导出活动清单。
- 发布按钮仅
completed 可点。
- 讲授课/实验课未选主案例时,用 UI 默认生成 应失败(死路 1.4);用正确
$T 的 API 应成功。
修复优先级(建议)
- P0 死路:去掉
lessonType: "案例讨论" 硬编码(面板 240 行 + generate route 71 行),否则无主案例讲授/实验永远到不了材料段。
- P0 完成态:确认材料的状态准入不要死盯
=== materials_draft(重生成教案后假死)。
- P1:空
exercises 不得静默导出题库;材料确认前后端空材料规则对齐。
- P1:persist 失败不要标
done;PPT 确认补半执行闸。
- P2:导出按钮按
need_* 禁用;五步卡片在 plan_confirmed 即显示;发布 422/409 把原因打到面板。
材料确认主路径代码是通的,真正会卡死「备课→上课」的是 UI/G2 把所有课当成案例课、以及确认按钮死盯 materials_draft。 题库/toolkit 缺材料会 400,缺习题却会空文件成功;activity_list 与五步勾选互不阻断。 实验课已有 objectives,G1 不再是死路,但必须显式传课型 实验 并保证材料里有 exercises,否则完成备课后题库仍空、样例也发不出去。