Files
NextEdu/docs/architecture/audit/archive/lesson-preparation-audit-report-v4.md

705 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 备课模块审计报告 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
#### 问题 1Parent/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"
#### 问题 29 个 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只能通过注入的接口调用"
#### 问题 54 个对话框/抽屉缺失 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键盘导航"
#### 问题 64 个对话框/抽屉 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 + 骨架屏"
#### 问题 8Error 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"
#### 问题 126 个 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
#### 问题 193 个组件用 `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 模式
#### 问题 203 个 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"
#### 问题 213 个 view 页错误处理不一致
- **位置**admin/view 用 `notFound()` 抛 404parent/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")` 风格不一致
#### 问题 26data-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 原则
#### 问题 27data-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。
**示例 1LessonPlanCard 多角色通过 props 注入而非分支**
```tsx
<LessonPlanCard
plan={plan}
actionsSlot={
<>
{canEdit && <EditButton planId={plan.id} />}
{canPublish && <PublishButton planId={plan.id} />}
{canDuplicate && <DuplicateButton planId={plan.id} />}
</>
}
/>
```
**示例 2AI 内容生成通过 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 FocusTrapP1-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 项修复。**