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

42 KiB
Raw Blame History

备课模块审计报告 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.mddocs/architecture/004_architecture_impact_map.md §2.27、docs/architecture/005_architecture_data.json modules.lesson_preparation 前序报告:lesson-preparation-audit-report-v3.md2026-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-3as 断言清零V2-3/V3-17/V3-22、组件完全通过 service 调用V3-16/V3-20、a11y + 类型守卫V3-7/V3-8、findChapterById 共享V3-42useCallback 性能优化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 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()"

问题 3textbook-segments.tsx 锚点段落键盘用户完全不可操作 — a11y 严重缺陷

  • 位置textbook-segments.tsx L47、L76
  • 问题:锚点 span 仅绑定 onClickrole="button" / tabIndex={0} / onKeyDown
  • 直接后果:键盘用户、读屏器用户完全无法触发锚点跳转/高亮,违反 WCAG 2.1 AA 的 2.1.1 键盘可访问性
  • 违反规则:项目规则 "a11y语义化标签、ARIA 属性、键盘导航"

2.2 架构与解耦问题P0/P1

问题 4default-data-service.ts 直接 import 其他业务模块的 actions

  • 位置default-data-service.ts L21
    import { getQuestionsAction } from "@/modules/questions/actions"
    
  • 问题:项目规则要求"模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access只能通过注入的接口调用"。虽收口在 service 实现层而非组件直接 import违反了"完全解耦"原则——本模块对 questions/actions 形成编译期硬依赖
  • 直接后果questions 模块 actions 签名变更会破坏本模块编译;无法在不引入 questions 模块的情况下独立测试本模块
  • 违反规则:审计任务要求 "完全解耦:模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access只能通过注入的接口调用"

问题 54 个对话框/抽屉缺失 FocusTrap — 焦点陷阱不完整

问题 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 覆盖不全

  • 位置
  • 问题:项目规则要求"每个独立的数据区块必须用 React Error Boundary 包裹"。当前仅 node-edit-panel.tsx L161-173 包裹 BlockRenderer,其他独立数据区块无兜底
  • 直接后果:单个 Picker 数据加载失败会冒泡到路由级 error.tsx,整个页面变白屏
  • 违反规则:项目规则 "每个独立的数据区块必须用 React Error Boundary 包裹"

2.3 类型安全与代码质量P1/P2

问题 9i18n-errors.ts L24 无注释的 as 断言

  • 位置i18n-errors.ts L24
    t(msg as Parameters<typeof t>[0])
    
  • 问题:从 string 收窄到 next-intl 的 i18n key 联合类型,无注释说明
  • 违反规则:项目规则 "禁止 as 断言(除非从 unknown 转换或测试中,需注释原因)"

问题 10硬编码颜色字符串散落多处未使用 CSS 变量

  • 位置
  • 问题V3 已修复 MiniMap 颜色硬编码V3-18但节点本身、画布背景、NODE_COLORS 仍硬编码
  • 违反规则:项目规则 "设计令牌在 src/app/globals.css 中使用 CSS 变量定义"

问题 11exercise-block.tsx 使用原生 <a> 标签

  • 位置exercise-block.tsx L123-128
    <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 稳定性最佳实践(隐含在"代码质量规则"中)

