# 备课模块审计报告 v3

> 审计日期：2026-06-24
> 审计范围：`src/modules/lesson-preparation/` 全部文件 + `src/app/(dashboard)/{teacher,admin,student,parent}/lesson-plans/` 全部页面
> 审计依据：`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-v2.md`

---

## 一、现有实现概要

### 1.1 文件分布

备课模块共 45 个文件，分布如下：

| 层级 | 文件数 | 主要文件 |
|------|--------|----------|
| types/schema/constants | 3 | types.ts(345行)、schema.ts(63行)、constants.ts(117行) |
| data-access | 4 | data-access.ts(593行)、data-access-versions.ts(181行)、data-access-templates.ts(129行)、data-access-knowledge.ts(95行) |
| actions | 4 | actions.ts(391行)、actions-publish.ts(82行)、actions-ai.ts(46行)、actions-kp.ts(47行) |
| services | 2 | publish-service.ts(198行)、ai-suggest.ts(82行) |
| lib | 6 | type-guards.ts(213行)、i18n-errors.ts(50行)、document-migration.ts(330行)、anchor-injector.ts(304行)、node-summary.ts(133行)、rf-mappers.ts(172行) |
| config | 1 | block-registry.tsx(194行) |
| providers | 2 | lesson-plan-provider.tsx(336行)、lesson-plan-provider-setup.tsx(29行) |
| services | 1 | default-data-service.ts(165行) |
| hooks | 1 | use-lesson-plan-editor.ts(303行) |
| components | 18 | 含 11 个 block 组件 + 4 个 node 组件 + 7 个业务组件 |
| seed | 1 | seed-templates.ts(9行) |
| 页面 | 13 | teacher(3) + admin(2) + student(2) + parent(2) + loading/error(4) |

### 1.2 数据流

```
页面(Server Component) → getAuthContext() → data-access → DB
                       ↓
                       LessonPlanProviderSetup → LessonPlanProvider
                       ↓
                       LessonPlanEditor(LessonPlanList) → useLessonPlanContextSafe()
                       ↓
                       default-data-service → Server Actions → requirePermission → data-access → DB
```

### 1.3 架构图完整性

架构影响地图 §2.27 记录的导出函数、文件清单、依赖关系与实际代码**基本一致**，但存在以下遗漏：
- `providers/lesson-plan-provider-setup.tsx` 已在 V3 续审计中补充
- `components/nodes/anchor-node-selector.tsx` 和 `textbook-segments.tsx` 已在 V3 续审计中补充
- `node-edit-panel.tsx` 对 `@/modules/ai` 的直接依赖**未在架构图依赖关系中记录**

---

## 二、现存问题与原因分析

### 2.1 安全与权限问题（P0）

#### 问题 1：Parent/Student 路由未校验孩子/班级归属 — 信息泄露漏洞

- **位置**：`data-access.ts` 第 197-204 行 `buildScopeCondition`
- **问题**：`children` 和 `class_members` 类型的 DataScope 仅过滤 `status = 'published'`，**完全未使用 `scope.childrenIds`/`scope.classIds`/`scope.gradeIds` 进行归属过滤**
- **违反规则**：项目规则 "Parent routes must include permission checks with both parentId and studentId to prevent information leakage"
- **后果**：任何家长可查看全校所有已发布课案；任何学生可查看全校所有已发布课案，构成信息泄露

#### 问题 2：`createLessonPlanVersion` 未校验 planId 归属

- **位置**：`data-access-versions.ts` 第 56-82 行
- **问题**：函数接收 `planId` 和 `userId`，但事务内只查询 `lessonPlanVersions` 表的 max(versionNo)，**未校验该 planId 是否属于 userId**
- **违反规则**：项目规则 "All Server Actions must call requirePermission() for permission verification" + "软删除/权限过滤在 data-access 层结合 userId 过滤"
- **后果**：调用方传入任意 planId 即可为他人课案创建版本记录，越权写入

#### 问题 3：`pruneAutoVersions` 完全无权限校验

- **位置**：`data-access-versions.ts` 第 152-181 行
- **问题**：函数签名 `pruneAutoVersions(planId, keep = 50)` **没有 userId 参数**，任何调用方传入 planId 即可删除该课案的自动版本记录
- **违反规则**：同问题 2
- **后果**：越权删除他人课案的版本历史

