# 备课模块审计报告

> 审查日期：2026-06-22
> 审查范围：`src/modules/lesson-preparation/**`（34 个文件）+ `src/app/(dashboard)/teacher/lesson-plans/**`（3 个路由页面）
> 架构图参考：`docs/architecture/004_architecture_impact_map.md` §2.27、`docs/architecture/005_architecture_data.json` `modules.lesson_preparation`
> 前置状态：v3 已完成节点图编辑器重构（React Flow）+ P1/P2 问题修复

---

## 一、现有实现概要

### 1.1 文件分布

| 层 | 路径 | 文件数 | 说明 |
|----|------|--------|------|
| 路由层 | `src/app/(dashboard)/teacher/lesson-plans/` | 3 个 `page.tsx` | 列表页 / 新建页 / 编辑页，均 `force-dynamic` |
| 模块层 - 数据 | `src/modules/lesson-preparation/` | 4 个 data-access + 2 个 service | data-access 按职责拆分（CRUD/versions/templates/knowledge） |
| 模块层 - Actions | `src/modules/lesson-preparation/` | 4 个 actions 文件 | actions/actions-publish/actions-ai/actions-kp |
| 模块层 - 组件 | `src/modules/lesson-preparation/components/` | 14 个组件 + 4 个 block + 1 个 node | 编辑器（NodeEditor + NodeEditPanel）、列表、卡片、筛选器、选择器、对话框 |
| 模块层 - Hook | `src/modules/lesson-preparation/hooks/` | 1 个（170 行） | `use-lesson-plan-editor.ts`（zustand 全局 store） |
| 模块层 - 其他 | `src/modules/lesson-preparation/` | types/schema/constants/seed-templates | 类型定义、Zod 校验、常量、种子 |

### 1.2 数据流

```
[Route] /teacher/lesson-plans/page.tsx
   └─▶ getLessonPlans({}, dataScope, userId)  + getSubjectOptions()
         └─▶ LessonPlanList (client) → getLessonPlansAction

[Route] /teacher/lesson-plans/new/page.tsx
   └─▶ TemplatePicker (client) → createLessonPlanAction

[Route] /teacher/lesson-plans/[planId]/edit/page.tsx
   ├─▶ getLessonPlanById(planId, userId)
   ├─▶ getTeacherClasses({ teacherId })
   └─▶ LessonPlanEditor (client)
         ├─▶ useLessonPlanEditor (zustand)
         ├─▶ NodeEditor (React Flow 画布)
         ├─▶ NodeEditPanel (侧边编辑)
         │     ├─▶ RichTextBlock / ExerciseBlock / TextStudyBlock / ReflectionBlock
         │     └─▶ KnowledgePointPicker → getKnowledgePointOptionsAction
         │           QuestionBankPicker → getQuestionsAction (跨模块)
         │           InlineQuestionEditor
         │           PublishHomeworkDialog → publishLessonPlanHomeworkAction
         ├─▶ VersionHistoryDrawer → getLessonPlanVersionsAction / revertLessonPlanVersionAction
         └─▶ 自动保存（debounce 3s）→ updateLessonPlanAction
             定时版本（30min）→ saveLessonPlanVersionAction

publish-service.publishLessonPlanHomework
   ├─▶ questions/data-access.createQuestionWithRelations
   ├─▶ exams/data-access.persistExamDraft
   ├─▶ ⚠️ 直接 db.insert(examQuestions)  ← 跨模块直查
   ├─▶ homework/data-access-write.createHomeworkAssignment
   └─▶ ⚠️ 直接 db.select(classEnrollments)  ← 跨模块直查
```

### 1.3 架构图记录情况

`004_architecture_impact_map.md` §2.27 与 `005_architecture_data.json` 已较完整记录该模块：
- ✅ 导出函数清单（dataAccess 22 个 + actions 15 个）
- ✅ 依赖关系（textbooks/questions/exams/homework/classes/files/shared/lib/ai/@xyflow/react）
- ✅ 文件清单（34 个）
- ✅ 数据结构 v1→v2 迁移说明
- ⚠️ **未记录** publish-service 中的两处跨模块直查（examQuestions / classEnrollments）
- ⚠️ **未记录** i18n 缺失状态
- ⚠️ **未记录** DataScope 过滤逻辑的安全隐患

