# 备课模块审计报告 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 正确使用了 `<FocusTrap>`
- **直接后果**：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<typeof t>[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` 使用原生 `<a>` 标签

- **位置**：[exercise-block.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/blocks/exercise-block.tsx) L123-128
  ```tsx
  <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.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 `<div>` 返回 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` 的 `<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`）**：

```typescript
// 新增对外部模块的抽象接口
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`）**：

```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
<LessonPlanCard
  plan={plan}
  actionsSlot={
    <>
      {canEdit && <EditButton planId={plan.id} />}
      {canPublish && <PublishButton planId={plan.id} />}
      {canDuplicate && <DuplicateButton planId={plan.id} />}
    </>
  }
/>
```

**示例 2：AI 内容生成通过 render props**：

```tsx
<LessonPlanEditor
  aiContentGenerator={(ctx: AiContext) => <AiLessonContentGenerator {...ctx} />}
/>
```

**示例 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 | `<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` 注册表），需扩展：

```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：替换原生 `<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 项修复。**