#### 问题 4：`getLessonPlansByKnowledgePoint`/`getLessonPlansByQuestion` 无权限过滤

- **位置**：`data-access-knowledge.ts` 第 11-95 行
- **问题**：两个函数直接 `db.select().from(lessonPlans)` 查询全表，**未过滤 creatorId，也未过滤 status（archived 课案也会被查出）**
- **违反规则**：同问题 2
- **后果**：任意调用方可获取所有课案（含他人 draft、archived 状态）的列表，严重越权

#### 问题 5：`saveLessonPlanVersionAction` 的 schema 不包含 content 字段

- **位置**：`actions.ts` 第 154-178 行、`schema.ts` 第 26-28 行
- **问题**：`saveVersionSchema` 仅校验 `planId` 和 `label`，**完全不包含 `content` 字段**，`input.content`（类型为 `LessonPlanDocument`）未经任何运行时校验直接持久化
- **违反规则**：项目规则 "输入使用 Zod 验证，验证失败返回结构化错误"
- **后果**：恶意或畸形文档结构可被写入数据库

#### 问题 6：`publishLessonPlanHomeworkAction` 中 homeworkTitle 传入 planId

- **位置**：`actions-publish.ts` 第 35 行
- **问题**：`homeworkTitle = t("publish.homeworkTitle", { title: parsed.data.planId })` — `title` 参数传入的是 `planId`（UUID），而非课案标题
- **违反规则**：业务逻辑正确性
- **后果**：作业标题将包含 UUID 而非有意义的课案名称

#### 问题 7：`getLessonPlansAction` 的 params 未经验证

- **位置**：`actions.ts` 第 43-53 行
- **问题**：`params`（含 `query`/`textbookId`/`chapterId`/`subjectId`/`status`）**未经过 Zod 验证**直接传入 `getLessonPlans()`
- **违反规则**：项目规则 "输入使用 Zod 验证"
- **后果**：若 `query` 用于 SQL LIKE 查询且未调用 `escapeLikePattern()`，存在 LIKE 通配符注入风险

### 2.2 架构违规问题（P0/P1）

#### 问题 8：`node-edit-panel.tsx` 直接 import `@/modules/ai`

- **位置**：`node-edit-panel.tsx` 第 11-12 行
- **问题**：直接 import `AiLessonContentGenerator` 和 `useAiClientOptional`，形成模块间紧耦合
- **违反规则**：项目规则 "模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access（只能通过注入的接口调用）"
- **后果**：模块间紧耦合，无法独立测试，AI 模块变更影响备课模块

#### 问题 9：admin/student/parent 列表页未包裹 `LessonPlanProviderSetup`

- **位置**：`admin/lesson-plans/page.tsx`、`student/lesson-plans/page.tsx`、`parent/lesson-plans/page.tsx`
- **问题**：`LessonPlanList` 调用 `useLessonPlanContextSafe()` 获取 `service`，当未在 Provider 内使用时 `service` 为 `null`，导致 `handleFilter` 静默返回 — **筛选功能在 admin/student/parent 页面完全失效**
- **违反规则**：项目规则 "Provider 是否正确注入数据服务"
- **后果**：筛选 UI 仍可见但点击无反应，用户体验差

#### 问题 10：`publish-service.ts` 多步写操作未包裹事务

- **位置**：`publish-service.ts` 第 56-197 行
- **问题**：`publishLessonPlanHomework` 包含 5 个写操作步骤（创建题目、创建 exam 草稿、插入 exam 题目关联、下发作业、回写溯源标记），**均未包裹在 `db.transaction` 中**
- **违反规则**：项目规则 "需要原子性的操作必须包裹在 db.transaction 中"
- **后果**：任一步骤失败将导致数据不一致（孤儿题目、草稿残留、溯源断裂）

### 2.3 类型安全问题（P1）

#### 问题 11：`rf-mappers.ts` 颜色映射 Bug

- **位置**：`rf-mappers.ts` 第 113 行
- **问题**：`getNodeColor(anchor.nodeId)` 传入 nodeId 而非 node type，`getNodeColor` 期望接收节点类型（如 `"objective"`），传入 nodeId 必然找不到匹配项
- **违反规则**：功能性 Bug
- **后果**：锚点边始终使用默认灰色 `#9e9e9e`，注释声称的"P1-4 修复"实际未生效

#### 问题 12：富文本类型配置三处不一致

