Files
NextEdu/bugs/lesson_preparation_bug_v3.md
SpecialX 27db170c0a docs: update architecture docs, audit reports, and bug tracking
- Update architecture impact map, data, feature checklist, gap audit

- Add audit reports for dashboard, exam-homework, grades-diagnostic, settings-profile, textbooks

- Update bug reports (admin, teacher, lesson-preparation, others, shared)

- Update coding standards, DR plan, design docs, and README
2026-06-23 17:36:18 +08:00

297 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 备课模块lesson-preparation审查报告 v3
> 审查日期2026-06-22
> 审查范围:`src/modules/lesson-preparation/` 全部 34 个文件 + 3 个路由页面
> 审查方式:代码审查 + Playwright 运行时测试
> 前置状态v2 已完成节点图编辑器重构React Flow+ P1 问题修复
---
## 一、审查结论
| 维度 | 状态 | 说明 |
|------|------|------|
| 编辑器可用性 | ✅ | 节点图渲染、选中、添加、编辑、保存均正常 |
| 功能完整性 | ⚠️ | 存在 5 个 P1 功能缺陷 + 2 个 P2 规范问题 |
| 代码质量 | ⚠️ | 存在 6 个 P2 代码规范违规 |
| 用户体验 | ⚠️ | 存在 4 个 P3 改进项 |
| 架构合规 | ✅ | 三层架构正确,权限校验完整 |
| 运行时稳定性 | ✅ | Playwright 测试无控制台错误 |
---
## 二、运行时测试结果Playwright
| 测试项 | 结果 | 说明 |
|--------|------|------|
| 登录 | ✅ | 正常跳转 dashboard |
| 新建课案 | ✅ | 模板选择 → 创建 → 跳转编辑页 |
| 节点渲染 | ✅ | 8 节点 + 7 边正确渲染 |
| 节点选中 | ✅ | 点击节点 → 侧边面板显示 |
| 标题编辑 | ✅ | 侧边面板输入框可编辑 |
| 添加节点 | ✅ | 8 → 9 节点 |
| 连线 Handle | ✅ | 18 个 handle9 节点 × 2 |
| 版本抽屉 | ✅ | 打开/关闭正常,显示"暂无版本" |
| 保存版本 | ✅ | 点击后无错误 |
| 控制台错误 | ✅ | 无 error/warning |
---
## 三、P1 功能缺陷
### [P1-1] 节点拖拽位置不持久化position 变化未触发自动保存)
**文件**[node-editor.tsx:59-74](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/node-editor.tsx#L59-L74)
**现象**拖拽节点改变位置后3 秒自动保存未触发,刷新页面位置丢失。
**原因**`onNodesChange``updateNodePosition` 调用了 `set({ isDirty: true })`,但 `lesson-plan-editor.tsx:71` 的自动保存 effect 依赖 `[editor.isDirty, editor.doc, planId]``editor.doc` 是 zustand 的订阅值,但 `updateNodePosition` 每次都创建新的 doc 对象,导致 effect 频繁触发。然而拖拽过程中会触发多次 position 变化debounce 3s 应该能生效。
**实际根因**React Flow 拖拽时 `change.position` 可能是中间状态dragging: true最终位置在 dragging: false 时才确定。当前代码未区分 dragging 状态,每次都写入 store但最终位置是正确的。问题在于 `editor.doc` 引用变化太快debounce timer 不断重置,如果用户持续拖拽超过 3s 仍未保存。
**修复建议**:在 `onNodesChange` 中检查 `change.dragging === false` 才写入最终位置,避免中间状态污染。
---
### [P1-2] 侧边面板关闭后无法重新打开
**文件**[lesson-plan-editor.tsx:65-68](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/lesson-plan-editor.tsx#L65-L68)
**现象**:用户点击节点选中 → 侧边面板打开 → 点击面板关闭按钮 → 再次点击同一节点,面板不会重新打开。
**原因**
```tsx
useEffect(() => {
if (editor.selectedNodeId) setPanelOpen(true);
}, [editor.selectedNodeId]);
```
点击关闭按钮调用 `selectNode(null)``selectedNodeId` 变为 null`panelOpen` 仍为 true。再次点击同一节点时`selectedNodeId` 从 null 变为该节点 ideffect 触发 `setPanelOpen(true)`,但 `panelOpen` 已经是 trueReact 不会重新渲染。
实际问题是:关闭按钮只调用 `selectNode(null)` 但没有 `setPanelOpen(false)`,导致面板在 `selectedNodeId` 为 null 时仍然显示(因为 `panelOpen && selectedNodeId` 条件中 panelOpen 为 true 但 selectedNodeId 为 null条件为 false面板隐藏。再次点击节点时 selectedNodeId 变化effect 触发 setPanelOpen(true),但已经是 true。
**实际根因**:关闭面板后 `panelOpen` 仍为 true`selectedNodeId` 为 null条件 `panelOpen && selectedNodeId` 为 false。再次点击节点时 `selectedNodeId` 变化effect 触发 `setPanelOpen(true)`(已是 true面板应该显示。但 `NodeEditPanel` 内部 `node` 查找依赖 `selectedNodeId`,如果找到了节点应该显示。
**验证**:需要实际测试确认。如果确实无法重新打开,可能是 `panelOpen` 状态管理问题。
**修复建议**:移除 `panelOpen` 状态,直接用 `selectedNodeId !== null` 控制面板显示。
---
### [P1-3] inline-question-editor 知识点标注缺失v2 遗留)
**文件**[inline-question-editor.tsx:22](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/inline-question-editor.tsx#L22)
**现象**:课案内新建题目无法关联知识点。
**原因**`kpIds` 被硬编码为常量空数组:
```tsx
const kpIds: string[] = [];
```
**修复建议**:添加知识点选择器 UI或复用 `KnowledgePointPicker`
---
### [P1-4] exercise-block 用 index 作为 keyv2 遗留)
**文件**[exercise-block.tsx:67](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/blocks/exercise-block.tsx#L67)
```tsx
{data.items.map((item, idx) => (
<div key={idx} ...>
```
**问题**:删除/排序时可能导致 React 状态错乱。
**修复建议**:用 `item.questionId` 作为 key。
---
### [P1-5] lesson-plan-card 用 window.location.reload()v2 遗留)
**文件**[lesson-plan-card.tsx:39,50](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/lesson-plan-card.tsx#L39)
**问题**:不符合 SPA 模式,导致整个页面重新加载。
**修复建议**:用 `useRouter().refresh()`
---
## 四、P2 代码规范问题
### [P2-1] node-editor 用 `as unknown as Record<string, unknown>` 类型断言
**文件**[node-editor.tsx:42](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/node-editor.tsx#L42)
```tsx
data: n as unknown as Record<string, unknown>,
```
**问题**:双重断言绕过类型检查,违反"禁止 as 断言"规范。
**建议**React Flow 的 `Node` 类型要求 `data``Record<string, unknown>`,可以构造一个符合类型的对象。
---
### [P2-2] node-editor 隐藏 span 传递 propshack
**文件**[node-editor.tsx:152](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/node-editor.tsx#L152)
```tsx
<span className="hidden" data-textbook={textbookId} data-chapter={chapterId} data-classes={classes?.length} />
```
**问题**:用隐藏 DOM 元素避免 unused 警告,是 hack 做法。
**建议**`textbookId`/`chapterId`/`classes` 是 NodeEditor 的 props 但未使用(实际由 NodeEditPanel 使用)。应移除这些 props或让 NodeEditor 不接收它们。
---
### [P2-3] publish-service 用 JSON.parse(JSON.stringify()) 深拷贝v2 遗留)
**文件**[publish-service.ts:83-85](file:///e:/Desktop/CICD/src/modules/lesson-preparation/publish-service.ts#L83-L85)
**问题**:性能差,且不支持 Date 等特殊类型。
**建议**:用 `structuredClone()`
---
### [P2-4] exercise-block 用 `as never` 类型断言v2 遗留)
**文件**[exercise-block.tsx:52](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/blocks/exercise-block.tsx#L52)
```tsx
update({ purpose: e.target.value as never })
```
**建议**:用 `as ExercisePurpose` 并添加类型守卫。
---
### [P2-5] 多个组件用 alert()/confirm()v2 遗留)
**文件**
- [version-history-drawer.tsx:46](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/version-history-drawer.tsx#L46)
- [lesson-plan-card.tsx:48](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/lesson-plan-card.tsx#L48)
- [inline-question-editor.tsx:26](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/inline-question-editor.tsx#L26)
- [text-study-block.tsx:42](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/blocks/text-study-block.tsx#L42)
**问题**:阻塞主线程,不符合现代 Web UI 规范。
**建议**:使用 `AlertDialog` 组件或 `sonner` toast。
---
### [P2-6] text-study-block 选区计算错误
**文件**[text-study-block.tsx:29-37](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/blocks/text-study-block.tsx#L29-L37)
**问题**`range.startOffset`/`range.endOffset` 是相对于当前 DOM 节点的偏移,不是相对于 `sourceText` 的字符偏移。如果 textarea 内有换行或子节点,偏移会不正确。
**建议**:用 `textarea.selectionStart`/`textarea.selectionEnd` 获取相对于文本的偏移。
---
## 五、P3 用户体验改进
### [P3-1] 节点画布无空状态提示
**问题**:空白课案(无节点)时画布只显示网格,无引导提示。
**建议**:当 `doc.nodes.length === 0` 时显示"点击左下角添加节点开始"提示。
---
### [P3-2] 版本抽屉无预览功能v2 遗留)
**问题**:版本列表只显示版本号和标签,无法预览版本内容差异。
**建议**:点击版本时展开内容预览。
---
### [P3-3] 编辑器无 loading 骨架屏v2 遗留)
**问题**:编辑器初始化时无加载状态。
**建议**:添加 Suspense fallback。
---
### [P3-4] 列表页英文标题与中文 UI 不一致
**文件**[page.tsx:24-25](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/lesson-plans/page.tsx#L24-L25)
```tsx
<h1>My Lesson Plans</h1>
<p>Manage your lesson preparation and teaching plans.</p>
```
**问题**:项目其他页面用中文,此处用英文。
**建议**:改为"我的备课"和"管理备课和教学计划"。
---
## 六、架构合规性检查
| 检查项 | 状态 | 说明 |
|--------|------|------|
| 三层架构app→modules→shared | ✅ | 路由层只调用 actions 和 data-access |
| 模块间通过 data-access 通信 | ✅ | publish-service 通过 questions/exams/homework 的 data-access |
| Server Action 权限校验 | ✅ | 所有 action 调用 requirePermission |
| Zod 校验 | ✅ | actions 使用 schema 校验输入 |
| ActionState 返回类型 | ✅ | 统一使用 ActionState<T> |
| "server-only" 标注 | ✅ | 所有 data-access 文件有 "server-only" |
| "use client" 标注 | ✅ | 所有客户端组件有 "use client" |
| revalidatePath 精确刷新 | ✅ | 创建/删除/回退后调用 revalidatePath |
| 架构图同步 | ✅ | 004/005 已同步 v2 节点图结构 |
| 数据结构向后兼容 | ✅ | normalizeDocument 自动迁移 v1→v2 |
---
## 七、修复优先级
| 优先级 | 问题编号 | 描述 | 影响 |
|--------|----------|------|------|
| **P1** | P1-2 | 侧边面板关闭后无法重新打开 | UX 阻塞 |
| **P1** | P1-1 | 节点拖拽位置可能不持久化 | 数据丢失风险 |
| **P1** | P1-4 | exercise-block 用 index 作为 key | 列表状态错乱 |
| **P1** | P1-5 | lesson-plan-card 用 window.location.reload | SPA 体验差 |
| **P1** | P1-3 | inline 题目无知识点标注 | 功能缺失 |
| **P2** | P2-2 | node-editor 隐藏 span hack | 代码质量 |
| **P2** | P2-1 | node-editor 类型断言 | 代码规范 |
| **P2** | P2-6 | text-study-block 选区计算错误 | 功能错误 |
| **P2** | P2-3 | publish-service 深拷贝方式 | 性能 |
| **P2** | P2-4 | exercise-block as never 断言 | 代码规范 |
| **P2** | P2-5 | alert/confirm 使用 | UX 规范 |
| **P3** | P3-4 | 列表页英文标题 | i18n 一致性 |
| **P3** | P3-1 | 画布空状态提示 | UX 引导 |
| **P3** | P3-2 | 版本预览 | UX 增强 |
| **P3** | P3-3 | 编辑器骨架屏 | UX 优化 |
---
## 八、验证记录
| 验证项 | 命令 | 结果 |
|--------|------|------|
| TypeScript | `npx tsc --noEmit` | ✅ exit 0 |
| ESLint | `npm run lint` | ✅ 备课模块零错误 |
| Playwright 节点渲染 | 8 节点 + 7 边 | ✅ |
| Playwright 节点选中 | 侧边面板显示 | ✅ |
| Playwright 添加节点 | 8 → 9 节点 | ✅ |
| Playwright 版本抽屉 | 打开/关闭 | ✅ |
| Playwright 保存版本 | 无错误 | ✅ |
| 控制台错误 | 无 error/warning | ✅ |
---
## 九、附录:测试截图
- `bugs/v3_01_initial.png` - 初始编辑页
- `bugs/v3_02_selected.png` - 节点选中状态
- `bugs/v3_03_versions.png` - 版本抽屉
- `bugs/v3_04_final.png` - 最终状态