705 lines
42 KiB
Markdown
705 lines
42 KiB
Markdown
# 备课模块审计报告 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 项修复。**
|