- **位置**：`constants.ts` 第 25-36 行、`block-registry.tsx` 第 50、56-69 行
- **问题**：`RICH_TEXT_BLOCK_TYPES`（10项）vs `RICH_TEXT_TYPES`（2项）vs `BLOCK_REGISTRY.isRichText`（2项）三处定义语义冲突
- **违反规则**：配置一致性
- **后果**：`isRichTextBlock()` 与 `RICH_TEXT_BLOCK_TYPES` 行为完全相反，调用方混用会产生矛盾结果

#### 问题 13：多处 `as` 断言违规

- **位置**：
  - `node-summary.ts` 第 28-55 行：`as { html?: string; ... }` 将联合类型转为内联接口
  - `rf-mappers.ts` 第 81、95 行：`as Record<string, unknown>`
  - `block-renderer.tsx` 第 107、115、121、125 行：4 处 `as XxxBlockData`
  - `lesson-plan-readonly-view.tsx` 第 91 行：`as Edge[]`
  - `inline-question-editor.tsx` 第 34 行：`as typeof QUESTION_TYPES[number]`
  - `data-access-knowledge.ts` 第 24、39、67、82 行：4 处 `as` 断言
  - `use-lesson-plan-editor.ts` 第 137、138、154、155 行：4 处 `as` 断言
  - 4 个页面文件中 `findChapter` 的 `as typeof chapters`（冗余断言）
  - `actions.ts` 第 144 行：`as unknown as LessonPlanDocument` 双重断言
- **违反规则**：项目规则 "禁止 `as` 断言（除类型收窄外）"
- **后果**：类型安全被绕过，潜在运行时错误

#### 问题 14：`actions-ai.ts`/`actions-kp.ts` 隐式 any

- **位置**：`actions-ai.ts` 第 34 行 `Array.isArray(doc.nodes)` 收窄为 `any[]`；`actions-kp.ts` 第 30 行 `let kps;` 隐式 any
- **违反规则**：项目规则 "禁止 `any`"
- **后果**：类型安全缺失

### 2.4 i18n 与错误处理问题（P1）

#### 问题 15：`actions-ai.ts`/`actions-kp.ts` 未使用 `handleActionError` 和 `translateFieldErrors`

- **位置**：`actions-ai.ts` 第 23、41-45 行；`actions-kp.ts` 第 23、42-46 行
- **问题**：Zod 错误未调用 `translateFieldErrors()` 翻译；catch 块未使用 `handleActionError()`，非权限错误被静默吞掉，无日志
- **违反规则**：项目规则 "所有 Server Action catch 块改用 handleActionError"
- **后果**：错误不可观测，用户看到未翻译的 i18n 键

#### 问题 16：`block-renderer.tsx` 硬编码中文

- **位置**：`block-renderer.tsx` 第 130 行
- **问题**：`"未知 block 类型"` 直接硬编码为中文，整个文件未 import `useTranslations`
- **违反规则**：项目规则 "所有用户可见文本必须适配 i18n"
- **后果**：多语言环境下显示中文

#### 问题 17：`schema.ts` 多个 schema 缺少 i18n 错误消息

- **位置**：`schema.ts` 第 14-32 行
- **问题**：仅 `createLessonPlanSchema` 和 `publishLessonPlanHomeworkSchema` 部分字段使用 i18n 键，其他 6 个 schema 均未使用
- **违反规则**：项目规则 "Zod schema 错误消息应使用 i18n 键"
- **后果**：Zod 校验错误返回英文默认消息

### 2.5 a11y 与 UX 问题（P1/P2）

#### 问题 18：`inline-question-editor.tsx` Modal 缺少焦点陷阱

- **位置**：`inline-question-editor.tsx` 第 72-227 行
- **问题**：模态框设置了 `role="dialog"` 和 `aria-modal="true"`，但未实现焦点陷阱、Escape 关闭、焦点管理
- **违反规则**：a11y 键盘导航
- **后果**：键盘用户无法正常使用模态框

#### 问题 19：多处缺少 aria-label

- **位置**：`lesson-plan-editor.tsx` 第 239-243 行（标题输入框）、第 330-335 行（添加节点按钮）；`block-renderer.tsx` 第 71-102 行（4 个图标按钮）；`lesson-plan-readonly-view.tsx` 第 89-103 行（画布缺少 aria）
- **违反规则**：a11y 语义化标签
- **后果**：屏幕阅读器无法识别元素用途

