Files
NextEdu/bugs/lesson_preparation_bug_v2.md
SpecialX 978d9a8309
Some checks failed
Security / deep-security-scan (push) Failing after 20m5s
DR Drill / dr-drill (push) Failing after 1m31s
CI / scheduled-backup (push) Failing after 1m31s
CI / backup-verify (push) Has been skipped
CI / weekly-dr-drill (push) Failing after 0s
CI / build-deploy (push) Has been cancelled
CI / security-scan (push) Has been cancelled
feat: 新增备课模块并修复全模块 P0/P1/P2 缺陷
主要变更:

- 新增 lesson-preparation 模块: 备课编辑器、节点编辑、AI 建议、知识点选择、版本历史、作业发布

- 新增 shared 通用组件: charts/question-bank-filters/schedule-list/ui (chip-nav/filter-bar/page-header/stat-card/stat-item)

- 新增 student/admin 端 loading.tsx 与 error.tsx, 优化加载与错误态体验

- 新增 teacher/lesson-plans 页面 (列表/新建/编辑)

- 新增 drizzle 迁移 0002_tiny_lionheart 及 snapshot

- 新增 textbooks/schema.ts 与 exams/utils/normalize-structure.ts

- 修复 Tiptap v3 SSR hydration 崩溃 (rich-text-block immediatelyRender: false)

- 重构多模块 data-access/actions/组件, 修复权限校验与类型规范

- 同步架构文档 004/005 反映新增模块、导出、依赖关系

- 归档 bugs/* 测试报告与 e2e 测试脚本 (admin/parent/student/teacher web_test)
2026-06-22 01:06:16 +08:00

12 KiB
Raw Permalink Blame History

备课模块lesson-preparation审查报告 v2

审查日期2026-06-20 审查范围:src/modules/lesson-preparation/ 全部文件 + 路由页面 审查方式:代码审查 + Playwright 运行时测试 前置状态v1 已进行一次修正Tiptap setContent 参数、lint 错误等)


一、审查结论

维度 状态
编辑页可用性 已修复v1 遗留的 Tiptap SSR 崩溃)
功能完整性 ⚠️ 存在 7 个 P1 功能缺陷
代码质量 ⚠️ 存在 8 个 P2 规范违规
用户体验 ⚠️ 存在 5 个 P3 改进项
架构合规 三层架构正确,权限校验完整

二、本次已修复问题

[P0-已修复] Tiptap SSR immediatelyRender 未设置导致编辑页崩溃

文件rich-text-block.tsx

现象:编辑页显示 "Something went wrong!",控制台报错:

Error: Tiptap Error: SSR has been detected, please set `immediatelyRender` explicitly to `false` to avoid hydration mismatches.

原因Tiptap v3 的 useEditor 在 SSR 环境下默认会尝试立即渲染,导致 hydration mismatch。Next.js App Router 的客户端组件会经历 SSR 阶段,必须显式设置 immediatelyRender: false

修复:在 useEditor 配置中添加 immediatelyRender: false

验证Playwright 测试编辑页正常渲染,无控制台错误。


三、P1 功能缺陷(建议修复)

[P1-1] 版本回退后编辑器内容不刷新

文件lesson-plan-editor.tsx:173-175

现象:用户点击"回退到此版本"后,服务端 content 已更新,但编辑器界面仍显示旧内容。

原因onReverted 回调是空函数:

<VersionHistoryDrawer
  onReverted={() => { /* 触发页面刷新由父组件处理 */ }}
/>

修复建议:回退成功后调用 useLessonPlanEditor.getState() 重新拉取课案内容并 replaceDoc,或用 router.refresh() 刷新服务端数据。


[P1-2] 版本抽屉 loading 状态失效

文件version-history-drawer.tsx:27-39

现象:打开版本抽屉时"加载中..."永不显示。

原因loading 初始为 falseeffect 中从未调用 setLoading(true)