---

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

### 2.1 跨模块直接查询数据库（P0 — 架构违规）

| 位置 | 问题 | 违反规则 |
|------|------|----------|
| [publish-service.ts:125-132](file:///e:/Desktop/CICD/src/modules/lesson-preparation/publish-service.ts#L125-L132) | 直接 `db.insert(examQuestions)` 插入考试题目表（归属 exams 模块） | "模块间只能通过对方 data-access 通信，**禁止跨模块直接查询数据库表**" |
| [publish-service.ts:37-43](file:///e:/Desktop/CICD/src/modules/lesson-preparation/publish-service.ts#L37-L43) | 直接 `db.select(classEnrollments)` 查询班级选课表（归属 classes 模块） | 同上 |

**原因**：发布作业时需要批量插入考试题目、查询班级学生，但 exams/classes 模块未暴露对应的跨模块写/读接口，开发者为图便利直接访问 DB。

**后果**：exams/classes 模块的表结构变更将直接破坏备课模块；数据完整性约束（如班级归属校验）被绕过；架构图与实现不一致，误导后续维护。

### 2.2 国际化完全缺失（P0 — 规范违规）

| 位置 | 问题 | 违反规则 |
|------|------|----------|
| [constants.ts:4-17](file:///e:/Desktop/CICD/src/modules/lesson-preparation/constants.ts#L4-L17) | `BLOCK_TYPE_LABELS` 硬编码中文（"教学目标"/"导入"/"新授"等 12 项） | "所有用户可见文本必须适配 i18n（使用 next-intl），提取翻译键" |
| [constants.ts:41-101](file:///e:/Desktop/CICD/src/modules/lesson-preparation/constants.ts#L41-L101) | `SYSTEM_TEMPLATES` 名称/hint 硬编码中文 | 同上 |
| [constants.ts:103-107](file:///e:/Desktop/CICD/src/modules/lesson-preparation/constants.ts#L103-L107) | `LESSON_PLAN_STATUS_LABELS` 硬编码中文 | 同上 |
| 所有组件 | "保存中..."/"未保存"/"已保存"/"添加节点"/"版本"/"画布为空"等数十处硬编码 | 同上 |
| 所有 actions | 返回中文错误消息（"获取课案列表失败"/"创建课案失败"等） | 同上 |
| [i18n/request.ts:22-29](file:///e:/Desktop/CICD/src/i18n/request.ts#L22-L29) | 未加载 `lesson-preparation.json` 翻译文件 | i18n 基础设施未接入 |
| `messages/` 目录 | **无 `lesson-preparation.json`** | 翻译文件缺失 |

**后果**：无法切换语言；维护时需逐文件改字符串；与项目其他已 i18n 的模块（dashboard/classes/auth）不一致。

### 2.3 类型安全：大量 `as` 断言（P1 — 规范违规）

| 位置 | 代码 | 违反规则 |
|------|------|----------|
| [data-access.ts:52-58](file:///e:/Desktop/CICD/src/modules/lesson-preparation/data-access.ts#L52-L58) | `content as { version?: number }` / `content as LessonPlanDocument` / `content as LessonPlanDocumentV1` | "禁止 `as` 断言（除非从 `unknown` 转换或测试中）" |
| [data-access.ts:174](file:///e:/Desktop/CICD/src/modules/lesson-preparation/data-access.ts#L174) | `rows as unknown as LessonPlanListItem[]` | 双重断言绕过类型检查 |
| [data-access.ts:194](file:///e:/Desktop/CICD/src/modules/lesson-preparation/data-access.ts#L194) | `row as unknown as LessonPlan` | 同上 |
| [data-access-templates.ts:40](file:///e:/Desktop/CICD/src/modules/lesson-preparation/data-access-templates.ts#L40) | `personalRows as unknown as LessonPlanTemplate[]` | 同上 |
| [data-access-knowledge.ts:25,43](file:///e:/Desktop/CICD/src/modules/lesson-preparation/data-access-knowledge.ts#L25) | `rows as unknown as LessonPlanListItem[]` | 同上 |
| [publish-service.ts:56](file:///e:/Desktop/CICD/src/modules/lesson-preparation/publish-service.ts#L56) | `rows[0] as unknown as {...}` | 同上 |
| [node-editor.tsx:39](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/node-editor.tsx#L39) | `data: { node: n } as Record<string, unknown>` | 断言绕过 React Flow 类型 |
| [node-edit-panel.tsx:61,69,78,82](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/node-edit-panel.tsx#L61) | `node.data as RichTextBlockData` / `as ExerciseBlockData` 等 | 联合类型未用类型守卫收窄 |
| [lesson-node.tsx:49](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/nodes/lesson-node.tsx#L49) | `data as { node: LessonPlanNode }` | 同上 |
| [inline-question-editor.tsx:76](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/inline-question-editor.tsx#L76) | `e.target.value as never` | `as never` 绕过类型检查 |

**后果**：类型系统形同虚设；运行时数据结构与类型声明不符时无法被编译器捕获；重构时易引入隐蔽 bug。

### 2.4 安全性：DataScope 过滤逻辑过宽（P1）

| 位置 | 问题 | 违反规则 |
|------|------|----------|
| [data-access.ts:93-111](file:///e:/Desktop/CICD/src/modules/lesson-preparation/data-access.ts#L93-L111) | `buildScopeCondition` 对 `class_taught`/`grade_managed`/`class_members`/`children` 四种 scope 统一返回 `creatorId = userId OR status = published` | "所有敏感数据查询必须在 data-access 层结合当前用户权限过滤" |
| 同上 | 教师可查看**所有** published 课案（不限学科/年级/班级） | 数据隔离不足 |
| 同上 | `class_members`（学生）/`children`（家长）scope 也返回 published 课案，但学生/家长角色未分配 `LESSON_PLAN_READ` 权限，**一旦分配则越权** | 权限边界依赖角色配置而非代码保证 |

**后果**：教师 A 可查看教师 B 的 published 课案（即使不同学科/年级）；未来若给 student/parent 开放只读权限，将立即暴露全部 published 课案。

### 2.5 错误与边界处理：仅路由级 + 阻塞式 UI（P1）

| 位置 | 问题 | 违反规则 |
|------|------|----------|
| 全模块 | 无按数据区块的 Error Boundary（版本抽屉/题库选择器/知识点选择器/发布对话框任一异常导致整页崩溃） | "每个独立的数据区块必须用 React Error Boundary 包裹" |
| 全模块 | 无 Suspense + 骨架屏（版本列表/题库列表/知识点列表加载时仅显示"加载中..."文字） | "异步数据使用 React Suspense + 骨架屏" |
| [version-history-drawer.tsx:47](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/version-history-drawer.tsx#L47) | `confirm("确认回退到 v${versionNo}？")` | 应使用 AlertDialog |
| [lesson-plan-card.tsx:52](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/lesson-plan-card.tsx#L52) | `confirm("确认归档此课案？")` | 同上 |
| [inline-question-editor.tsx:30](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/inline-question-editor.tsx#L30) | `alert("请输入题干")` | 应使用 toast |
| [text-study-block.tsx:39](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/blocks/text-study-block.tsx#L39) | `alert("请先在课文中选中一段文本")` | 同上 |
| [exercise-block.tsx:155](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/blocks/exercise-block.tsx#L155) | `window.location.reload()` | 应使用 `router.refresh()` |

**后果**：单个 Widget 故障导致整页不可用；`alert/confirm` 阻塞主线程且不可定制样式；`window.location.reload()` 丢失未保存的编辑器状态。

### 2.6 可测试性：纯逻辑与 UI 耦合 + 全局 store（P1）

| 位置 | 耦合的逻辑 | 违反规则 |
|------|-----------|----------|
| [use-lesson-plan-editor.ts](file:///e:/Desktop/CICD/src/modules/lesson-preparation/hooks/use-lesson-plan-editor.ts) | zustand **全局单例** store，组件直接 `useLessonPlanEditor()` 订阅 | "组合优先：逻辑复用一律抽取为自定义 hooks" — 全局 store 无法多实例、无法注入 mock |
| [data-access.ts:31-90](file:///e:/Desktop/CICD/src/modules/lesson-preparation/data-access.ts#L31-L90) | `migrateV1ToV2`/`normalizeDocument`/`buildInitialContent` 为纯函数但与 DB 操作同文件 | 纯函数应独立便于单测 |
| [lesson-node.tsx:24-43](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/nodes/lesson-node.tsx#L24-L43) | `getNodeSummary` 业务逻辑内联在组件中 | "数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks" |
| [node-editor.tsx:33-54](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/node-editor.tsx#L33-L54) | `rfNodes`/`rfEdges` 映射逻辑内联在组件 useMemo 中 | 同上 |

**后果**：无法对迁移/规范化/摘要逻辑做单元测试；编辑器无法多实例（如对比两个课案）；组件无法独立测试（依赖全局 store）。

### 2.7 可复用性：角色零共享 + 无配置驱动（P1）

| 维度 | 现状 | 违反规则 |
|------|------|----------|
| 角色覆盖 | 仅 teacher 角色可访问（admin 有权限但无 UI 入口）；student/parent 完全无法查看 published 课案 | "最大化复用：识别四个角色共用的 UI 块和业务逻辑块" |
| 配置驱动 | 无角色配置，新增角色需新建整套组件 | "采用配置驱动设计，例如通过角色配置决定该模块渲染哪些 Widget/子模块" |
| 数据服务注入 | 组件直接 import actions（`getLessonPlansAction`/`updateLessonPlanAction` 等），无法替换实现 | "通过定义 TypeScript 接口抽象数据依赖，使用 React Context 注入数据服务" |
| Block 渲染 | `NodeEditPanel` 用 if/else 链渲染 4 种 block 类型，新增 block 类型需改组件 | 应改为注册表/配置驱动 |

**后果**：无法支持 admin 查看全校课案统计、student/parent 查看教师发布的课案；未来新增角色（如教研组长）需重写模块；组件无法独立复用。

### 2.8 可访问性：缺失（P2）

| 位置 | 问题 | 违反规则 |
|------|------|----------|
| 所有图标按钮 | 无 `aria-label`（如 `<X className="w-4 h-4" />` 关闭按钮） | "语义化标签、ARIA 属性、键盘导航" |
| 所有模态对话框 | 无 `role="dialog"`/`aria-modal`/焦点陷阱 | 同上 |
| [node-editor.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/node-editor.tsx) | React Flow 画布无键盘导航支持（Tab/方向键无法聚焦/移动节点） | 同上 |
| [lesson-plan-filters.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/lesson-plan-filters.tsx) | `<select>` 无 `<label>` 关联 | 同上 |
| [exercise-block.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/blocks/exercise-block.tsx) | 题目列表用 `<div>` 非 `<ul>/<li>` | 语义化标签缺失 |

### 2.9 性能：全量 force-dynamic + 无流式渲染（P2）

| 位置 | 问题 | 违反规则 |
|------|------|----------|
| 所有 `page.tsx` | `export const dynamic = "force-dynamic"`，`Promise.all` 等全部数据就绪后才渲染 | "优先使用 React Server Components 获取初始数据；支持流式渲染" |
| 编辑器自动保存 | debounce 3s 但每次保存整个 `content` JSON（含全部 nodes/edges），无增量/diff | 大课案（100+ 节点）保存开销大 |
| [question-bank-picker.tsx](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/question-bank-picker.tsx) | 搜索时全量加载题目，无虚拟滚动 | 题库大时卡顿 |

### 2.10 监控：无埋点（P2）

| 位置 | 问题 | 违反规则 |
|------|------|----------|
| 全模块 | 无任何操作埋点（创建/保存/发布/回退/复制等关键操作未记录） | "监控：方案中预留关键操作埋点接口" |

---

## 三、行业差距对比

### 3.1 K12 备课模块主流设计模式

| 模式 | 行业实践（如希沃白板/钉钉教育/企业微信教育/PowerSchool） | 本项目现状 | 差距影响 |
|------|----------|------------|----------|
| **多角色协同** | 教研组长审核课案、教师共享/协作编辑、学生查看预习案、家长查看教学进度 | 仅教师可访问 | 教研活动无法线上化；学生/家长无法了解教学进度 |
| **课案库/共享** | 校内/年级/学科共享课案库，支持 fork/收藏/评分 | 无共享机制 | 优质课案无法复用，教师重复造轮子 |
| **模板生态** | 学科专属模板、区级/市级优秀模板下发、模板市场 | 仅 5 个系统模板（内存常量） | 模板覆盖面不足，无法按学科/课型细分 |
| **教材联动** | 拖拽教材章节自动生成课案骨架、教材资源一键插入 | 仅按章节过滤列表，无深度联动 | 备课效率低，需手动复制教材内容 |
| **学情数据嵌入** | 课案中嵌入上次作业正确率/知识点掌握度，辅助教学决策 | 无 | 教师备课缺乏数据支撑，无法精准教学 |
| **协作编辑** | 多人实时协作（如腾讯文档/飞书文档模式） | 单人编辑 | 教研组无法协同备课 |
| **导出/打印** | 一键导出 PDF/Word/图片，支持打印备课稿 | 无 | 教师需手动截图，无法线下使用 |
| **版本对比** | 版本间 diff 可视化（高亮增删改） | 仅列表，无 diff | 教师无法直观看到版本差异 |
| **AI 辅助** | AI 生成教学目标/活动设计/习题/板书，AI 评课 | 仅 AI 推荐知识点 | AI 能力单薄，未覆盖备课全流程 |
| **资源管理** | 附件/图片/视频/音频统一管理，支持拖拽上传 | 依赖 files 模块但未深度集成 | 多媒体备课体验差 |
| **课案与作业联动** | 课案直接下发为作业/考试，作业数据回流课案 | 有发布为作业功能但单向（无回流） | 教师无法基于作业反馈优化课案 |
| **空状态引导** | 新手引导/示例课案/视频教程 | 仅"暂无课案"文字 | 新教师上手慢 |

### 3.2 各角色差距详述

**Teacher（当前唯一角色）**：
- 缺少教研组协作入口
- 缺少学情数据嵌入（上次作业正确率/常见错误）
- 缺少课案库/共享机制
- 缺少导出/打印
- 缺少版本 diff
- AI 能力仅限知识点推荐，未覆盖目标/活动/习题生成

**Admin（有权限无 UI）**：
- 无法查看全校课案统计（按学科/年级/教师分布）
- 无法管理/下发校级/区级模板
- 无法审核/下架不当课案

**Student（无权限无 UI）**：
- 无法查看教师发布的预习案/复习案
- 无法查看课案中的学习目标/重难点

**Parent（无权限无 UI）**：
- 无法了解孩子本周学习内容/教学进度
- 无法查看教师发布的教学计划

---

## 四、改进优先级建议

### P0（紧急 — 架构合规与安全）

| # | 问题 | 改进方向 |
|---|------|----------|
| P0-1 | publish-service 跨模块直查 examQuestions/classEnrollments | exams 模块新增 `addExamQuestions(examId, items)` 跨模块写接口；classes 模块新增 `getStudentIdsByClassIds(classIds)` 跨模块读接口（若已存在则复用） |
| P0-2 | i18n 完全缺失 | 创建 `messages/{zh-CN,en}/lesson-preparation.json`；`i18n/request.ts` 加载该文件；所有组件接入 `useTranslations`/`getTranslations`；constants 中的标签改为 i18n 键 |
| P0-3 | DataScope 过滤过宽 | `buildScopeCondition` 对 `class_taught` scope 增加学科/年级过滤（`subjectId IN teacher.subjects AND gradeId IN teacher.grades`）；对 `class_members`/`children` 仅允许查看 published 且关联自己班级/孩子的课案 |

### P1（较严重 — 架构与质量）

| # | 问题 | 改进方向 |
|---|------|----------|
| P1-1 | 类型安全：大量 `as` 断言 | data-access 用 Drizzle 的 `inferSelect` 类型；`normalizeDocument` 用类型守卫收窄；block data 用判别联合 + 类型守卫函数 |
| P1-2 | 错误边界缺失 | 创建 `LessonPlanErrorBoundary` 组件，包裹版本抽屉/题库选择器/知识点选择器/发布对话框；每个区块独立 fallback |
| P1-3 | 骨架屏缺失 | 为版本列表/题库列表/知识点列表创建 `Skeleton` 组件，配合 Suspense |
| P1-4 | alert/confirm/window.location.reload | 替换为 `AlertDialog`（shadcn）+ `sonner` toast + `router.refresh()` |
| P1-5 | 全局 zustand store 无法多实例/测试 | 改为 React Context + useReducer，或保留 zustand 但通过 Context 注入 store 实例 |
| P1-6 | 纯逻辑与 UI 耦合 | 抽取 `lib/document-migration.ts`（migrateV1ToV2/normalizeDocument/buildInitialContent）、`lib/node-summary.ts`（getNodeSummary）、`lib/rf-mappers.ts`（toRfNodes/toRfEdges） |
| P1-7 | 角色零共享 + 无配置驱动 | 定义 `LessonPlanRoleConfig`（角色 → 可见 Widget/操作）；定义 `LessonPlanDataService` 接口，各角色不同实现；通过 `LessonPlanProvider` 注入 |
| P1-8 | Block 渲染 if/else 链 | 改为注册表模式：`BLOCK_REGISTRY: Record<BlockType, BlockComponent>`，新增 block 类型只需注册 |

### P2（优化 — 体验与扩展）

| # | 问题 | 改进方向 |
|---|------|----------|
| P2-1 | a11y 缺失 | 图标按钮加 `aria-label`；模态对话框加 `role="dialog"`/`aria-modal`/焦点陷阱；`<select>` 关联 `<label>`；题目列表用 `<ul>/<li>` |
| P2-2 | 无流式渲染 | 列表页改用 RSC + `<Suspense>` 包裹各区块；编辑器初始数据用 RSC 获取 |
| P2-3 | 无单测 | 为 `lib/document-migration.ts`/`lib/node-summary.ts`/`lib/rf-mappers.ts`/`buildScopeCondition` 添加单测 |
| P2-4 | 无监控埋点 | 预留 `trackLessonPlanEvent(event, payload)` 接口，在 create/save/publish/revert/duplicate 处调用 |
| P2-5 | 无导出/打印 | 新增 `exportLessonPlanToPdf`/`exportLessonPlanToDocx` |
| P2-6 | 无版本 diff | 新增 `diffDocuments(docA, docB)` 纯函数 + 可视化组件 |
| P2-7 | AI 能力单薄 | 扩展 `ai-suggest.ts`：`suggestObjectives`/`suggestActivities`/`suggestExercises`/`suggestBlackboard` |

---

## 五、架构图同步说明

本次审计发现架构图存在以下遗漏，需在实现后同步更新：

### 5.1 `004_architecture_impact_map.md` 需补充

1. **§2.27 已知问题**：新增"publish-service 跨模块直查 examQuestions/classEnrollments"（P0-1）
2. **§2.27 文件清单**：新增 `lib/document-migration.ts`、`lib/node-summary.ts`、`lib/rf-mappers.ts`、`components/lesson-plan-error-boundary.tsx`、`components/lesson-plan-skeleton.tsx`、`providers/lesson-plan-provider.tsx`、`config/role-config.ts`、`services/data-service.ts`（接口）
3. **§2.27 依赖关系**：标注 publish-service 改为通过 exams/classes data-access 跨模块通信
4. **§2.27 已知问题**：新增"i18n 缺失"（P0-2）、"DataScope 过滤过宽"（P0-3）

### 5.2 `005_architecture_data.json` 需修改

1. `modules.lesson_preparation.exports.dataAccess`：新增 exams/classes 跨模块接口调用说明
2. `modules.lesson_preparation.files`：新增上述 8 个文件
3. `modules.lesson_preparation.dependencies`：确认 exams/classes 已存在（✅），但需标注 publish-service 不再直查
4. `modules.lesson_preparation` 新增 `i18n` 字段：`{ "namespace": "lesson-preparation", "status": "planned" }`

### 5.3 无需修改部分

- 数据库表结构（lessonPlans/lessonPlanVersions/lessonPlanTemplates）无变更
- 权限点（LESSON_PLAN_*）无变更
- 路由（3 个页面）无变更
