42 KiB
备课模块审计报告 v4
审计日期:2026-06-25 审计范围:
src/modules/lesson-preparation/全部 45 个文件 +src/app/(dashboard)/{teacher,admin,student,parent}/lesson-plans/共 27 个路由文件 审计依据:e:\Desktop\CICD\.trae\rules\project_rules.md、docs/architecture/004_architecture_impact_map.md§2.27、docs/architecture/005_architecture_data.jsonmodules.lesson_preparation 前序报告:lesson-preparation-audit-report-v3.md(2026-06-24) 审计方法:三路并行子代理深度代码审计(actions/data-access 层 + 组件/hooks/lib 层 + app 路由/i18n 层)
一、现有实现概要
1.1 文件分布(45 + 27 = 72 个文件)
| 层级 | 文件数 | 主要文件(行数实测) |
|---|---|---|
| types/schema/constants | 3 | types.ts(345) / schema.ts(81) / constants.ts(106) |
| data-access | 4 | data-access.ts(606) / data-access-versions.ts(201) / data-access-templates.ts(130) / data-access-knowledge.ts(138) |
| actions | 4 | actions.ts(401) / actions-publish.ts(89) / actions-ai.ts(47) / actions-kp.ts(51) |
| services | 3 | publish-service.ts(222) / ai-suggest.ts(84) / services/default-data-service.ts(165) |
| lib | 6 | type-guards.ts(241) / i18n-errors.ts(50) / document-migration.ts(330) / anchor-injector.ts(304) / node-summary.ts(136) / rf-mappers.ts(186) |
| config | 1 | block-registry.tsx(198) |
| providers | 2 | lesson-plan-provider.tsx(338) / lesson-plan-provider-setup.tsx(29) |
| hooks | 1 | use-lesson-plan-editor.ts(319) |
| components | 21 | 7 业务组件 + 4 nodes + 11 blocks 中 + block-renderer.tsx(@deprecated) |
| seed | 1 | seed-templates.ts(9) |
| app 路由 | 27 | 9 个 page.tsx + 9 loading.tsx + 9 error.tsx + 1 slot.tsx |
行数合规性:所有文件均 ≤ 800 行(actions/data-access 上限),≤ 500 行(组件上限),未触发 1000 行硬性上限。
1.2 数据流
app/(dashboard)/{role}/lesson-plans/page.tsx (Server Component)
├─ getAuthContext() → data-access.getLessonPlans({}, scope, userId) → DB
└─ getTranslations("lessonPreparation") → 渲染
↓
LessonPlanProviderSetup (Client Component 包装)
└─ LessonPlanProvider (Context 注入 service + roleConfig + tracker)
↓
LessonPlanList / LessonPlanCard / LessonPlanEditor
└─ useLessonPlanContextSafe().service.getLessonPlans(...)
↓
default-data-service → Server Actions
└─ requirePermission() → data-access → DB
1.3 架构图完整性核对
docs/architecture/004_architecture_impact_map.md§2.27 记录的导出函数、文件清单、依赖关系与实际代码完全一致docs/architecture/005_architecture_data.jsonmodules.lesson_preparation.auditFixes 已记录 P0-1 至 V3-46 共 60+ 项修复- V3 已落地修复:跨模块直查(P0-1)、i18n 接入(P0-2)、DataScope 过滤(P0-3)、
as断言清零(V2-3/V3-17/V3-22)、组件完全通过 service 调用(V3-16/V3-20)、a11y + 类型守卫(V3-7/V3-8)、findChapterById 共享(V3-42)、useCallback性能优化(V3-44)等 - 本审计未发现新的架构图遗漏节点,但建议在 auditFixes 增补 v4 新增条目(详见第五章)
二、现存问题与原因分析
2.1 安全与权限问题(P0)
问题 1:Parent/Student 只读路由未校验 gradeId 跨年级归属 — 信息泄露漏洞
-
位置:
src/app/(dashboard)/parent/lesson-plans/[planId]/view/page.tsxL21-31、src/app/(dashboard)/student/lesson-plans/[planId]/view/page.tsxL21-41 -
问题:两个路由仅校验
plan.status === "published",完全未校验plan.gradeId是否在ctx.dataScope.gradeIds范围内 -
直接后果:任何家长/学生通过修改 URL 中的
planId(仅校验 published),即可查看其他年级已发布课案 -
数据访问层明示警示:data-access.ts L323-326 注释明确写道:
安全说明:此函数仅校验 creator 或 published 状态。对于 parent/student 角色,调用方(页面层)必须额外校验 plan.gradeId 是否在 ctx.dataScope.gradeIds 范围内,防止跨年级信息泄露。
页面层未补齐该校验,形成显式的"已知漏洞"
-
违反规则:项目记忆"Parent routes must include permission checks with both parentId and studentId to prevent information leakage"
问题 2:9 个 lesson-plans 页面层全部缺失 requirePermission() 调用
- 位置:所有
app/(dashboard)/{teacher,admin,student,parent}/lesson-plans/**/page.tsx - 问题:全部 9 个页面仅调用
getAuthContext()获取上下文,未调用requirePermission(Permissions.LESSON_PLAN_READ) - 对比:本仓库
app/(dashboard)/{announcements,attendance,messages,textbooks,exams,...}等 100+ 处页面均显式调用requirePermission(),本模块属唯一例外 - 直接后果:依赖 data-access 层 scope 过滤兜底;如未来 data-access 出现 scope 缺失(如新增查询函数忘记加 scope),页面层无二次防线
- 违反规则:项目规则 "All Server Actions must call
requirePermission()for permission verification" + "Frontend permission checks must useusePermission().hasPermission()"
问题 3:textbook-segments.tsx 锚点段落键盘用户完全不可操作 — a11y 严重缺陷
- 位置:textbook-segments.tsx L47、L76
- 问题:锚点 span 仅绑定
onClick,无role="button"/tabIndex={0}/onKeyDown - 直接后果:键盘用户、读屏器用户完全无法触发锚点跳转/高亮,违反 WCAG 2.1 AA 的 2.1.1 键盘可访问性
- 违反规则:项目规则 "a11y:语义化标签、ARIA 属性、键盘导航"
2.2 架构与解耦问题(P0/P1)
问题 4:default-data-service.ts 直接 import 其他业务模块的 actions
- 位置:default-data-service.ts L21
import { getQuestionsAction } from "@/modules/questions/actions" - 问题:项目规则要求"模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access(只能通过注入的接口调用)"。虽收口在 service 实现层而非组件直接 import,但违反了"完全解耦"原则——本模块对
questions/actions形成编译期硬依赖 - 直接后果:
questions模块 actions 签名变更会破坏本模块编译;无法在不引入 questions 模块的情况下独立测试本模块 - 违反规则:审计任务要求 "完全解耦:模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access(只能通过注入的接口调用)"
问题 5:4 个对话框/抽屉缺失 FocusTrap — 焦点陷阱不完整
- 位置:
- publish-homework-dialog.tsx 全文
- question-bank-picker.tsx L117-119
- knowledge-point-picker.tsx L77-79
- version-history-drawer.tsx L87-89
- 问题:4 个组件均使用
role="dialog"+aria-modal="true",但未使用 FocusTrap 包裹。仅inline-question-editor.tsxL82 正确使用了<FocusTrap> - 直接后果:Tab 键焦点可逃逸到对话框背后的页面,违反 WAI-ARIA Dialog 模式
- 违反规则:项目规则 "a11y:键盘导航"
问题 6:4 个对话框/抽屉 ESC 键关闭未实现
- 位置:同问题 5 的 4 个组件
- 问题:仅有遮罩点击关闭 (
onClick={onClose}),未监听keydownESC 键 - 直接后果:用户只能点击关闭按钮,无键盘快捷关闭路径
- 违反规则:项目规则 "a11y:键盘导航"
问题 7:未使用 React Suspense + 骨架屏(项目规则明确要求)
- 位置:全模块客户端组件
- 问题:项目规则要求"异步数据使用 React Suspense + 骨架屏"。本模块在
lesson-plan-skeleton.tsx定义了 4 个骨架屏组件(VersionListSkeleton/QuestionBankSkeleton/KnowledgePointSkeleton/LessonPlanListSkeleton),但这些骨架屏仅被loading.tsx文件使用(路由级 Suspense),未在客户端组件数据加载时使用 - 现状:客户端组件用本地
isLoadingstate + 文本"加载中..."代替骨架屏 - 直接后果:组件级异步加载(如打开 version-history-drawer)显示纯文本而非骨架,UX 不一致
- 违反规则:项目规则 "异步数据使用 React Suspense + 骨架屏"
问题 8:Error Boundary 覆盖不全
- 位置:
- lesson-plan-editor.tsx — NodeEditor 画布未被 Error Boundary 包裹(仅 NodeEditPanel 内的 BlockRenderer 被包裹)
- version-history-drawer.tsx 全文未包裹
- publish-homework-dialog.tsx 全文未包裹
- knowledge-point-picker.tsx 全文未包裹
- question-bank-picker.tsx 全文未包裹
- 问题:项目规则要求"每个独立的数据区块必须用 React Error Boundary 包裹"。当前仅
node-edit-panel.tsxL161-173 包裹BlockRenderer,其他独立数据区块无兜底 - 直接后果:单个 Picker 数据加载失败会冒泡到路由级
error.tsx,整个页面变白屏 - 违反规则:项目规则 "每个独立的数据区块必须用 React Error Boundary 包裹"
2.3 类型安全与代码质量(P1/P2)
问题 9:i18n-errors.ts L24 无注释的 as 断言
- 位置:i18n-errors.ts L24
t(msg as Parameters<typeof t>[0]) - 问题:从
string收窄到next-intl的 i18n key 联合类型,无注释说明 - 违反规则:项目规则 "禁止
as断言(除非从unknown转换或测试中,需注释原因)"
问题 10:硬编码颜色字符串散落多处,未使用 CSS 变量
- 位置:
- node-editor.tsx L228
stroke: "#1976d2"、L237color="#ccc" - lesson-node.tsx L43
borderColor: selected ? "#1976d2" : color、L44boxShadowrgba - textbook-content-node.tsx L319
borderColor: selected ? "#1976d2" : "#455a64"、L329backgroundColor: "#455a64"、L386backgroundColor: "#1976d2" - node-summary.ts L118-132
NODE_COLORS13 个 hex 颜色
- node-editor.tsx L228
- 问题:V3 已修复 MiniMap 颜色硬编码(V3-18),但节点本身、画布背景、NODE_COLORS 仍硬编码
- 违反规则:项目规则 "设计令牌在 src/app/globals.css 中使用 CSS 变量定义"
问题 11:exercise-block.tsx 使用原生 <a> 标签
- 位置:exercise-block.tsx L123-128
<a href="/teacher/homework">...</a> - 问题:未使用 Next.js
<Link>,导致整页刷新 - 违反规则:项目记忆 "Link navigation must use Next.js
<Link>component instead of raw<a>tags"
问题 12:6 个 block 组件使用 key={idx} 列表 key
- 位置:
homework-block.tsxL46 /key-point-block.tsxL46 /new-teaching-block.tsxL51 /objective-block.tsxL49 /reflection-block.tsxL46 /summary-block.tsxL41 - 问题:列表项增删时 React key 不稳定,可能引发状态错乱
- 违反规则:React key 稳定性最佳实践(隐含在"代码质量规则"中)
问题 13:template-picker.tsx 字符串拼接动态类名
- 位置:template-picker.tsx L265-269、L296-300
className={`... ${selected === tpl.id ? "border-primary ..." : "border-transparent ..."}`} - 问题:违反"禁止字符串拼接动态类名"规则(应使用
cn()) - 违反规则:项目规则 "禁止字符串拼接动态类名(
bg-${color}-500)" + "使用cn()工具函数管理条件类名" - 波及范围:本模块全部 21 个组件均未使用
cn(),全部用模板字符串拼接,但template-picker.tsx的条件分支最复杂
问题 14:block-registry.tsx 缺失 "use client" 指令
- 位置:block-registry.tsx L1
- 问题:
BlockRenderer函数(L88)返回 ReactElement 并 switch 渲染各 client block 组件,本文件应明确标注"use client" - 违反规则:项目规则 "需要交互时才添加
\"use client\"(必须位于文件第一行)"
问题 15:NodeEditPanel.knownTypes 与 BLOCK_REGISTRY 重复定义(违反 DRY)
- 位置:node-edit-panel.tsx L241
- 问题:本地维护
knownTypes数组与BLOCK_REGISTRY的 keys 完全重复 - 违反规则:项目规则隐含的 DRY 原则
问题 16:inline-question-editor.tsx 的 QUESTION_TYPES 与 type-guards.VALID_QUESTION_TYPES 重复
- 位置:inline-question-editor.tsx L21、type-guards.ts L184
- 问题:
inline-question-editor仅定义 3 种题型子集,type-guards已定义完整 5 种,应复用并 filter
问题 17:ROLE_CONFIGS 缺失 gradeHead 角色,与 LessonPlanCard.viewMode 不一致
- 位置:lesson-plan-provider.tsx L255-260
ROLE_CONFIGSvs lesson-plan-card.tsx L50viewMode类型包含"gradeHead" - 问题:组件类型支持 gradeHead 视图,但 Provider 配置表无对应配置
- 直接后果:gradeHead 用户访问时会静默回退到 TEACHER_ROLE_CONFIG,可能误显示教师操作按钮
问题 18:useRoleConfig 静默回退教师配置,无开发环境告警
- 位置:lesson-plan-provider.tsx L319-322
- 问题:Provider 外调用
useRoleConfig()时静默回退到TEACHER_ROLE_CONFIG,无 console.warn - 直接后果:开发者误用时不报警,可能在生产环境暴露教师操作按钮给非教师角色
2.4 性能与代码组织(P2)
问题 19:3 个组件用 Promise.resolve().then() / queueMicrotask 规避同步 setState
- 位置:
- 问题:可读性差,应使用
useEffect内 async + ignore flag 模式
问题 20:3 个 view 页 textbook/chapters 串行查询,可并行化
- 位置:
parent/lesson-plans/[planId]/view/page.tsx、student/lesson-plans/[planId]/view/page.tsx、admin/lesson-plans/[planId]/view/page.tsxL37-46 - 问题:textbook + chapters 查询彼此独立(依赖均为 plan.textbookId),但代码串行 await
- 违反规则:项目记忆 "Data fetching for parent dashboard should use
Promise.allorPromise.allSettledfor parallel queries"
问题 21:3 个 view 页错误处理不一致
- 位置:admin/view 用
notFound()抛 404;parent/view 与 student/view 用 inline<div>返回 200 + 错误文案 - 问题:行为分歧,应统一为
notFound()或统一的InlineLessonPlanError组件
问题 22:block-renderer.tsx 已 @deprecated 但仍存在
- 位置:block-renderer.tsx L3-6 注释
- 问题:已被
NodeEditor替代,但保留 195 行废弃代码 - 直接后果:维护负担、可能被误用
问题 23:anchor-injector.ts 的 skipPatterns 与 markdownToPlainText 逻辑不一致
- 位置:anchor-injector.ts L117-123 vs L26-51
- 问题:前者用
^多行匹配,后者用gm,可能导致偏移映射与纯文本转换结果不一致,锚点定位偏差
问题 24:use-lesson-plan-editor.ts Zustand store 319 行,超出 Hook ≤80 行建议
- 位置:use-lesson-plan-editor.ts
- 问题:项目规则原文 "自定义 Hook ≤ 80 行"。虽然 Zustand store 性质与 React hook 不同(集中定义 state + actions),但仍建议拆分为多个 slice
- 违反规则:项目规则 "自定义 Hook:建议 ≤ 80 行"
问题 25:actions.ts L187 返回原始字符串而非 i18n key
- 位置:actions.ts L187
return { success: false, message: "PLAN_NOT_FOUND" } - 问题:与文件其余部分使用
t("error.xxx")风格不一致
问题 26:data-access-templates.ts 与 data-access.ts 类型守卫重复定义
- 位置:data-access-templates.ts L19-50 与 lib/type-guards.ts L30-49 完全重复
- 问题:
isTemplateType/isTemplateScope/mapRowToTemplate在两个文件 + lib/type-guards.ts 三处定义 - 违反规则:DRY 原则
问题 27:data-access-knowledge.ts 两处 .map(r => ({ ... })) 完全重复
- 位置:data-access-knowledge.ts L63-86 与 L114-137(各 24 行完全相同)
- 问题:应提取为共享辅助函数
三、行业差距对比
基于 K12 教育信息化主流产品对标(Planboard / Chalk.com、Nearpod、PlanbookEdu、Common Curriculum、SMART Learning Suite、Google Classroom、Microsoft Teams + OneNote Class Notebook 等),本模块相对行业优秀实践的差距如下:
3.1 课程标准对标缺失(严重差距)
| 行业实践 | 本模块现状 | 影响 |
|---|---|---|
| Planboard/Common Curriculum 强制要求每节课关联州/国家标准(如 Common Core、NGSS、TEKS) | 本模块仅支持知识点标注,无课程标准(Standards)维度 | 教研组无法横向追踪课标覆盖度,无法生"课标覆盖报告";管理员无法看到全校课标达成情况 |
| 行业普遍支持课标搜索与一键关联到课案 | 本模块知识点来自 textbook 章节自定义树,无标准课标库 | 跨教材、跨学段的课标追溯断链 |
3.2 协同备课完全缺失(严重差距)
| 行业实践 | 本模块现状 | 影响 |
|---|---|---|
| Planboard/Google Docs 支持实时多人协同编辑(OT/CRDT 算法) | 本模块仅支持单人编辑,他人只能只读查看 | 教研组无法共同备课,新教师无法被老教师"带教"修改 |
| OneNote Class Notebook 支持评论批注(评论挂在 Block 上) | 本模块无评论能力 | 备课反馈只能线下进行,无法沉淀为知识资产 |
| Common Curriculum 支持协作工作流(提交审核 → 反馈 → 定稿) | 本模块有 publish/unpublish 但无审核工作流 |
教研组长无法对备课质量把关 |
3.3 课程地图(Curriculum Mapping)缺失
| 行业实践 | 本模块现状 | 影响 |
|---|---|---|
| Planboard/Atlas 支持垂直对齐(vertical alignment):跨年级查看某课标覆盖情况 | 本模块仅按 textbook+chapter 切分,无跨年级视图 | 学段衔接断链风险(小学升初中知识断层) |
| 行业支持Pacing Guide(进度指南):按周/月对齐课标进度 | 本模块无进度概念 | 教学进度失控,无法判断是否落后于计划 |
3.4 形成性评价闭环不完整
| 行业实践 | 本模块现状 | 影响 |
|---|---|---|
| Nearpod/SMART 支持课中实时互动(投票、Exit Ticket、快速问答) | 本模块有 publish-homework 课后作业,无课中互动 | 教师无法在备课时设计嵌入式互动,无法实时获取学情反馈 |
| 行业支持课堂形成性评价结果回写到课案 | 本模块作业数据与课案无回写关联 | 教师备课迭代无数据支撑,不知道哪些环节学生掌握差 |
3.5 资源管理弱
| 行业实践 | 本模块现状 | 影响 |
|---|---|---|
| PlanbookEdu 支持附件库(视频/PDF/图片/外链)挂载到课案 | 本模块虽有 files 模块但未与 lesson-preparation 集成(无 attachment 关联表) |
教师备课需重复上传素材,无统一资源中心 |
| 行业支持 OER(开放教育资源)库嵌入 | 本模块无 OER 集成 | 教师需手动搜索素材,备课效率低 |
3.6 多角色协同不足
| 行业实践 | 本模块现状 | 影响 |
|---|---|---|
| Planboard 支持代课教师(substitute teacher)计划 | 本模块无代课机制 | 教师请假时代课教师无法快速接手备课 |
| 行业支持学生互动课件(不只是只读查看) | 本模块学生只能 LessonPlanReadonlyView 只读查看 | 学生被动接受,无法参与课中互动 |
| 行业支持家长可见备课摘要(隐私脱敏后) | 本模块家长与学生看到的相同,无差异化摘要 | 家长无法获取"如何辅助孩子预习"的指导 |
3.7 缺失分析能力
| 行业实践 | 本模块现状 | 影响 |
|---|---|---|
| 行业支持备课分析仪表盘:每位教师的备课频次、模板使用率、平均时长 | 本模块仅 admin 页有 4 个统计卡片(total/published/draft/archived) | 学校无法评估教师备课投入,无法识别备课质量低的教师 |
| 行业支持课标覆盖热力图 | 本模块无 | 教研组无法识别薄弱课标 |
| 行业支持版本对比(diff 视图) | 本模块有版本管理但回滚无 diff 预览 | 教师回滚前无法判断改动幅度 |
3.8 移动端与离线能力
| 行业实践 | 本模块现状 | 影响 |
|---|---|---|
| Planboard 支持移动端原生 App | 本模块仅 Web 端,且节点图画布在平板上交互差 | 教师课堂使用不便 |
| 行业支持离线模式 | 本模块无 Service Worker / PWA | 教室网络不稳定时无法备课 |
3.9 AI 增强仍有空间
| 行业实践 | 本模块现状 | 影响 |
|---|---|---|
| MagicSchool AI / Khanmigo 支持按学生学情差异化生成多版本备课 | 本模块 AI 仅生成单版本内容 | 无法应对班级差异化(基础班/提高班) |
| 行业支持AI 自动关联课标 | 本模块 AI 仅推荐知识点 | 课标对标仍需手工 |
| 行业支持AI 评估备课质量(结构完整性、活动多样性等) | 本模块无 | 教师无备课改进建议 |
3.10 UX 细节差距
| 行业实践 | 本模块现状 | 影响 |
|---|---|---|
| Planboard 提供周历/月历视图拖拽课案 | 本模块仅列表 + 编辑器,无日历 | 教师无法直观看到本周备课分布 |
| 行业普遍使用快捷键体系(保存 Cmd+S、撤销 Cmd+Z 等) | 本模块节点图编辑器仅基础键盘导航 | 高频操作效率低 |
| 行业普遍有自动保存提示("已保存 · 2 分钟前") | 本模块编辑器无自动保存状态提示 | 教师不确定是否已保存 |
四、改进优先级建议
P0(安全与可用性阻塞,立即修复)
| # | 问题 | 改进方向(不含实现代码) |
|---|---|---|
| P0-1 | parent/student view 跨年级信息泄露 | 在 parent/lesson-plans/[planId]/view/page.tsx 与 student/lesson-plans/[planId]/view/page.tsx 拉取 plan 后追加 gradeId scope 校验;建议提取共享校验函数 assertPlanInScope(plan, ctx) 在两处复用 |
| P0-2 | 9 个页面缺失 requirePermission() |
在每个 page.tsx 的 getAuthContext() 前调用 requirePermission(Permissions.LESSON_PLAN_READ)(admin 可使用更强的 LESSON_PLAN_MANAGE_ALL 或就 LESSON_PLAN_READ) |
| P0-3 | textbook-segments.tsx a11y 严重缺陷 | 锚点 span 添加 role="button" + tabIndex={0} + onKeyDown(Enter/Space 触发 onClick);并补全 aria-label |
| P0-4 | default-data-service 直接 import questions/actions | 通过 LessonPlanDataService 接口注入 QuestionService,由 lesson-plan-provider-setup.tsx 在 app 层组合 questions/data-access.getQuestions(data-access→data-access 合规) |
P1(架构与体验,本次审计内修复)
| # | 问题 | 改进方向 |
|---|---|---|
| P1-1 | 4 个对话框/抽屉缺失 FocusTrap | 全部包裹 shared/components/a11y/focus-trap.tsx 的 <FocusTrap>,并补全 ESC 监听 |
| P1-2 | 未使用 React Suspense + 骨架屏 | 客户端组件中将 isLoading state 改为 React 18 use(promise) 或 <Suspense fallback={<Skeleton/>}> 模式;消费已定义的 4 个 Skeleton 组件 |
| P1-3 | Error Boundary 覆盖不全 | 在 LessonPlanEditor 内对 NodeEditor、VersionHistoryDrawer、各 Picker/Dialog 独立包裹 LessonPlanErrorBoundary |
| P1-4 | 硬编码颜色 | 提取到 globals.css CSS 变量(--lesson-node-color-objective 等 11 种),组件用 var(--...);NODE_COLORS 改为映射表 |
| P1-5 | exercise-block 原生 <a> |
替换为 next/link 的 <Link> |
| P1-6 | 6 个 block key={idx} |
改为业务唯一 key(如 homework-${hw.id ?? idx}) |
| P1-7 | template-picker 字符串拼接 className | 引入 cn() 替代模板字符串 |
| P1-8 | block-registry 缺 "use client" | 文件第一行添加 "use client" |
| P1-9 | NodeEditPanel.knownTypes 与 BLOCK_REGISTRY 重复 | 改为 Object.keys(BLOCK_REGISTRY) as BlockType[] |
| P1-10 | inline-question-editor QUESTION_TYPES 重复 | 改为从 lib/type-guards.ts 导入 VALID_QUESTION_TYPES 并 filter |
| P1-11 | ROLE_CONFIGS 缺 gradeHead | 新增 gradeHead 角色配置(只读 + 无操作按钮),对齐 viewMode 类型 |
| P1-12 | useRoleConfig 静默回退 | 在 process.env.NODE_ENV === "development" 下 console.warn |
| P1-13 | i18n-errors.ts as 断言 | 改用 t.has(msg) ? t(msg) : msg 模式或添加注释 |
| P1-14 | data-access-templates 重复类型守卫 | 删除本地副本,统一从 lib/type-guards.ts 导入 |
| P1-15 | data-access-knowledge 重复 .map | 提取共享 mapRowToListItemWithoutJoin 辅助函数 |
| P1-16 | actions.ts L187 原始字符串 | 改为 t("error.notFound") |
P2(性能与一致性,本次审计内修复)
| # | 问题 | 改进方向 |
|---|---|---|
| P2-1 | 3 个 view 页 textbook/chapters 串行 | 改为 Promise.all([getTextbookById, getChaptersByTextbookId]) |
| P2-2 | 3 个 view 页错误处理不一致 | 统一为 notFound() 调用,或新建 InlineLessonPlanError 共享组件 |
| P2-3 | Promise.resolve().then() 规避 setState | 改为 useEffect 内 async + ignore flag |
| P2-4 | block-renderer.tsx @deprecated | 确认无引用后删除 |
| P2-5 | anchor-injector skipPatterns 不一致 | 统一 regex 标志为 gm,添加单元测试 |
| P2-6 | use-lesson-plan-editor Zustand store 拆分 | 拆分为 editor-slice.ts + selection-slice.ts + version-slice.ts,主文件 < 80 行 |
中长期(功能补齐,需独立立项)
| # | 缺口 | 改进方向 |
|---|---|---|
| M1 | 课标(Standards)对标体系 | 新增 standards 模块,支持国家标准/课标/自定义三层级;课案关联多对多课标;管理后台提供课标库导入 |
| M2 | 协同备课(实时多人编辑 + 评论) | 评估 Yjs/Liveblocks 等方案;评论挂载到 Block 级别(需新表 lessonPlanComments) |
| M3 | 审核工作流(draft → submitted → approved → published) | 扩展 status 枚举 + 新增 reviewer 角色;教研组长审核 |
| M4 | 课程地图(Curriculum Map) | 新增 admin/curriculum-map 页面,按年级/学科/课标矩阵展示覆盖度 |
| M5 | 形成性评价闭环 | publish 时嵌入互动组件(poll/quiz),课中学生作答,结果回写课案 |
| M6 | 资源附件库 | 新增 lessonPlanAttachments 关联表;UI 提供附件选择器 |
| M7 | 移动端 PWA + 离线 | 评估 next-pwa;节点图改为 touch-friendly 模式 |
| M8 | AI 差异化生成 + 课标推荐 + 备课质量评估 | 扩展 ai-suggest.ts;新增 evaluateLessonPlan 函数 |
| M9 | 日历视图(周历/月历) | 新增 teacher/lesson-plans/calendar 路由,使用全日历组件 |
| M10 | 备课分析仪表盘 | 扩展 admin 页,新增"教师备课投入""模板使用率""课标覆盖热力图"3 张图表 |
| M11 | 版本 diff 预览 | version-history-drawer 增加"对比当前"按钮,使用 diff-match-patch |
| M12 | 代课教师(substitute)机制 | 新增 lessonPlanSubstitutes 表与角色映射 |
五、重构方案设计(强制满足全部原则)
5.1 完全解耦:通过接口抽象数据依赖 + Provider 注入
目标:本模块内部组件不直接 import 任何其他业务模块(questions、exams、homework、classes、textbooks、ai、files)的 actions 或 data-access,所有跨模块依赖通过 LessonPlanDataService 接口的"方法契约"定义,由 lesson-plan-provider-setup.tsx 在 app 层注入具体实现。
接口契约扩展(lesson-plan-provider.tsx):
// 新增对外部模块的抽象接口
export interface QuestionService {
getQuestions(params: QuestionPickerParams): Promise<ActionState<{ items: QuestionPickerItem[]; total: number }>>
createQuestion(input: unknown): Promise<ActionState<{ questionId: string }>>
}
export interface HomeworkService {
createHomeworkAssignment(input: unknown): Promise<ActionState<{ assignmentId: string }>>
}
export interface ExamService {
persistExamDraft(input: unknown): Promise<ActionState<{ examId: string }>>
addExamQuestions(examId: string, items: unknown[]): Promise<ActionState<void>>
}
export interface ClassService {
getStudentIdsByClassIds(classIds: string[]): Promise<ActionState<{ studentIds: string[] }>>
}
export interface TextbookService {
getTextbooks(): Promise<ActionState<{ items: TextbookPickerOption[] }>>
getChaptersByTextbookId(textbookId: string): Promise<ActionState<{ tree: ChapterPickerOption[] }>>
findChapterById(chapters: ChapterPickerOption[], chapterId: string): ChapterPickerOption | undefined
}
export interface AiService {
generateLessonContent(input: unknown): Promise<ActionState<{ content: unknown }>>
suggestKnowledgePoints(doc: unknown): Promise<ActionState<{ suggestions: unknown[] }>>
}
// 扩展主接口
export interface LessonPlanDataService {
// ...现有 16 个方法...
// 新增对外部服务的访问器(仅返回注入的 service,不直接调用)
questionService: QuestionService
homeworkService: HomeworkService
examService: ExamService
classService: ClassService
textbookService: TextbookService
aiService: AiService
}
注入点(lesson-plan-provider-setup.tsx):
"use client"
import { useMemo } from "react"
import { createDefaultDataService } from "../services/default-data-service"
import { createDefaultQuestionService } from "../services/default-question-service" // 新增
import { createDefaultHomeworkService } from "../services/default-homework-service" // 新增
// ...
default-question-service.ts 等新增文件实现 QuestionService 接口,内部仅 import @/modules/questions/data-access(data-access→data-access 合规)。
publish-service.ts 改为接收已注入的 service 对象,不再直接 import @/modules/questions/data-access 等。
5.2 组合优先:通过 children/slots/render props 实现灵活性
目标:所有 UI 通过组件组合实现,逻辑复用一律抽取为自定义 hooks。
示例 1:LessonPlanCard 多角色通过 props 注入而非分支:
<LessonPlanCard
plan={plan}
actionsSlot={
<>
{canEdit && <EditButton planId={plan.id} />}
{canPublish && <PublishButton planId={plan.id} />}
{canDuplicate && <DuplicateButton planId={plan.id} />}
</>
}
/>
示例 2:AI 内容生成通过 render props:
<LessonPlanEditor
aiContentGenerator={(ctx: AiContext) => <AiLessonContentGenerator {...ctx} />}
/>
示例 3:跨模块逻辑抽取为 hooks:
// hooks/use-question-search.ts (新文件,<=80 行)
export function useQuestionSearch(service: QuestionService, initialFilters: QuestionPickerParams) {
// 纯逻辑 + 状态管理,与 UI 分离
// 可独立测试
}
5.3 国际化就绪:所有展示文本使用 i18n 键
当前状态:本模块 i18n 已彻底化(zh-CN/en 各 400 行结构一致),本次审计未发现新的硬编码中文。
翻译文件结构示例(建议为 P0-3/P1 等新增键):
// shared/i18n/messages/zh-CN/lesson-preparation.json
{
"error": {
"notFound": "未找到课案",
"versionNotFound": "未找到版本",
"gradeScopeViolation": "无权访问该年级的课案",
"permissionDenied": "无权访问此课案"
},
"picker": {
"questionType": {
"single_choice": "单选题",
"multiple_choice": "多选题",
"true_false": "判断题",
"short_answer": "简答题",
"essay": "论述题"
}
}
}
5.4 最大化复用:识别四角色共用块
通用 UI 块(抽象为泛型组件):
| 复用单元 | 类型 | 抽象为 |
|---|---|---|
| 课案卡片 | UI | <LessonPlanCard actionsSlot /> (已实现,需扩展 actionsSlot) |
| 课案列表 | UI | <LessonPlanList viewMode /> (已实现) |
| 只读画布 | UI | <LessonPlanReadonlyView /> (已实现) |
| 节点图编辑器 | UI | <NodeEditor readonly /> (通过 readonly prop 切换) |
| 模板选择器 | UI | <TemplatePicker /> (已实现) |
| 版本历史 | UI | <VersionHistoryDrawer /> (已实现) |
通用业务 hooks(抽取为泛型):
| 复用单元 | 类型 | 抽象为 |
|---|---|---|
| 课案权限配置 | 逻辑 | useRoleConfig() + ROLE_CONFIGS 注册表 (已实现,需补 gradeHead) |
| 数据 scope 校验 | 逻辑 | assertPlanInScope(plan, ctx) (新增,共享) |
| 节点编辑器状态 | 逻辑 | useLessonPlanEditor (Zustand,需拆 slice) |
| 锚点注入算法 | 逻辑 | lib/anchor-injector.ts (已实现,纯函数) |
| 文档迁移 | 逻辑 | lib/document-migration.ts (已实现,纯函数) |
5.5 错误与边界处理
- 每个独立数据区块用 React Error Boundary 包裹:在
LessonPlanEditor内对NodeEditor、VersionHistoryDrawer、各 Picker/Dialog 独立包裹LessonPlanErrorBoundary。 - React Suspense + 骨架屏:客户端组件数据加载使用
<Suspense fallback={<LessonPlanListSkeleton />}>;消费lesson-plan-skeleton.tsx已定义的 4 个 Skeleton。 - 空数据/无权限/网络异常:
- 空数据:在
lesson-plan-list.tsx已实现空状态文案(V3 续已有)。 - 无权限:在 P0-1 中通过
assertPlanInScope抛错或返回notFound()。 - 网络异常:
LessonPlanErrorBoundary兜底,提供"重试"按钮。
- 空数据:在
5.6 可测试性
- 纯逻辑全部放入纯函数或 hooks:
lib/已有 6 个纯函数模块(type-guards/i18n-errors/document-migration/anchor-injector/node-summary/rf-mappers),无 UI 依赖,可独立单元测试。 - 建议新增单元测试:
lib/type-guards.test.ts:覆盖 20+ 类型守卫的边界情况lib/anchor-injector.test.ts:覆盖 markdownToPlainText 与 injectPlaceholders 的偏移一致性(修 P2-5 后)lib/document-migration.test.ts:覆盖 v1→v2→v3 链式迁移hooks/use-lesson-plan-editor.test.ts:覆盖 Zustand store 状态变更
- 导出清晰的接口类型:
LessonPlanDataService已定义为接口(V3 已实现),测试可注入 mock 实现。
5.7 可扩展性:配置驱动设计
目标:通过角色配置决定该模块渲染哪些 Widget/子模块。
当前已部分实现(ROLE_CONFIGS 注册表),需扩展:
// 扩展 ROLE_CONFIGS 增加 widget 可见性
export interface LessonPlanRoleConfig {
canEdit: boolean
canPublish: boolean
canDelete: boolean
canDuplicate: boolean
canSaveAsTemplate: boolean
// 新增 widget 配置
visibleWidgets: {
nodeEditor: boolean // 教师可见
versionHistory: boolean
knowledgePointPicker: boolean
questionBankPicker: boolean
publishHomeworkDialog: boolean
aiContentGenerator: boolean
templatePicker: boolean // 仅新建页可见
}
defaultStatus: LessonPlanStatus // teacher: "draft"; student/parent: "published"
}
新增角色只需扩展 ROLE_CONFIGS 而不动组件代码。
5.8 企业级补充
a11y:
- 补全所有 dialog FocusTrap(P1-1)
- 补全 textbook-segments 锚点键盘可访问性(P0-3)
- 补全按钮
aria-disabled/aria-expanded/aria-label
性能:
- 优先使用 React Server Components 获取初始数据(已实现,9 个 page.tsx 均为 async Server Component)
- 客户端组件仅负责交互(已实现,全部交互组件有
"use client") - 支持流式渲染(Next.js 15 默认支持,无需额外配置)
- 3 个 view 页 textbook/chapters 并行查询(P2-1)
安全性:
- 所有敏感数据查询在 data-access 层结合
ctx.dataScope过滤(已实现) - Server Action 二次校验(已实现
requirePermission()) - 页面层补齐
assertPlanInScope(P0-1)
监控埋点:
LessonPlanTracker接口已预留(V2-6 实现useLessonPlanTrackerSafe)- 6 个关键操作已埋点(create/save/publish/revert/duplicate/archive)
- 建议扩展埋点:a11y 违规、Error Boundary 触发、节点图加载时长
六、文档实施计划
第一阶段:本次审计内实施(P0 + P1 + P2)
按本报告"四、改进优先级建议"中的 P0、P1、P2 共 30 项全部完成代码修改,并运行 npm run lint 与 npx tsc --noEmit 验证零错误。
第二阶段:中长期独立立项(M1-M12)
中长期计划需各自独立立项,本次审计仅记录在案,不在本次实施范围内:
- M1 课标对标体系(高优,影响 K12 合规性)
- M2 协同备课(高优,影响教研效率)
- M3 审核工作流(中优)
- M4 课程地图(中优)
- M5 形成性评价闭环(中优)
- M6 资源附件库(低优)
- M7 移动端 PWA(低优)
- M8 AI 差异化生成(中优)
- M9 日历视图(低优)
- M10 备课分析仪表盘(中优)
- M11 版本 diff 预览(低优)
- M12 代课教师机制(低优)
七、架构图同步说明
本次审计未发现架构图遗漏节点,004/005 文档与本模块当前代码状态一致。但需在 005_architecture_data.json 的 modules.lesson_preparation.auditFixes 中新增以下 v4 条目:
| ID | 内容 |
|---|---|
| V4-1 | P0-1 parent/student view gradeId scope 校验:新增 assertPlanInScope 共享函数,两路由页面调用 |
| V4-2 | P0-2 9 个页面 requirePermission:补全 LESSON_PLAN_READ 权限校验 |
| V4-3 | P0-3 textbook-segments a11y:锚点 span 添加 role=button + tabIndex + onKeyDown |
| V4-4 | P0-4 default-data-service 解耦:新增 QuestionService/HomeworkService/ExamService/ClassService/TextbookService/AiService 6 个接口,由 lesson-plan-provider-setup 在 app 层注入 data-access 实现 |
| V4-5 | P1-1 4 个 dialog FocusTrap + ESC:包裹 FocusTrap + 监听 ESC |
| V4-6 | P1-2 Suspense + 骨架屏:客户端组件消费 4 个已定义 Skeleton |
| V4-7 | P1-3 Error Boundary 全覆盖:NodeEditor/VersionHistoryDrawer/Picker/Dialog 独立包裹 |
| V4-8 | P1-4 硬编码颜色:提取到 globals.css CSS 变量 + NODE_COLORS 映射表 |
| V4-9 | P1-5 exercise-block Link:替换原生 <a> |
| V4-10 | P1-6 6 个 block key 改业务唯一 key |
| V4-11 | P1-7 template-picker cn():替换字符串拼接 |
| V4-12 | P1-8 block-registry use client |
| V4-13 | P1-9 NodeEditPanel knownTypes 改 Object.keys(BLOCK_REGISTRY) |
| V4-14 | P1-10 inline-question-editor 复用 VALID_QUESTION_TYPES |
| V4-15 | P1-11 ROLE_CONFIGS 新增 gradeHead |
| V4-16 | P1-12 useRoleConfig 开发环境 warn |
| V4-17 | P1-13 i18n-errors.ts as 断言修复 |
| V4-18 | P1-14 data-access-templates 重复类型守卫清理 |
| V4-19 | P1-15 data-access-knowledge 重复 .map 提取 |
| V4-20 | P1-16 actions.ts L187 改 i18n key |
| V4-21 | P2-1 3 个 view 页 textbook/chapters 并行 |
| V4-22 | P2-2 3 个 view 页错误处理统一 notFound() |
| V4-23 | P2-3 Promise.resolve().then() 改 useEffect+ignore flag |
| V4-24 | P2-4 block-renderer.tsx @deprecated 删除 |
| V4-25 | P2-5 anchor-injector skipPatterns 一致性修复 + 单测 |
| V4-26 | P2-6 use-lesson-plan-editor Zustand 拆 slice |
004_architecture_impact_map.md §2.27 章节末尾追加"V4 审计修复"小节,简述上述条目。
审计完成。下一步:实施第一阶段全部 30 项修复。