const [loading, setLoading] = useState(false);
useEffect(() => {
  if (!open) return;
  let cancelled = false;
  (async () => {
    const res = await getLessonPlanVersionsAction(planId); // 缺少 setLoading(true)
    if (cancelled) return;
    if (res.success && res.data) setVersions(res.data.versions);
    setLoading(false);
  })();
  // ...
}, [open, planId]);

修复建议:在 async IIFE 开头添加 setLoading(true)


[P1-3] 初始化 useEffect 依赖对象引用导致 store 被重置

文件lesson-plan-editor.tsx:55-63

现象:父组件 re-render 时,initialDoc 对象引用变化,触发 useEffect 重新执行 useLessonPlanEditor.setState(),覆盖用户正在编辑的内容。

原因

useEffect(() => {
  useLessonPlanEditor.setState({
    planId, title: initialTitle, doc: initialDoc, // ← 整个 doc 被重置
    isDirty: false, lastSavedAt: Date.now(),
  });
}, [planId, initialTitle, initialDoc]); // ← initialDoc 是对象,引用每次都变

修复建议:只依赖 planId,在 planId 变化时才初始化;或用 useRef 缓存 initialDoc 的原始引用。


[P1-4] 自动保存闭包问题导致保存旧内容

文件lesson-plan-editor.tsx:66-83

现象用户快速连续编辑时3 秒后保存的可能不是最新内容。

原因debounce 的 setTimeout 闭包了触发时的 editor.titleeditor.doc 快照。虽然 effect 依赖包含 editor.doc,但用户在 3 秒内继续编辑会创建新的 setTimeout旧的被 clearTimeout所以实际上保存的是最后一次 effect 触发时的快照。但 editor.titleeditor.doc 是 zustand 的订阅值,在 setTimeout 执行时可能已过期。

修复建议:在 setTimeout 回调中用 useLessonPlanEditor.getState() 获取最新值,而非闭包值。


[P1-5] 题库搜索无 debounce

文件question-bank-picker.tsx:32-46

现象:搜索输入每次按键都触发 server action 请求。

原因useEffect 依赖 filters,而 filters 在每次 onChange 时更新。

修复建议:对搜索输入添加 300ms debounce。


[P1-6] 课案列表搜索无 debounce

文件lesson-plan-filters.tsx:17

现象:搜索框每次按键触发 server action。

修复建议:添加 debounce 或使用 useTransition


[P1-7] inline-question-editor 知识点标注缺失

文件inline-question-editor.tsx:22

现象:课案内新建题目无法关联知识点。

原因kpIds 被硬编码为常量空数组:

const kpIds: string[] = [];

修复建议:添加知识点选择器 UI或复用 KnowledgePointPicker


四、P2 代码质量/架构问题

[P2-1] publish-service 用 JSON.parse(JSON.stringify()) 深拷贝

文件publish-service.ts:78-80

问题:性能差,且不支持 Date 等特殊类型。

建议:用 structuredClone() 或手动构造新对象。


[P2-2] publish-service 用非空断言 !

文件publish-service.ts:82-83

问题:违反项目规范"可选链后禁止跟非空断言"。

const newBlock = newContent.blocks.find((b) => b.id === input.blockId)!;

建议:添加 null 检查并抛出明确错误。


[P2-3] 多个组件用 alert()/confirm()

文件version-history-drawer.tsx:42, lesson-plan-card.tsx:48, inline-question-editor.tsx:26

问题:不符合现代 Web UI 规范,阻塞主线程。

建议:使用项目的 AlertDialog 组件(@/shared/components/ui/alert-dialog)或 sonner toast。


[P2-4] block-renderer 用 as never 类型断言

文件block-renderer.tsx:103,111,117,122

问题block.data as never 绕过类型检查,违反"禁止 as 断言"规范。

建议:用类型守卫函数根据 block.type 收窄 block.data 类型。