问题 13template-picker.tsx 字符串拼接动态类名

  • 位置template-picker.tsx L265-269、L296-300
    className={`... ${selected === tpl.id ? "border-primary ..." : "border-transparent ..."}`}
    
  • 问题:违反"禁止字符串拼接动态类名"规则(应使用 cn()
  • 违反规则:项目规则 "禁止字符串拼接动态类名(bg-${color}-500" + "使用 cn() 工具函数管理条件类名"
  • 波及范围:本模块全部 21 个组件均未使用 cn(),全部用模板字符串拼接,但 template-picker.tsx 的条件分支最复杂

问题 14block-registry.tsx 缺失 "use client" 指令

  • 位置block-registry.tsx L1
  • 问题BlockRenderer 函数L88返回 ReactElement 并 switch 渲染各 client block 组件,本文件应明确标注 "use client"
  • 违反规则:项目规则 "需要交互时才添加 \"use client\"(必须位于文件第一行)"

问题 15NodeEditPanel.knownTypesBLOCK_REGISTRY 重复定义(违反 DRY

  • 位置node-edit-panel.tsx L241
  • 问题:本地维护 knownTypes 数组与 BLOCK_REGISTRY 的 keys 完全重复
  • 违反规则:项目规则隐含的 DRY 原则

问题 16inline-question-editor.tsxQUESTION_TYPEStype-guards.VALID_QUESTION_TYPES 重复

问题 17ROLE_CONFIGS 缺失 gradeHead 角色,与 LessonPlanCard.viewMode 不一致

  • 位置lesson-plan-provider.tsx L255-260 ROLE_CONFIGS vs lesson-plan-card.tsx L50 viewMode 类型包含 "gradeHead"
  • 问题:组件类型支持 gradeHead 视图,但 Provider 配置表无对应配置
  • 直接后果gradeHead 用户访问时会静默回退到 TEACHER_ROLE_CONFIG可能误显示教师操作按钮

问题 18useRoleConfig 静默回退教师配置,无开发环境告警

  • 位置lesson-plan-provider.tsx L319-322
  • 问题Provider 外调用 useRoleConfig() 时静默回退到 TEACHER_ROLE_CONFIG,无 console.warn
  • 直接后果:开发者误用时不报警,可能在生产环境暴露教师操作按钮给非教师角色

2.4 性能与代码组织P2

问题 193 个组件用 Promise.resolve().then() / queueMicrotask 规避同步 setState

问题 203 个 view 页 textbook/chapters 串行查询,可并行化

  • 位置parent/lesson-plans/[planId]/view/page.tsxstudent/lesson-plans/[planId]/view/page.tsxadmin/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 组件

问题 22block-renderer.tsx 已 @deprecated 但仍存在

  • 位置block-renderer.tsx L3-6 注释
  • 问题:已被 NodeEditor 替代,但保留 195 行废弃代码
  • 直接后果:维护负担、可能被误用

问题 23anchor-injector.tsskipPatternsmarkdownToPlainText 逻辑不一致

  • 位置anchor-injector.ts L117-123 vs L26-51
  • 问题:前者用 ^ 多行匹配,后者用 gm,可能导致偏移映射与纯文本转换结果不一致,锚点定位偏差

问题 24use-lesson-plan-editor.ts Zustand store 319 行,超出 Hook ≤80 行建议

  • 位置use-lesson-plan-editor.ts
  • 问题:项目规则原文 "自定义 Hook ≤ 80 行"。虽然 Zustand store 性质与 React hook 不同(集中定义 state + actions但仍建议拆分为多个 slice
  • 违反规则:项目规则 "自定义 Hook建议 ≤ 80 行"

问题 25actions.ts L187 返回原始字符串而非 i18n key

  • 位置actions.ts L187
    return { success: false, message: "PLAN_NOT_FOUND" }
    
  • 问题:与文件其余部分使用 t("error.xxx") 风格不一致

问题 26data-access-templates.ts 与 data-access.ts 类型守卫重复定义

  • 位置data-access-templates.ts L19-50 与 lib/type-guards.ts L30-49 完全重复
  • 问题isTemplateType/isTemplateScope/mapRowToTemplate 在两个文件 + lib/type-guards.ts 三处定义
  • 违反规则DRY 原则

问题 27data-access-knowledge.ts 两处 .map(r => ({ ... })) 完全重复

  • 位置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.tsxstudent/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} + onKeyDownEnter/Space 触发 onClick并补全 aria-label
P0-4 default-data-service 直接 import questions/actions 通过 LessonPlanDataService 接口注入 QuestionService,由 lesson-plan-provider-setup.tsx 在 app 层组合 questions/data-access.getQuestionsdata-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 内对 NodeEditorVersionHistoryDrawer、各 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} 改为业务唯一 keyhomework-${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 任何其他业务模块(questionsexamshomeworkclassestextbooksaifiles)的 actions 或 data-access所有跨模块依赖通过 LessonPlanDataService 接口的"方法契约"定义,由 lesson-plan-provider-setup.tsx 在 app 层注入具体实现。

接口契约扩展(lesson-plan-provider.tsx

// 新增对外部模块的抽象接口
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

"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-accessdata-access→data-access 合规)。

publish-service.ts 改为接收已注入的 service 对象,不再直接 import @/modules/questions/data-access 等。

5.2 组合优先:通过 children/slots/render props 实现灵活性

目标:所有 UI 通过组件组合实现,逻辑复用一律抽取为自定义 hooks。

示例 1LessonPlanCard 多角色通过 props 注入而非分支

<LessonPlanCard
  plan={plan}
  actionsSlot={
    <>
      {canEdit && <EditButton planId={plan.id} />}
      {canPublish && <PublishButton planId={plan.id} />}
      {canDuplicate && <DuplicateButton planId={plan.id} />}
    </>
  }
/>

示例 2AI 内容生成通过 render props

<LessonPlanEditor
  aiContentGenerator={(ctx: AiContext) => <AiLessonContentGenerator {...ctx} />}
/>

示例 3跨模块逻辑抽取为 hooks

// 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 等新增键):

// 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 内对 NodeEditorVersionHistoryDrawer、各 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 可测试性

  • 纯逻辑全部放入纯函数或 hookslib/ 已有 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 注册表),需扩展:

// 扩展 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()
  • 页面层补齐 assertPlanInScopeP0-1

监控埋点

  • LessonPlanTracker 接口已预留V2-6 实现 useLessonPlanTrackerSafe
  • 6 个关键操作已埋点create/save/publish/revert/duplicate/archive
  • 建议扩展埋点a11y 违规、Error Boundary 触发、节点图加载时长

六、文档实施计划

第一阶段本次审计内实施P0 + P1 + P2

按本报告"四、改进优先级建议"中的 P0、P1、P2 共 30 项全部完成代码修改,并运行 npm run lintnpx 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.jsonmodules.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 项修复。