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
22 KiB
备课模块审计报告
审查日期: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.jsonmodules.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.ts(zustand 全局 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 | buildScopeCondition 对 class_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 错误与边界处理:仅路由级 + 阻塞式 UI(P1)
| 位置 | 问题 | 违反规则 |
|---|---|---|
| 全模块 | 无按数据区块的 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 耦合 + 全局 store(P1)
| 位置 | 耦合的逻辑 | 违反规则 |
|---|---|---|
| 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 actions(getLessonPlansAction/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.json;i18n/request.ts 加载该文件;所有组件接入 useTranslations/getTranslations;constants 中的标签改为 i18n 键 |
| P0-3 | DataScope 过滤过宽 | buildScopeCondition 对 class_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 | 替换为 AlertDialog(shadcn)+ sonner toast + router.refresh() |
| P1-5 | 全局 zustand store 无法多实例/测试 | 改为 React Context + useReducer,或保留 zustand 但通过 Context 注入 store 实例 |
| P1-6 | 纯逻辑与 UI 耦合 | 抽取 lib/document-migration.ts(migrateV1ToV2/normalizeDocument/buildInitialContent)、lib/node-summary.ts(getNodeSummary)、lib/rf-mappers.ts(toRfNodes/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.ts:suggestObjectives/suggestActivities/suggestExercises/suggestBlackboard |
五、架构图同步说明
本次审计发现架构图存在以下遗漏,需在实现后同步更新:
5.1 004_architecture_impact_map.md 需补充
- §2.27 已知问题:新增"publish-service 跨模块直查 examQuestions/classEnrollments"(P0-1)
- §2.27 文件清单:新增
lib/document-migration.ts、lib/node-summary.ts、lib/rf-mappers.ts、components/lesson-plan-error-boundary.tsx、components/lesson-plan-skeleton.tsx、providers/lesson-plan-provider.tsx、config/role-config.ts、services/data-service.ts(接口) - §2.27 依赖关系:标注 publish-service 改为通过 exams/classes data-access 跨模块通信
- §2.27 已知问题:新增"i18n 缺失"(P0-2)、"DataScope 过滤过宽"(P0-3)
5.2 005_architecture_data.json 需修改
modules.lesson_preparation.exports.dataAccess:新增 exams/classes 跨模块接口调用说明modules.lesson_preparation.files:新增上述 8 个文件modules.lesson_preparation.dependencies:确认 exams/classes 已存在(✅),但需标注 publish-service 不再直查modules.lesson_preparation新增i18n字段:{ "namespace": "lesson-preparation", "status": "planned" }
5.3 无需修改部分
- 数据库表结构(lessonPlans/lessonPlanVersions/lessonPlanTemplates)无变更
- 权限点(LESSON_PLAN_*)无变更
- 路由(3 个页面)无变更