# 备课模块审计报告 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.json` modules.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.json` modules.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.tsx` L21-31、`src/app/(dashboard)/student/lesson-plans/[planId]/view/page.tsx` L21-41 - **问题**:两个路由仅校验 `plan.status === "published"`,**完全未校验 `plan.gradeId` 是否在 `ctx.dataScope.gradeIds` 范围内** - **直接后果**:任何家长/学生通过修改 URL 中的 `planId`(仅校验 published),即可查看**其他年级**已发布课案 - **数据访问层明示警示**:[data-access.ts](file:///e:/Desktop/CICD/src/modules/lesson-preparation/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 use `usePermission().hasPermission()`" #### 问题 3:`textbook-segments.tsx` 锚点段落键盘用户完全不可操作 — a11y 严重缺陷 - **位置**:[textbook-segments.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/nodes/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](file:///e:/Desktop/CICD/src/modules/lesson-preparation/services/default-data-service.ts) L21 ```typescript 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](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/publish-homework-dialog.tsx) 全文 - [question-bank-picker.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/question-bank-picker.tsx) L117-119 - [knowledge-point-picker.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/knowledge-point-picker.tsx) L77-79 - [version-history-drawer.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/version-history-drawer.tsx) L87-89 - **问题**:4 个组件均使用 `role="dialog"` + `aria-modal="true"`,但**未使用 FocusTrap 包裹**。仅 `inline-question-editor.tsx` L82 正确使用了 `` - **直接后果**:Tab 键焦点可逃逸到对话框背后的页面,违反 WAI-ARIA Dialog 模式 - **违反规则**:项目规则 "a11y:键盘导航" #### 问题 6:4 个对话框/抽屉 ESC 键关闭未实现 - **位置**:同问题 5 的 4 个组件 - **问题**:仅有遮罩点击关闭 (`onClick={onClose}`),未监听 `keydown` ESC 键 - **直接后果**:用户只能点击关闭按钮,无键盘快捷关闭路径 - **违反规则**:项目规则 "a11y:键盘导航" #### 问题 7:未使用 React Suspense + 骨架屏(项目规则明确要求) - **位置**:全模块客户端组件 - **问题**:项目规则要求"异步数据使用 React Suspense + 骨架屏"。本模块在 `lesson-plan-skeleton.tsx` 定义了 4 个骨架屏组件(`VersionListSkeleton`/`QuestionBankSkeleton`/`KnowledgePointSkeleton`/`LessonPlanListSkeleton`),但**这些骨架屏仅被 `loading.tsx` 文件使用(路由级 Suspense),未在客户端组件数据加载时使用** - **现状**:客户端组件用本地 `isLoading` state + 文本"加载中..."代替骨架屏 - **直接后果**:组件级异步加载(如打开 version-history-drawer)显示纯文本而非骨架,UX 不一致 - **违反规则**:项目规则 "异步数据使用 React Suspense + 骨架屏" #### 问题 8:Error Boundary 覆盖不全 - **位置**: - [lesson-plan-editor.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/lesson-plan-editor.tsx) — NodeEditor 画布未被 Error Boundary 包裹(仅 NodeEditPanel 内的 BlockRenderer 被包裹) - [version-history-drawer.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/version-history-drawer.tsx) 全文未包裹 - [publish-homework-dialog.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/publish-homework-dialog.tsx) 全文未包裹 - [knowledge-point-picker.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/knowledge-point-picker.tsx) 全文未包裹 - [question-bank-picker.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/question-bank-picker.tsx) 全文未包裹 - **问题**:项目规则要求"每个独立的数据区块必须用 React Error Boundary 包裹"。当前仅 `node-edit-panel.tsx` L161-173 包裹 `BlockRenderer`,其他独立数据区块无兜底 - **直接后果**:单个 Picker 数据加载失败会冒泡到路由级 `error.tsx`,整个页面变白屏 - **违反规则**:项目规则 "每个独立的数据区块必须用 React Error Boundary 包裹" ### 2.3 类型安全与代码质量(P1/P2) #### 问题 9:`i18n-errors.ts` L24 无注释的 `as` 断言 - **位置**:[i18n-errors.ts](file:///e:/Desktop/CICD/src/modules/lesson-preparation/lib/i18n-errors.ts) L24 ```typescript t(msg as Parameters[0]) ``` - **问题**:从 `string` 收窄到 `next-intl` 的 i18n key 联合类型,无注释说明 - **违反规则**:项目规则 "禁止 `as` 断言(除非从 `unknown` 转换或测试中,需注释原因)" #### 问题 10:硬编码颜色字符串散落多处,未使用 CSS 变量 - **位置**: - [node-editor.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/node-editor.tsx) L228 `stroke: "#1976d2"`、L237 `color="#ccc"` - [lesson-node.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/nodes/lesson-node.tsx) L43 `borderColor: selected ? "#1976d2" : color`、L44 `boxShadow` rgba - [textbook-content-node.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/nodes/textbook-content-node.tsx) L319 `borderColor: selected ? "#1976d2" : "#455a64"`、L329 `backgroundColor: "#455a64"`、L386 `backgroundColor: "#1976d2"` - [node-summary.ts](file:///e:/Desktop/CICD/src/modules/lesson-preparation/lib/node-summary.ts) L118-132 `NODE_COLORS` 13 个 hex 颜色 - **问题**:V3 已修复 MiniMap 颜色硬编码(V3-18),但节点本身、画布背景、NODE_COLORS 仍硬编码 - **违反规则**:项目规则 "设计令牌在 src/app/globals.css 中使用 CSS 变量定义" #### 问题 11:`exercise-block.tsx` 使用原生 `` 标签 - **位置**:[exercise-block.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/blocks/exercise-block.tsx) L123-128 ```tsx ... ``` - **问题**:未使用 Next.js ``,导致整页刷新 - **违反规则**:项目记忆 "Link navigation must use Next.js `` component instead of raw `` tags" #### 问题 12:6 个 block 组件使用 `key={idx}` 列表 key - **位置**:`homework-block.tsx` L46 / `key-point-block.tsx` L46 / `new-teaching-block.tsx` L51 / `objective-block.tsx` L49 / `reflection-block.tsx` L46 / `summary-block.tsx` L41 - **问题**:列表项增删时 React key 不稳定,可能引发状态错乱 - **违反规则**:React key 稳定性最佳实践(隐含在"代码质量规则"中) #### 问题 13:`template-picker.tsx` 字符串拼接动态类名 - **位置**:[template-picker.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/template-picker.tsx) L265-269、L296-300 ```tsx 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](file:///e:/Desktop/CICD/src/modules/lesson-preparation/config/block-registry.tsx) L1 - **问题**:`BlockRenderer` 函数(L88)返回 ReactElement 并 switch 渲染各 client block 组件,本文件应明确标注 `"use client"` - **违反规则**:项目规则 "需要交互时才添加 `\"use client\"`(必须位于文件第一行)" #### 问题 15:`NodeEditPanel.knownTypes` 与 `BLOCK_REGISTRY` 重复定义(违反 DRY) - **位置**:[node-edit-panel.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/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](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/inline-question-editor.tsx) L21、[type-guards.ts](file:///e:/Desktop/CICD/src/modules/lesson-preparation/lib/type-guards.ts) L184 - **问题**:`inline-question-editor` 仅定义 3 种题型子集,`type-guards` 已定义完整 5 种,应复用并 filter #### 问题 17:`ROLE_CONFIGS` 缺失 `gradeHead` 角色,与 `LessonPlanCard.viewMode` 不一致 - **位置**:[lesson-plan-provider.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/providers/lesson-plan-provider.tsx) L255-260 `ROLE_CONFIGS` vs [lesson-plan-card.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/lesson-plan-card.tsx) L50 `viewMode` 类型包含 `"gradeHead"` - **问题**:组件类型支持 gradeHead 视图,但 Provider 配置表无对应配置 - **直接后果**:gradeHead 用户访问时会静默回退到 TEACHER_ROLE_CONFIG,可能误显示教师操作按钮 #### 问题 18:`useRoleConfig` 静默回退教师配置,无开发环境告警 - **位置**:[lesson-plan-provider.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/providers/lesson-plan-provider.tsx) L319-322 - **问题**:Provider 外调用 `useRoleConfig()` 时静默回退到 `TEACHER_ROLE_CONFIG`,无 console.warn - **直接后果**:开发者误用时不报警,可能在生产环境暴露教师操作按钮给非教师角色 ### 2.4 性能与代码组织(P2) #### 问题 19:3 个组件用 `Promise.resolve().then()` / `queueMicrotask` 规避同步 setState - **位置**: - [knowledge-point-picker.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/knowledge-point-picker.tsx) L33-65 - [version-history-drawer.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/version-history-drawer.tsx) L46 - **问题**:可读性差,应使用 `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.tsx` L37-46 - **问题**:textbook + chapters 查询彼此独立(依赖均为 plan.textbookId),但代码串行 await - **违反规则**:项目记忆 "Data fetching for parent dashboard should use `Promise.all` or `Promise.allSettled` for parallel queries" #### 问题 21:3 个 view 页错误处理不一致 - **位置**:admin/view 用 `notFound()` 抛 404;parent/view 与 student/view 用 inline `
` 返回 200 + 错误文案 - **问题**:行为分歧,应统一为 `notFound()` 或统一的 `InlineLessonPlanError` 组件 #### 问题 22:`block-renderer.tsx` 已 @deprecated 但仍存在 - **位置**:[block-renderer.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/block-renderer.tsx) L3-6 注释 - **问题**:已被 `NodeEditor` 替代,但保留 195 行废弃代码 - **直接后果**:维护负担、可能被误用 #### 问题 23:`anchor-injector.ts` 的 `skipPatterns` 与 `markdownToPlainText` 逻辑不一致 - **位置**:[anchor-injector.ts](file:///e:/Desktop/CICD/src/modules/lesson-preparation/lib/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](file:///e:/Desktop/CICD/src/modules/lesson-preparation/hooks/use-lesson-plan-editor.ts) - **问题**:项目规则原文 "自定义 Hook ≤ 80 行"。虽然 Zustand store 性质与 React hook 不同(集中定义 state + actions),但仍建议拆分为多个 slice - **违反规则**:项目规则 "自定义 Hook:建议 ≤ 80 行" #### 问题 25:`actions.ts` L187 返回原始字符串而非 i18n key - **位置**:[actions.ts](file:///e:/Desktop/CICD/src/modules/lesson-preparation/actions.ts) L187 ```typescript return { success: false, message: "PLAN_NOT_FOUND" } ``` - **问题**:与文件其余部分使用 `t("error.xxx")` 风格不一致 #### 问题 26:data-access-templates.ts 与 data-access.ts 类型守卫重复定义 - **位置**:[data-access-templates.ts](file:///e:/Desktop/CICD/src/modules/lesson-preparation/data-access-templates.ts) L19-50 与 [lib/type-guards.ts](file:///e:/Desktop/CICD/src/modules/lesson-preparation/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](file:///e:/Desktop/CICD/src/modules/lesson-preparation/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` 的 ``,并补全 ESC 监听 | | P1-2 | 未使用 React Suspense + 骨架屏 | 客户端组件中将 `isLoading` state 改为 React 18 `use(promise)` 或 `}>` 模式;消费已定义的 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 原生 `` | 替换为 `next/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`)**: ```typescript // 新增对外部模块的抽象接口 export interface QuestionService { getQuestions(params: QuestionPickerParams): Promise> createQuestion(input: unknown): Promise> } export interface HomeworkService { createHomeworkAssignment(input: unknown): Promise> } export interface ExamService { persistExamDraft(input: unknown): Promise> addExamQuestions(examId: string, items: unknown[]): Promise> } export interface ClassService { getStudentIdsByClassIds(classIds: string[]): Promise> } export interface TextbookService { getTextbooks(): Promise> getChaptersByTextbookId(textbookId: string): Promise> findChapterById(chapters: ChapterPickerOption[], chapterId: string): ChapterPickerOption | undefined } export interface AiService { generateLessonContent(input: unknown): Promise> suggestKnowledgePoints(doc: unknown): Promise> } // 扩展主接口 export interface LessonPlanDataService { // ...现有 16 个方法... // 新增对外部服务的访问器(仅返回注入的 service,不直接调用) questionService: QuestionService homeworkService: HomeworkService examService: ExamService classService: ClassService textbookService: TextbookService aiService: AiService } ``` **注入点(`lesson-plan-provider-setup.tsx`)**: ```typescript "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 注入而非分支**: ```tsx {canEdit && } {canPublish && } {canDuplicate && } } /> ``` **示例 2:AI 内容生成通过 render props**: ```tsx } /> ``` **示例 3:跨模块逻辑抽取为 hooks**: ```typescript // 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 等新增键): ```json // 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 | `` (已实现,需扩展 actionsSlot) | | 课案列表 | UI | `` (已实现) | | 只读画布 | UI | `` (已实现) | | 节点图编辑器 | UI | `` (通过 readonly prop 切换) | | 模板选择器 | UI | `` (已实现) | | 版本历史 | UI | `` (已实现) | **通用业务 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 + 骨架屏**:客户端组件数据加载使用 `}>`;消费 `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` 注册表),需扩展: ```typescript // 扩展 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:替换原生 `` | | 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 项修复。**