Files
NextEdu/docs/architecture/audit/lesson-preparation-audit-report.md
SpecialX 20691f53ce feat(lesson-preparation): 备课模块审计重构 — 跨模块解耦 + i18n + 纯函数抽取 + 错误边界
P0-1 跨模块直查修复:publish-service 不再直查 examQuestions 表,新增 exams/data-access.addExamQuestions 接口,复用 classes/data-access.getStudentIdsByClassIds

P0-2 i18n 接入:新增 zh-CN/en 翻译文件,注册 lessonPreparation 命名空间,17 个组件改造为 useTranslations/getTranslations

P1 纯函数抽取:lib/document-migration.ts(类型守卫替代 as 断言)、lib/node-summary.ts(翻译函数注入)、lib/rf-mappers.ts

P1 错误边界+骨架屏:新增 LessonPlanErrorBoundary 和 4 个 Skeleton 组件

P1 Block 注册表:新增 config/block-registry.tsx(BlockRenderer 组件),node-edit-panel 重构为配置驱动渲染

P1 其他修复:exercise-block 改用 router.refresh(),node-editor/lesson-node 复用 lib/ 纯函数

架构图同步:更新 004 和 005 文档

Refs: docs/architecture/audit/lesson-preparation-audit-report.md
2026-06-22 16:17:58 +08:00

22 KiB
Raw Blame History

备课模块审计报告

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


一、现有实现概要

1.1 文件分布

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

1.2 数据流

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

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

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

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

1.3 架构图记录情况

004_architecture_impact_map.md §2.27 与 005_architecture_data.json 已较完整记录该模块:

  • 导出函数清单dataAccess 22 个 + actions 15 个)
  • 依赖关系textbooks/questions/exams/homework/classes/files/shared/lib/ai/@xyflow/react
  • 文件清单34 个)
  • 数据结构 v1→v2 迁移说明
  • ⚠️ 未记录 publish-service 中的两处跨模块直查examQuestions / classEnrollments
  • ⚠️ 未记录 i18n 缺失状态
  • ⚠️ 未记录 DataScope 过滤逻辑的安全隐患

二、现存问题与原因分析

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

位置 问题 违反规则
publish-service.ts:125-132 直接 db.insert(examQuestions) 插入考试题目表(归属 exams 模块) "模块间只能通过对方 data-access 通信,禁止跨模块直接查询数据库表"
publish-service.ts:37-43 直接 db.select(classEnrollments) 查询班级选课表(归属 classes 模块) 同上

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

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

2.2 国际化完全缺失P0 — 规范违规)

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

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

2.3 类型安全:大量 as 断言P1 — 规范违规)

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

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

2.4 安全性DataScope 过滤逻辑过宽P1

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

后果:教师 A 可查看教师 B 的 published 课案(即使不同学科/年级);未来若给 student/parent 开放只读权限,将立即暴露全部 published 课案。

2.5 错误与边界处理:仅路由级 + 阻塞式 UIP1

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

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

2.6 可测试性:纯逻辑与 UI 耦合 + 全局 storeP1

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

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

2.7 可复用性:角色零共享 + 无配置驱动P1

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

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

2.8 可访问性缺失P2

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

2.9 性能:全量 force-dynamic + 无流式渲染P2

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

2.10 监控无埋点P2

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

三、行业差距对比

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

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

3.2 各角色差距详述

Teacher当前唯一角色

  • 缺少教研组协作入口
  • 缺少学情数据嵌入(上次作业正确率/常见错误)
  • 缺少课案库/共享机制
  • 缺少导出/打印
  • 缺少版本 diff
  • AI 能力仅限知识点推荐,未覆盖目标/活动/习题生成

Admin有权限无 UI

  • 无法查看全校课案统计(按学科/年级/教师分布)
  • 无法管理/下发校级/区级模板
  • 无法审核/下架不当课案

Student无权限无 UI

  • 无法查看教师发布的预习案/复习案
  • 无法查看课案中的学习目标/重难点

Parent无权限无 UI

  • 无法了解孩子本周学习内容/教学进度
  • 无法查看教师发布的教学计划

四、改进优先级建议

P0紧急 — 架构合规与安全)

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

P1较严重 — 架构与质量)

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

P2优化 — 体验与扩展)

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

五、架构图同步说明

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

5.1 004_architecture_impact_map.md 需补充

  1. §2.27 已知问题:新增"publish-service 跨模块直查 examQuestions/classEnrollments"P0-1
  2. §2.27 文件清单:新增 lib/document-migration.tslib/node-summary.tslib/rf-mappers.tscomponents/lesson-plan-error-boundary.tsxcomponents/lesson-plan-skeleton.tsxproviders/lesson-plan-provider.tsxconfig/role-config.tsservices/data-service.ts(接口)
  3. §2.27 依赖关系:标注 publish-service 改为通过 exams/classes data-access 跨模块通信
  4. §2.27 已知问题:新增"i18n 缺失"P0-2、"DataScope 过滤过宽"P0-3

5.2 005_architecture_data.json 需修改

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

5.3 无需修改部分

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