docs(audit): add audit reports for grades, homework, lesson-preparation, messaging, permissions, question-bank, settings, textbooks

- Add grades-audit-report

- Add homework-audit-report and homework-exams-audit-report

- Add lesson-preparation-audit-report-v3 and v4

- Add messaging-audit-report

- Add permissions-audit-report

- Add question-bank-audit-report

- Add settings-profile-audit-report-v3

- Add textbooks-audit-report-v3
This commit is contained in:
SpecialX
2026-07-03 10:23:34 +08:00
parent 365c36d97b
commit 89b9e181d2
29 changed files with 13009 additions and 230 deletions

View File

@@ -0,0 +1,704 @@
# 备课模块审计报告 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 项修复。**