#### 问题 20：`getLessonPlanStats` 的 archived 恒为 0

- **位置**：`data-access.ts` 第 553-566 行、`admin/lesson-plans/page.tsx` 第 63-72 行
- **问题**：WHERE 子句排除 archived 后硬编码返回 `archived: 0`，但 admin 页面仍渲染"已归档"统计卡片
- **违反规则**：数据准确性
- **后果**：统计卡片永远显示 0，具有误导性

### 2.6 性能与代码质量问题（P2）

#### 问题 21：`use-lesson-plan-editor.ts` 303 行超标

- **位置**：`hooks/use-lesson-plan-editor.ts`
- **问题**：文件 303 行，远超 Hook 80 行建议上限
- **违反规则**：项目规则 "自定义 Hook：建议 ≤ 80 行"
- **后果**：可维护性差

#### 问题 22：`findChapter` 在 4 个页面重复

- **位置**：teacher edit、admin view、student view、parent view
- **问题**：4 个页面文件中存在完全相同的 `findChapter` 递归函数实现
- **违反规则**：DRY 原则
- **后果**：代码重复，维护成本高

#### 问题 23：`lesson-plan-card.tsx` 4 个异步函数未使用 useCallback

- **位置**：`lesson-plan-card.tsx` 第 72、89、105、122 行
- **问题**：`handleArchive`/`handleDuplicate`/`handlePublish`/`handleUnpublish` 每次渲染重新创建
- **违反规则**：性能最佳实践
- **后果**：不必要渲染

#### 问题 24：多处 Tailwind 任意值

- **位置**：`lesson-plan-editor.tsx`（4处）、`node-edit-panel.tsx`（2处）、`inline-question-editor.tsx`（3处）
- **违反规则**：项目规则 "禁止使用任意值（`w-[137px]`），除非有充分理由并注释"

---

## 三、行业差距对比

### 3.1 与优秀 K12 产品的差距

| 维度 | 优秀实践 | 当前实现 | 差距影响 |
|------|----------|----------|----------|
| **权限隔离** | 家长仅能查看自己孩子班级的课案，学生仅能查看自己班级课案 | 家长/学生可查看全校已发布课案 | 信息泄露风险，违反 FERPA/GDPR 等隐私法规 |
| **数据一致性** | 发布作业等关键操作使用事务保证原子性 | 多步写操作无事务 | 部分失败导致数据不一致，需要人工修复 |
| **版本管理** | 版本创建/删除有严格的归属校验 | 版本创建/删除可越权操作 | 用户可篡改他人课案历史 |
| **a11y** | 模态框实现完整焦点管理，所有交互元素有 aria-label | Modal 无焦点陷阱，多处缺少 aria-label | 残障用户无法使用 |
| **错误可观测性** | 所有异常记录日志，有统一错误处理 | 部分Action 静默吞掉错误 | 生产问题无法排查 |
| **配置一致性** | 单一数据源定义 Block 类型属性 | 三处富文本配置冲突 | 运行时行为不可预测 |
| **模块解耦** | 通过接口抽象依赖，支持独立测试 | 直接 import AI 模块组件 | 无法独立测试，变更影响扩散 |

### 3.2 缺失的功能

- **课案协作**：优秀产品支持多位教师协作编辑同一课案，当前仅支持单教师
- **课案评课**：教研组长可对课案添加评课意见，当前缺失
- **课案资源库**：跨学期/学年的课案资源库检索，当前仅按教材章节组织
- **AI 智能备课**：基于课标自动生成教学目标/重难点，当前仅有知识点推荐

---

## 四、改进优先级建议

### P0（严重 — 安全/正确性，必须立即修复）