[P2-5] data-access-knowledge 用 LIKE 查 JSON 字段

文件data-access-knowledge.ts:14-17

问题like(lessonPlans.content, '%${id}%') 可能误匹配(如 ID 是另一个 ID 的子串),且无法用索引。

建议MySQL 8.0+ 可用 JSON_CONTAINS;或维护关联表。


[P2-6] buildScopeCondition switch 无 default 分支

文件data-access.ts:49-67

问题switch 未覆盖所有 DataScope 类型时无 fallback虽然 TypeScript 会报错但逻辑上不完整。

建议:添加 default 分支返回空条件或抛错。


[P2-7] lesson-plan-card 用 window.location.reload()

文件lesson-plan-card.tsx:39,50

问题:不符合 SPA 模式,导致整个页面重新加载。

建议:用 useRouter().refresh()revalidatePath 后自动刷新。


[P2-8] data-access-templates 用 as never 类型断言

文件data-access-templates.ts:66

type: b.type as never,

建议:用 b.type as BlockType 并添加运行时校验。


五、P3 用户体验改进

[P3-1] 编辑器无离开未保存提示

问题:用户有未保存内容时关闭/离开页面不会提示。

建议:监听 beforeunload 事件,isDirty 时弹出确认。


[P3-2] 添加环节菜单点击外部不关闭

文件lesson-plan-editor.tsx:150-166

问题:点击菜单外部不会关闭菜单。

建议:添加 useRef + mousedown 事件监听,或用 Radix DropdownMenu


[P3-3] 版本抽屉无预览功能

问题:版本列表只显示版本号和标签,无法预览版本内容差异。

建议:点击版本时展开内容预览,或显示 block 数量/摘要。


[P3-4] exercise-block 用 index 作为 key

文件exercise-block.tsx:67

{data.items.map((item, idx) => (
  <div key={idx} ...>

问题:删除/排序时可能导致 React 状态错乱。

建议:用 item.questionId 作为 key。


[P3-5] 编辑器无 loading 骨架屏

问题:编辑器初始化时无加载状态,网络慢时白屏。

建议:添加 Suspense fallback 或骨架屏。


六、架构合规性检查

检查项 状态 说明
三层架构app→modules→shared 路由层只调用 actions 和 data-access
模块间通过 data-access 通信 publish-service 通过 questions/exams/homework 的 data-access
Server Action 权限校验 所有 action 调用 requirePermission
Zod 校验 actions 使用 schema 校验输入
ActionState 返回类型 统一使用 ActionState
"server-only" 标注 所有 data-access 文件有 "server-only"
"use client" 标注 所有客户端组件有 "use client"
revalidatePath 精确刷新 创建/删除/回退后调用 revalidatePath
架构图同步 004/005 已同步

七、修复优先级建议

优先级 问题编号 描述 影响
P0 已修复 Tiptap SSR 崩溃 编辑页完全不可用
P1 P1-3 初始化 useEffect 重置 store 用户编辑内容丢失
P1 P1-4 自动保存闭包问题 保存旧内容
P1 P1-1 版本回退不刷新 回退后看到旧内容
P1 P1-2 版本抽屉 loading 失效 UX 体验差
P1 P1-7 inline 题目无知识点 功能缺失
P1 P1-5,6 搜索无 debounce 性能问题
P2 P2-1~8 代码规范 可维护性
P3 P3-1~5 UX 改进 体验优化

八、验证记录

验证项 命令 结果
TypeScript npx tsc --noEmit exit 0
ESLint npm run lint exit 0
数据库迁移 npm run db:migrate 成功
编辑页渲染 Playwright 测试 正常渲染,无错误
控制台错误 Playwright 捕获 无 error/warning

九、附录:测试截图

  • bugs/v2_list.png - 课案列表页
  • bugs/v2_new.png - 新建课案页
  • bugs/v2_edit.png - 编辑页(修复后正常)