| # | 问题 | 改进方向 |
|---|------|----------|
| P0-1 | Parent/Student 路由未校验归属关系 | 修复 `buildScopeCondition` 的 `children`/`class_members` 分支，使用 `scope.classIds`/`scope.gradeIds`/`scope.childrenIds` 过滤；修复 `getLessonPlanById` 增加归属校验 |
| P0-2 | `createLessonPlanVersion` 未校验 planId 归属 | 事务开头增加 `lessonPlans.creatorId = userId` 校验 |
| P0-3 | `pruneAutoVersions` 无 userId 参数 | 增加 `userId` 参数，删除前校验 planId 归属 |
| P0-4 | `getLessonPlansByKnowledgePoint`/`getLessonPlansByQuestion` 无权限过滤 | 增加 `userId` 参数，过滤 `status != 'archived'`，按 creatorId/DataScope 限制 |
| P0-5 | `saveLessonPlanVersionAction` schema 不含 content | 定义 `lessonPlanDocumentSchema` 递归校验文档结构 |
| P0-6 | `publishLessonPlanHomeworkAction` homeworkTitle 传入 planId | 先查询课案标题再传入 |
| P0-7 | `getLessonPlansAction` params 未验证 | 定义 Zod schema，data-access 使用 `escapeLikePattern` |
| P0-8 | `rf-mappers.ts` 颜色映射 Bug | 传入节点 type 而非 nodeId |
| P0-9 | 富文本配置三处不一致 | 统一为单一数据源，删除冗余定义 |
| P0-10 | `block-renderer.tsx` 硬编码中文 + as 断言 | 使用 i18n + 类型守卫 |
| P0-11 | `node-edit-panel.tsx` 直接 import @/modules/ai | 通过 props 注入 AI 组件 |
| P0-12 | `publish-service.ts` 多步写操作无事务 | 包裹 `db.transaction` |
| P0-13 | admin/student/parent 列表页未包裹 Provider | 包裹 `LessonPlanProviderSetup` |

### P1（高 — 类型安全/规范一致性）

| # | 问题 | 改进方向 |
|---|------|----------|
| P1-1 | 多处 `as` 断言（node-summary/rf-mappers/block-renderer/readonly-view/inline-question-editor/data-access-knowledge/use-lesson-plan-editor/4页面） | 使用类型守卫替代 |
| P1-2 | `actions-ai.ts`/`actions-kp.ts` 隐式 any | 显式类型标注 |
| P1-3 | `actions-ai.ts`/`actions-kp.ts` 未用 handleActionError/translateFieldErrors | 统一错误处理 |
| P1-4 | `schema.ts` 多个 schema 缺 i18n 错误消息 | 补全 i18n 键 |
| P1-5 | `getLessonPlanStats` archived 恒为 0 | 修复统计逻辑 |
| P1-6 | `findChapter` 4 处重复 | 提取为 textbooks 共享工具 |
| P1-7 | `inline-question-editor.tsx` Modal 缺焦点陷阱 | 实现焦点管理 |
| P1-8 | 多处缺少 aria-label | 补全 a11y 属性 |
| P1-9 | `data-access-knowledge.ts` LIKE 未用 escapeLikePattern | 统一转义 |
| P1-10 | `actions-publish.ts` 重复 requirePermission | 复用 AuthContext |

### P2（中 — 性能/代码质量）

| # | 问题 | 改进方向 |
|---|------|----------|
| P2-1 | `use-lesson-plan-editor.ts` 303 行超标 | 拆分为多个 slice |
| P2-2 | `lesson-plan-card.tsx` 4 函数未 useCallback | 包裹 useCallback |
| P2-3 | 多处 Tailwind 任意值 | 提取设计令牌 |
| P2-4 | `lesson-plan-list.tsx` 缺筛选加载状态 | 增加 isLoading |
| P2-5 | `block-renderer.tsx` 废弃文件 | 评估是否可删除 |
| P2-6 | `ai-suggest.ts` prompt 硬编码中文 | 参数化注入 |

---

## 五、架构图同步说明

本次审计发现架构图需补充以下内容：

1. **依赖关系补充**：`node-edit-panel.tsx` 对 `@/modules/ai` 的直接依赖需在 `005_architecture_data.json` 的 `lesson_preparation.dependsOn` 中记录
2. **auditFixes 补充**：本次 V3 续续审计的修复记录（V3-23 ~ V3-35）需添加到 `005_architecture_data.json` 的 `auditFixes` 对象
3. **文件清单更新**：若 `use-lesson-plan-editor.ts` 拆分为多个 slice，需更新 `004` 文件清单和 `005` files 数组
4. **配置冲突修复**：`constants.ts` 与 `block-registry.tsx` 的富文本配置统一后，需更新 `004` 中两者的描述

---

## 六、实施计划

本报告所有问题将按 P0 → P1 → P2 顺序完整实施，实施过程中同步更新架构图 004/005。
