docs: update architecture map and add lesson-preparation usage fixes design

- Update architecture impact map (004) and data (005) with new modules

- Add lesson-preparation usage fixes design spec

- Add teacher web test post-audit report
This commit is contained in:
SpecialX
2026-06-24 12:01:35 +08:00
parent 9d87388524
commit e4254f0f8e
5 changed files with 3139 additions and 107 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -113,7 +113,7 @@
│ │ data-access │
┌─┴────────┐ │ │
│ grades │◀────┘ 仅外键引用(合理)
│ (成绩) │
│ (成绩) │◀──── exams data-access(✅ 2026-06-24 新增:getExamsForGradeEntry/getExamForGradeEntry 按试卷录入成绩)
└────┬─────┘
│ ✅ P1-1 已修复
│ 通过 classes/school/users data-access
@@ -499,7 +499,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `lib/ai.ts` | 9 | 向后兼容重导出(P2-2 已拆分到 `ai/` 目录) |
| `lib/ai/payload-parser.ts` | 78 | 请求负载解析 |
| `lib/ai/api-key-crypto.ts` | 28 | API Key 加密/解密 |
| `lib/ai/provider-config.ts` | 61 | Provider 配置查询 |
| `lib/ai/provider-config.ts` | 132 | Provider 配置查询(V3.1 增强:基于 session 用户的可见性/所有权校验,public + own private 过滤) |
| `lib/ai/client.ts` | 58 | AI 客户端创建与调用 |
| `lib/ai/errors.ts` | 8 | 错误格式化 |
| `lib/ai/index.ts` | 5 | 聚合导出 |
@@ -541,15 +541,16 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**导出函数**:
- Actions:`createExamAction` / `createAiExamAction` / `previewAiExamAction` / `regenerateAiQuestionAction` / `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` / `getSubjectsAction` / `getGradesAction` / `getExamsByGradeIdAction`(✅ v4-P2-7 新增:年级仪表盘维度3,按 gradeId 查询年级下所有考试 + 提交统计,EXAM_READ 权限)(✅ P1-2 已修复:actions 层不再直接访问 DB,全部下沉到 data-access)
- Data-access:`getExams` / `getExamById` / `persistExamDraft` / `persistAiGeneratedExamDraft` / `buildExamDescription` / `resolveSubjectGradeNames` / `getExamCreatorId` / `updateExamWithQuestions` / `deleteExamById` / `duplicateExam` / `getExamPreview` / `getExamSubjects` / `getExamGrades` / `getExamsByGradeId`(✅ v4-P2-7 新增:年级仪表盘维度3,exams 表有直接 gradeId 字段,配合 examSubmissions 聚合提交数/已评分数/平均分,支持 scope 行级过滤)(后 8 个为 P1-2 新增)
- Data-access:`getExams` / `getExamById` / `persistExamDraft` / `persistAiGeneratedExamDraft` / `buildExamDescription` / `resolveSubjectGradeNames` / `getExamCreatorId` / `updateExamWithQuestions` / `deleteExamById` / `duplicateExam` / `getExamPreview` / `getExamSubjects` / `getExamGrades` / `getExamsByGradeId`(✅ v4-P2-7 新增:年级仪表盘维度3,exams 表有直接 gradeId 字段,配合 examSubmissions 聚合提交数/已评分数/平均分,支持 scope 行级过滤)(后 8 个为 P1-2 新增)/ `getExamsForGradeEntry`(✅ 2026-06-24 新增:按 scope 过滤试卷列表,只返回有题目的试卷,供成绩录入页试卷选择器使用,返回 id/title/subjectName/gradeName/questionCount/totalScore)/ `getExamForGradeEntry`(✅ 2026-06-24 新增:获取单个试卷详情含题目列表,innerJoin questions 获取 type,含 scope 校验,返回 id/title/subjectId/gradeId/totalScore/questions[{id,order,score,type}],供 grades 模块按试卷录入成绩使用)
- AI Pipeline:`generateAiCreateDraftFromSource` / `generateAiPreviewData` / `regenerateAiQuestionByInstruction`
- Utils:`normalizeStructure`(v3 新增:将持久化的 `exam.structure` unknown JSON 运行时校验并归一化为类型安全的 `ExamNode[]`,类型守卫模式无 `as` 断言,从 `teacher/exams/[id]/build/page.tsx` 提取)
- Stats-service(V3-8 新增):`getExamAnalytics`(cache 包装,聚合考试所有作业的已批改提交,计算平均分/及格率/分数段分布/逐题错误率与难度等级,对标智学网考试分析)+ `ExamAnalyticsSummary` 类型
- Types(✅ 2026-06-24 新增成绩录入相关类型):`ExamQuestionItem`(试卷中单个题目的精简结构 { id, order, score, type })/ `ExamForGradeEntry`(成绩录入用的试卷详情,含题目列表)/ `ExamOptionForEntry`(成绩录入页试卷选择器选项 { id, title, subjectName, gradeName, questionCount, totalScore })
- Components(V3-8 新增):`ExamAnalyticsDashboard`(考试分析仪表盘:汇总卡片+分数段分布+逐题分析表)
**依赖关系**:
- 依赖:`shared/*`、`@/auth`、`questions`(✅ P0-1 已修复:通过 data-access.createQuestionWithRelations)、`classes`(✅ P0-2 已修复:通过 data-access.getClassGradeIdsByClassIds)、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getGradeOptions)、`homework`(V3-8 新增:stats-service 通过 `homework/data-access.getHomeworkAssignmentsByExamId` / `getGradedSubmissionsByExamId` 获取作业与提交数据,合理跨模块调用)
- 被依赖:`homework`(通过 sourceExamId 外键,合理)、`dashboard`(通过 data-access,P0-4 已修复)、`proctoring`(✅ P1-1 已修复:通过 exams data-access)、`diagnostic`(✅ P1-1 已修复:通过 exams data-access)
- 被依赖:`homework`(通过 sourceExamId 外键,合理)、`dashboard`(通过 data-access,P0-4 已修复)、`proctoring`(✅ P1-1 已修复:通过 exams data-access)、`diagnostic`(✅ P1-1 已修复:通过 exams data-access)、`grades`(✅ 2026-06-24 新增:通过 data-access.getExamsForGradeEntry/getExamForGradeEntry 获取试卷列表和详情供按试卷录入成绩使用)
**已知问题**:
- ✅ P0-1 已修复:~~`persistAiGeneratedExamDraft` 直接 insert 到 `questions` 表~~ 改为调用 `questions/data-access.createQuestionWithRelations`,通过 ID 映射保持 structure 引用一致
@@ -565,9 +566,9 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|------|------|------|
| `actions.ts` | 691 | 10 个 Server Action(P1-2 已修复,无直接 DB 操作) |
| `ai-pipeline.ts` | 857 | AI 出题管线(超限) |
| `data-access.ts` | 473 | 考试 CRUD(含 P1-2 新增 7 个写/查询函数,P0-1/P0-2 已修复:通过 questions/classes data-access 跨模块通信) |
| `data-access.ts` | 560+ | 考试 CRUD(含 P1-2 新增 7 个写/查询函数,P0-1/P0-2 已修复:通过 questions/classes data-access 跨模块通信;v4-P2-7 新增 getExamsByGradeId;2026-06-24 新增 getExamsForGradeEntry/getExamForGradeEntry 供 grades 模块按试卷录入成绩) |
| `stats-service.ts` | - | V3-8 新增:考试分析数据聚合(`getExamAnalytics` + `ExamAnalyticsSummary` 类型) |
| `types.ts` | 31 | 类型定义 |
| `types.ts` | 50+ | 类型定义(2026-06-24 新增:ExamQuestionItem/ExamForGradeEntry/ExamOptionForEntry 供成绩录入使用) |
| `hooks/use-exam-preview.ts` | 295 | 预览 Hook |
| `utils/normalize-structure.ts` | 57 | v3 新增:exam.structure 运行时校验与归一化(从 build/page.tsx 提取) |
| `components/exam-analytics-dashboard.tsx` | - | V3-8 新增:考试分析仪表盘组件 |
@@ -725,16 +726,16 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
**职责**:成绩分析(录入/查询/统计/导出/趋势对比分析)。
**导出函数**:
- Actions:`getGradeRecordsAction` / `createGradeRecordAction`(v4-P1-6 增强:成绩录入后通知学生和家长,调用 `notifyGradeEntered`)/ `updateGradeRecordAction` / `deleteGradeRecordAction` / `exportGradesAction`(v4-P1-12 增强:新增可选 `studentId` 参数,支持按学生导出,家长视角调用 `exportStudentGradeRecordsToExcel`,校验 studentId 属于家长子女)/ `getGradeTrendAction` / `getClassComparisonAction` / `getSubjectComparisonAction` / `getGradeDistributionAction` / `getGradeDistributionByGradeIdAction`(✅ v4-P2-7 新增:年级仪表盘维度1,按 gradeId 查询年级整体 + 按班级拆分的成绩分布,GRADE_RECORD_READ 权限)/ `getClassRankingAction` / `getRankingTrendAction` / `getGradeRecordByIdAction` / `getClassGradeStatsAction` / `getStudentGradeSummaryAction` / `batchCreateGradeRecordsAction`(v4-P1-6 增强:批量成绩录入后通知学生和家长)/ `saveGradeDraftAction` / `getGradeDraftAction` / `deleteGradeDraftAction`(✅ v3-P2 新增:成绩录入草稿 Server Actions,分别使用 GRADE_RECORD_MANAGE/GRADE_RECORD_READ/GRADE_RECORD_MANAGE 权限)。注:`assertClassInScope` 原位于 actions.ts(✅ P3 新增导出:班级 scope 校验工具,供 actions-analytics 复用),✅ v4-P2-6 修复:因 "use server" 文件要求所有 export 为 async,而 `assertClassInScope` 是同步函数,已迁移至独立文件 `lib/scope-check.ts`,actions.ts 与 actions-analytics.ts 均从 `./lib/scope-check` 导入
- Data-access:`getGradeRecords` / `getStudentGradeSummary` / `getClassRanking` / `getClassStudentsForEntry` / `getClassGradeStats` / `getClassGradeStatsWithMeta` / `getGradeTrend` / `getClassComparison` / `getSubjectComparison` / `getGradeDistribution` / `getGradeDistributionByGradeId`(✅ v4-P2-7 新增:年级仪表盘维度1,通过 getClassesByGradeId 获取年级下所有班级,inArray 查询成绩记录,复用 computeGradeDistribution/computeGradeStats 纯函数,返回整体分布 + 按班级拆分)/ `getRankingTrend` / `PaginatedGradeRecords`(✅ P3 新增:分页结果接口 `{ records, total }`)/ `saveGradeDraft` / `getGradeDraft` / `deleteGradeDraft`(✅ v3-P2 新增:成绩录入草稿 CRUD,upsert + 24 小时过期)/ `getExamOptionsForGrades` / `getSchoolWideGradeSummary`(✅ v3-P2 新增:考试选项查询 + 全校各年级成绩汇总,管理员视图按年级聚合平均分/及格率/优秀率/学生数/班级数,加权平均计算全校汇总)
- Types(✅ v3-P2 新增,✅ v4-P2-7 新增年级分布类型):`SchoolWideGradeSummaryItem` / `SchoolWideGradeSummary` / `GradeDraftData`(草稿数据接口)/ `GradeDistributionByGradeResult`(✅ v4-P2-7 新增:年级维度成绩分布结果,含 overall 整体分布 + stats 统计 + byClass 按班级拆分数组)/ `GradeDistributionByGradeClassItem`(✅ v4-P2-7 新增:按班级拆分的分布项:classId/className/distribution/stats)
- Actions:`getGradeRecordsAction` / `createGradeRecordAction`(v4-P1-6 增强:成绩录入后通知学生和家长,调用 `notifyGradeEntered`)/ `updateGradeRecordAction` / `deleteGradeRecordAction` / `exportGradesAction`(v4-P1-12 增强:新增可选 `studentId` 参数,支持按学生导出,家长视角调用 `exportStudentGradeRecordsToExcel`,校验 studentId 属于家长子女)/ `getGradeTrendAction` / `getClassComparisonAction` / `getSubjectComparisonAction` / `getGradeDistributionAction` / `getGradeDistributionByGradeIdAction`(✅ v4-P2-7 新增:年级仪表盘维度1,按 gradeId 查询年级整体 + 按班级拆分的成绩分布,GRADE_RECORD_READ 权限)/ `getClassRankingAction` / `getRankingTrendAction` / `getGradeRecordByIdAction` / `getClassGradeStatsAction` / `getStudentGradeSummaryAction` / `batchCreateGradeRecordsAction`(v4-P1-6 增强:批量成绩录入后通知学生和家长)/ `batchCreateGradeRecordsByExamAction`(✅ 2026-06-24 新增:按试卷批量录入每题得分,流程 requirePermission → getExamForGradeEntry scope 校验 → assertClassInScope → safeJsonParse → BatchGradeEntryByExamSchema 校验 → batchCreateGradeRecordsByExam 单事务写入 → updateMasteryFromExamScore → notifyGradeEntered → revalidatePath,返回 gradeRecordId 列表供撤销)/ `saveGradeDraftAction` / `getGradeDraftAction` / `deleteGradeDraftAction`(✅ v3-P2 新增:成绩录入草稿 Server Actions,分别使用 GRADE_RECORD_MANAGE/GRADE_RECORD_READ/GRADE_RECORD_MANAGE 权限)。注:`assertClassInScope` 原位于 actions.ts(✅ P3 新增导出:班级 scope 校验工具,供 actions-analytics 复用),✅ v4-P2-6 修复:因 "use server" 文件要求所有 export 为 async,而 `assertClassInScope` 是同步函数,已迁移至独立文件 `lib/scope-check.ts`,actions.ts 与 actions-analytics.ts 均从 `./lib/scope-check` 导入
- Data-access:`getGradeRecords` / `getStudentGradeSummary` / `getClassRanking` / `getClassStudentsForEntry` / `getClassGradeStats` / `getClassGradeStatsWithMeta` / `getGradeTrend` / `getClassComparison` / `getSubjectComparison` / `getGradeDistribution` / `getGradeDistributionByGradeId`(✅ v4-P2-7 新增:年级仪表盘维度1,通过 getClassesByGradeId 获取年级下所有班级,inArray 查询成绩记录,复用 computeGradeDistribution/computeGradeStats 纯函数,返回整体分布 + 按班级拆分)/ `getRankingTrend` / `PaginatedGradeRecords`(✅ P3 新增:分页结果接口 `{ records, total }`)/ `saveGradeDraft` / `getGradeDraft` / `deleteGradeDraft`(✅ v3-P2 新增:成绩录入草稿 CRUD,upsert + 24 小时过期)/ `getExamOptionsForGrades` / `getSchoolWideGradeSummary`(✅ v3-P2 新增:考试选项查询 + 全校各年级成绩汇总,管理员视图按年级聚合平均分/及格率/优秀率/学生数/班级数,加权平均计算全校汇总)/ `batchCreateGradeRecordsByExam`(✅ 2026-06-24 新增:按试卷批量录入成绩,单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions(status=graded) + submission_answers(answerContent=null),使错题集/成绩分析等下游模块无需改造即可读取教师录入的成绩,返回 gradeRecordId 列表供撤销)
- Types(✅ v3-P2 新增,✅ v4-P2-7 新增年级分布类型,✅ 2026-06-24 新增按试卷录入类型):`SchoolWideGradeSummaryItem` / `SchoolWideGradeSummary` / `GradeDraftData`(草稿数据接口)/ `GradeDistributionByGradeResult`(✅ v4-P2-7 新增:年级维度成绩分布结果,含 overall 整体分布 + stats 统计 + byClass 按班级拆分数组)/ `GradeDistributionByGradeClassItem`(✅ v4-P2-7 新增:按班级拆分的分布项:classId/className/distribution/stats)/ `GradeRecordAnswer`(✅ 2026-06-24 新增:成绩记录-题目得分明细,对应 grade_record_answers 表)/ `BatchGradeEntryByExamQuestion`(✅ 2026-06-24 新增:按试卷录入时单个题目得分 { questionId, score })/ `BatchGradeEntryByExamItem`(✅ 2026-06-24 新增:按试卷录入时单个学生所有题目得分 { studentId, answers })
- Lib(✅ P1-2 新增,✅ P3 更新签名,✅ P3-26 拆分,✅ v4-P2-6 新增 scope-check):`toNumber` / `normalize`(位于 `lib/grade-utils.ts`);`buildScopeClassFilter(scope, currentUserId?)`(P3-26 从 grade-utils.ts 迁移至 `lib/scope-filter.ts`,P3 修复:`class_members` scope 内置 studentId 过滤,需传入 currentUserId 参数);`assertClassInScope(scope: DataScope, classId: string): string | null`(✅ v4-P2-6 从 actions.ts 迁移至 `lib/scope-check.ts`:校验 classId 是否在 scope 允许范围内,供 actions.ts 与 actions-analytics.ts 复用。迁移原因:actions.ts 是 "use server" 文件要求所有 export 为 async,而 assertClassInScope 是同步函数)
- Stats-service(✅ P1-1 新增):`computeGradeStats` / `computeAverageScore` / `buildGradeTrendPoints` / `computeTrendAverage` / `computeClassComparisonStats` / `computeSubjectComparisonStats` / `computeGradeDistribution` / `buildRankingTrendPoints`(从 3 个 data-access 文件抽取的纯函数,使数据层专注 DB I/O,统计逻辑可独立测试)
- Export(✅ v4-P1-12 新增):`exportGradeRecordsToExcel` / `exportClassGradeReportToExcel` / `exportStudentGradeRecordsToExcel`(v4-P1-12 新增:导出单个学生成绩单家长视角,仅含成绩明细 + 个人统计,不含班级数据,scope 为 children 自动按 studentId 过滤)/ `formatDateForFile`(已迁移至 shared/lib/utils)
- Components(✅ P1-5 新增):`WidgetBoundary`(Error Boundary + Suspense + Skeleton 组合,含 a11y 属性)/ `SchoolWideSummaryCard`(✅ v3-P2 新增:管理员全校成绩汇总卡片,4 个统计卡片 + 各年级对比表格)/ `ScoreCell`(✅ v4-P1-7 新增:成绩单元格组件,根据得分率着色——红<60%/黄60-84%/绿≥85%,使用语义化 Tailwind 类名避免动态拼接,fullScore<=0 时不着色)
**依赖关系**:
- 依赖:`shared/*`、`@/auth`、`classes`(✅ P1-1 已修复:通过 classes data-access.getClassExists/getClassNameById/getClassNamesByIds/getActiveStudentIdsByClassId/getStudentActiveClassId/getClassesByGradeId)、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getGradeOptions)、`users`(✅ P1-1 已修复:通过 users data-access.getUserNamesByIds)
- 依赖:`shared/*`、`@/auth`、`classes`(✅ P1-1 已修复:通过 classes data-access.getClassExists/getClassNameById/getClassNamesByIds/getActiveStudentIdsByClassId/getStudentActiveClassId/getClassesByGradeId/getClassGradeIdsByClassIds)、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getGradeOptions)、`users`(✅ P1-1 已修复:通过 users data-access.getUserNamesByIds)、`exams`(✅ 2026-06-24 新增:通过 exams data-access.getExamsForGradeEntry/getExamForGradeEntry 获取试卷列表和详情供按试卷录入成绩使用)
- 被依赖:`parent`(通过 data-access,合理)、`dashboard`
**已知问题**:
@@ -784,22 +785,23 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- ✅ v3-P3-2 改进(2026-06-23):`grade-record-list.tsx` 新增多选复选框(全选/单选)+ 批量删除工具栏 + 批量删除确认对话框;新增 `bulkDeleteGradeRecords` data-access 函数(使用 inArray 一次性删除避免 N+1)+ `bulkDeleteGradeRecordsAction` Server Action(限制单次最多 500 条)
- ✅ v4-P3-2 改进(2026-06-23):`batch-grade-entry.tsx` 顶部新增可折叠新手引导提示框(4 步使用说明),使用 localStorage 记住用户关闭状态避免重复显示
- ✅ v4-P2-6 修复(2026-06-23):~~`assertClassInScope` 是同步函数但位于 "use server" 文件 actions.ts 中~~ Next.js 要求 "use server" 文件中所有 export 必须为 async,同步 export 会导致构建错误。修复:将 `assertClassInScope` 迁移至独立文件 `lib/scope-check.ts`(含 `import "server-only"`),actions.ts 与 actions-analytics.ts 均从 `./lib/scope-check` 导入
- ✅ 2026-06-24 重新设计:批量录入从"只录总分"改为"按试卷录入每题得分"。新增 `grade_record_answers` 表存储每题得分明细(迁移 0010_grade_record_answers.sql)。`batchCreateGradeRecordsByExam` data-access 单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions(status=graded) + submission_answers(answerContent=null),使错题集/成绩分析等下游模块无需改造即可读取教师录入的成绩(学生只看到对错,不知道学生答案)。`BatchGradeEntryByExam` 组件完全重写为 Excel 式表格(行=学生,列=题目,末列=总分自动计算),支持多行多列粘贴、Enter 跳下一行、Tab 跳下一格、分数校验、撤销机制(sessionStorage 5 分钟有效)。新增对 `exams` 模块的依赖(getExamsForGradeEntry/getExamForGradeEntry)。i18n 新增 batchByExam 章节(zh-CN/en ~35 键)
**文件清单**:
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | 670+ | 19 个 Server Action(含 Zod 校验,含 v2-P1-5 安全修复:assertClassInScope + 行级 scope 校验;P3 修复:handleActionError + safeJsonParse + scope 传递 + DB 层分页;v3-P2 新增:saveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction;v4-P1-6:createGradeRecordAction/batchCreateGradeRecordsAction 新增通知;v4-P1-12:exportGradesAction 新增 studentId 参数;v3-P3-2 新增:bulkDeleteGradeRecordsAction 批量删除;v4-P2-6:assertClassInScope 迁移至 lib/scope-check.ts) |
| `actions.ts` | 770+ | 20 个 Server Action(含 Zod 校验,含 v2-P1-5 安全修复:assertClassInScope + 行级 scope 校验;P3 修复:handleActionError + safeJsonParse + scope 传递 + DB 层分页;v3-P2 新增:saveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction;v4-P1-6:createGradeRecordAction/batchCreateGradeRecordsAction 新增通知;v4-P1-12:exportGradesAction 新增 studentId 参数;v3-P3-2 新增:bulkDeleteGradeRecordsAction 批量删除;v4-P2-6:assertClassInScope 迁移至 lib/scope-check.ts;2026-06-24 新增:batchCreateGradeRecordsByExamAction 按试卷录入每题得分) |
| `actions-analytics.ts` | 170 | 5 个分析 Action(含 Zod 校验,P3 修复:handleActionError + assertClassInScope 校验;v4-P2-6:assertClassInScope 改从 ./lib/scope-check 导入) |
| `data-access.ts` | 450+ | 成绩 CRUD + 统计 + 草稿(含 v2-P2-9 修复:recorderName 批量查询;P3 修复:PaginatedGradeRecords 接口 + DB 层分页 + 事务 + 存在性检查 + scope 过滤 + 并列排名;v3-P2 新增:saveGradeDraft/getGradeDraft/deleteGradeDraft + GradeDraftData 接口;v3-P3-2 新增:bulkDeleteGradeRecords 使用 inArray 批量删除) |
| `data-access.ts` | 600+ | 成绩 CRUD + 统计 + 草稿 + 按试卷录入(含 v2-P2-9 修复:recorderName 批量查询;P3 修复:PaginatedGradeRecords 接口 + DB 层分页 + 事务 + 存在性检查 + scope 过滤 + 并列排名;v3-P2 新增:saveGradeDraft/getGradeDraft/deleteGradeDraft + GradeDraftData 接口;v3-P3-2 新增:bulkDeleteGradeRecords 使用 inArray 批量删除;2026-06-24 新增:batchCreateGradeRecordsByExam 单事务写入 grade_records + grade_record_answers + 投影到 exam_submissions/submission_answers) |
| `data-access-analytics.ts` | 200+ | 趋势/对比分析(P3 修复:getClassComparison 应用 buildScopeClassFilter;v3-P2 新增:getExamOptionsForGrades/getSchoolWideGradeSummary;getGradeTrend/getClassComparison/getSubjectComparison/getGradeDistribution 新增 semester/examId 可选参数) |
| `data-access-ranking.ts` | 83 | 排名查询(P3 修复:getRankingTrend 接受 scope 参数 + class_taught 校验) |
| `stats-service.ts` | 285 | 统计计算纯函数(P1-1 新增:8 个纯函数 + 2 个常量 + 2 个接口;P3-10:createDefaultBuckets 改为内部函数;P3-24:buildGradeTrendPoints 使用 isGradeTrendType 类型守卫替代 as 断言) |
| `export.ts` | 290+ | Excel 导出(v2-P1-5 修复:传递 currentUserId 到 data-access;P3 修复:适配 PaginatedGradeRecords 结构 + 传递 scope;P3-6:复用 stats-service.computeAverageScore 替代局部 avg;P3-7:硬编码中文改用 next-intl getTranslations;v4-P1-12 新增:exportStudentGradeRecordsToExcel 家长视角单学生导出) |
| `schema.ts` | 113+ | Zod 校验(含 12 个查询 schema;P3 修复:score .max(1000) + records .max(500) + 补全查询字段;v3-P2 新增:grade_drafts 表定义第 1444-1469 行) |
| `schema.ts` | 130+ | Zod 校验(含 12 个查询 schema;P3 修复:score .max(1000) + records .max(500) + 补全查询字段;v3-P2 新增:grade_drafts 表定义第 1444-1469 行;2026-06-24 新增:BatchGradeEntryByExamSchema 按试卷录入校验 + BatchGradeEntryByExamQuestionSchema/ItemSchema 子 schema,导出 BatchGradeEntryByExamInput 类型) |
| `lib/grade-utils.ts` | 20 | 公共工具函数(toNumber/normalize;P3-26:buildScopeClassFilter 迁移至 scope-filter.ts) |
| `lib/scope-filter.ts` | 56 | DB 行级权限过滤(buildScopeClassFilter;P3-26 从 grade-utils.ts 迁移;v2-P2-2 修复:改用 classes data-access 子查询;P3 修复:新增 currentUserId 参数) |
| `lib/scope-check.ts` | 34 | v4-P2-6 新增:班级 scope 校验工具(assertClassInScope 同步函数,从 actions.ts 迁移至此独立文件以避开 "use server" 文件要求 export 必须为 async 的限制;含 `import "server-only"`) |
| `types.ts` | 168+ | 类型定义(v3-P2 新增:SchoolWideGradeSummaryItem/SchoolWideGradeSummary) |
| `types.ts` | 200+ | 类型定义(v3-P2 新增:SchoolWideGradeSummaryItem/SchoolWideGradeSummary;2026-06-24 新增:GradeRecordAnswer/BatchGradeEntryByExamQuestion/BatchGradeEntryByExamItem) |
| `components/widget-boundary.tsx` | 136 | Widget 边界组件(P1-5 新增,v2-P1-1 已在 3 个页面应用) |
| `components/school-wide-summary-card.tsx` | - | v3-P2 新增:管理员全校成绩汇总卡片(4 个统计卡片 + 各年级对比表格) |
| `components/score-cell.tsx` | 41 | v4-P1-7 新增:成绩单元格组件,根据得分率着色(红<60%/黄60-84%/绿≥85%),使用语义化 Tailwind 类名 |
@@ -810,7 +812,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| `components/class-comparison-chart.tsx` | 194 | 班级对比图(v2-P1-4:i18n;v3-P3-5 新增:显著性分析区域,基于极差和样本量的经验规则判断班级间差异,含可折叠详细分析) |
| `components/grade-trend-chart.tsx` | 59 | 趋势图(v2-P1-4:i18n) |
| `components/grade-record-form.tsx` | 177 | 录入表单(v2-P2-7 修复:Label htmlFor;v2-P1-4:i18n;P3 修复:safeActionCall 包装提交) |
| `components/batch-grade-entry.tsx` | 500+ | 批量录入(v2-P2-7 修复:Label htmlFor;v2-P1-4:i18n;P3 修复:safeActionCall + localStorage 安全检查 + 区分未录入与录入 0;v3-P2 新增:接入服务端草稿 saveGradeDraftAction/getGradeDraftAction/deleteGradeDraftAction;v3-P3-1 新增:下载 CSV 录入模板按钮含学生姓名列表;v4-P3-2 新增:可折叠新手引导提示框,localStorage 记住关闭状态) |
| `components/batch-grade-entry.tsx` | 600+ | 2026-06-24 完全重写:导出名改为 BatchGradeEntryByExam,按试卷录入每题得分。Excel 式表格(行=学生,列=题目,末列=总分自动计算)。交互:试卷选择器(按 scope 过滤)→ 班级选择器(按试卷 gradeId 过滤);多行多列 Excel 粘贴(Tab 分隔);Enter 跳下一行同一列;Tab 跳下一格;实时统计(已录入/总数/均分/最高/最低);分数校验(超过题目满分标红);撤销机制(sessionStorage 5 分钟有效)。Props: exams/classes/classGradeMap/exam/students/defaultExamId?/defaultClassId?(原 v2-P2-7 Label htmlFor;v2-P1-4 i18n;P3 safeActionCall + localStorage 安全检查;v3-P2 服务端草稿;v3-P3-1 下载 CSV 模板;v4-P3-2 新手引导均已替换) |
| `components/grade-filters.tsx` | 76 | 过滤器(v2-P1-4:i18n) |
| `components/student-grade-summary.tsx` | 107 | 学生成绩摘要(v2-P1-4:i18n) |
| `components/export-button.tsx` | 79 | 导出按钮(v2-P1-4:i18n;P3 修复:safeActionCall 包装导出操作) |
@@ -1632,21 +1634,21 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
## 2.23 settings(设置模块)— V3 AI 配置统一入口
**职责**:系统设置(学校信息/安全策略/文件上传/通知配置)+ AI Provider 管理 + 密码修改 + 个人资料 + 主题偏好 + 通知偏好 + 个人信息页(学生/教师概览)。V3 将 AI Provider 配置统一到 `/admin/ai-settings` 独立页面,移除 `/settings?tab=ai` 标签页和考试页面内嵌弹窗。
**职责**:系统设置(学校信息/安全策略/文件上传/通知配置)+ AI Provider 管理 + 密码修改 + 个人资料 + 主题偏好 + 通知偏好 + 个人信息页(学生/教师概览)。V3 将 AI Provider 配置统一到 `/admin/ai-settings` 独立页面,移除 `/settings?tab=ai` 标签页和考试页面内嵌弹窗。V3.1 新增 public/private 可见性模型:管理员发布 public Provider 全员可用,普通用户可创建 private Provider 仅本人可见。
**导出函数**:
- Actions:`getAiProviderSummaries` / `upsertAiProviderAction` / `testAiProviderAction` / `deleteAiProviderAction`(V3 新增删除能力)
- Actions:`getAiProviderSummaries`(V3.1 增强:按用户身份过滤 public + own private)/ `upsertAiProviderAction`(V3.1 增强:visibility 字段,非管理员强制 private)/ `testAiProviderAction` / `deleteAiProviderAction`(V3 新增删除能力)/ `canConfigurePublicAiProvider`(V3.1 新增:检查当前用户是否拥有 AI_CONFIGURE 权限)
- Actions-password:`changePasswordAction`(✅ P1 已修复:使用 `requirePermission(USER_PROFILE_UPDATE)` + Zod 校验 + DB 操作下沉到 data-access)
- Actions-avatar:`updateUserAvatarAction` / `removeUserAvatarAction`(✅ P2-8 新增:头像上传/删除,复用 `/api/upload` 路由)
- Actions-notifications:`sendTestNotificationAction`(✅ P2-10 新增:发送测试通知,占位实现待接入真实通知服务)
- Actions-system-settings:`getAdminSystemSettingsAction` / `saveAdminSystemSettingsAction`(✅ P0-3 新增:管理员系统设置 CRUD,4 分类 Zod 校验)
- Actions-security:`getSecurityCenterAction` / `toggleTwoFactorAction`(✅ P2-9 新增:2FA 状态查询/切换 + 最近登录历史)
- Data-access:`getAiProviderSummaries` / `countDefaultAiProviders` / `getAiProviderForUpdate` / `updateAiProvider` / `createAiProvider` / `deleteAiProvider`(V3 新增:事务删除 + 自动转移默认)/ `getUserPasswordHash` / `getPasswordSecurityByUserId` / `updateUserPassword` / `upsertPasswordSecurityOnPasswordChange`(P1 新增,从 actions 下沉)
- Data-access:`getAiProviderSummaries`(管理员视图,返回全部)/ `getAiProviderSummariesForUser`(V3.1 新增:用户视图,返回 public + own private)/ `countDefaultAiProviders` / `getAiProviderForUpdate`(V3.1 增强:可选 userId 做所有权校验)/ `updateAiProvider`(V3.1 增强:visibility 字段)/ `createAiProvider`(V3.1 增强:visibility 字段)/ `deleteAiProvider`(V3 新增:事务删除 + 自动转移默认;V3.1 增强:可选 userId 做所有权校验)/ `getUserPasswordHash` / `getPasswordSecurityByUserId` / `updateUserPassword` / `upsertPasswordSecurityOnPasswordChange`(P1 新增,从 actions 下沉)
- Data-access-system-settings:`getSystemSettingsByCategory` / `getAllSystemSettings` / `getSystemSetting` / `upsertSystemSetting` / `upsertSystemSettings`(✅ P0-3 新增:system_settings 表 CRUD,键值对存储模式)
- Components:`SettingsView`(统一设置页布局,V3 移除 AI 标签页后为 4 标签页 General/Notifications/Appearance/Security;角色差异通过 `resolveRoleSettingsConfig` 配置驱动 + `generalExtra` props 注入;Tab URL 持久化;每个 TabsContent 包裹 `SettingsSectionErrorBoundary` + `Suspense` 骨架屏)、`SettingsServiceProvider` / `useSettingsService`(Context 注入 `SettingsService` 接口,解耦组件对 users/messaging actions 的直接依赖)、`SettingsSectionErrorBoundary`(分区 Error Boundary,局部失败不影响整页)、`QuickLinksCard`(快捷链接卡片,i18n 键驱动)、`ProfileStudentOverview` / `ProfileStudentOverviewSkeleton`(学生概览异步 Server Component + 骨架屏)、`ProfileTeacherOverview` / `ProfileTeacherOverviewSkeleton`(教师概览异步 Server Component + 骨架屏)、`AdminSettingsView`(✅ P0-3 已修复:从 mock 改为真实数据层,通过 Server Actions 加载/保存到 system_settings 表)、`AvatarUpload`(✅ P2-8 新增:头像上传/预览/删除客户端组件,文件验证 + i18n)、`SecurityCenterCard`(✅ P2-9 新增:2FA 开关 + 最近登录历史卡片)、`ThemePreferencesCard`(✅ P2-11 已增强:集成 `LocaleSwitcher` 语言切换)、`AiProviderSettingsCard`(V3 增强:新增删除按钮 + AlertDialog 确认,统一在 `/admin/ai-settings` 页面渲染)
- Components:`SettingsView`(统一设置页布局,V3 移除 AI 标签页后为 4 标签页 General/Notifications/Appearance/Security;角色差异通过 `resolveRoleSettingsConfig` 配置驱动 + `generalExtra` props 注入;Tab URL 持久化;每个 TabsContent 包裹 `SettingsSectionErrorBoundary` + `Suspense` 骨架屏)、`SettingsServiceProvider` / `useSettingsService`(Context 注入 `SettingsService` 接口,解耦组件对 users/messaging actions 的直接依赖)、`SettingsSectionErrorBoundary`(分区 Error Boundary,局部失败不影响整页)、`QuickLinksCard`(快捷链接卡片,i18n 键驱动)、`ProfileStudentOverview` / `ProfileStudentOverviewSkeleton`(学生概览异步 Server Component + 骨架屏)、`ProfileTeacherOverview` / `ProfileTeacherOverviewSkeleton`(教师概览异步 Server Component + 骨架屏)、`AdminSettingsView`(✅ P0-3 已修复:从 mock 改为真实数据层,通过 Server Actions 加载/保存到 system_settings 表)、`AvatarUpload`(✅ P2-8 新增:头像上传/预览/删除客户端组件,文件验证 + i18n)、`SecurityCenterCard`(✅ P2-9 新增:2FA 开关 + 最近登录历史卡片)、`ThemePreferencesCard`(✅ P2-11 已增强:集成 `LocaleSwitcher` 语言切换)、`AiProviderSettingsCard`(V3 增强:新增删除按钮 + AlertDialog 确认;V3.1 增强:visibility 选择器 + 可见性/归属 Badge,isAdmin prop 控制公开选项,currentUserId prop 标识"我的")
- Config:`ROLE_SETTINGS_CONFIG` / `resolveRoleSettingsConfig`(配置驱动角色 → 设置视图映射,新增角色只需添加条目)
- Lib:`buildStudentOverviewData` / `computeStudentStats` / `sortUpcomingAssignments` / `filterTodaySchedule` / `toWeekday`(纯数据计算函数,与 UI 分离,便于单元测试)
- Types:`AiProviderSummary` / `AiProviderName` / `AiProviderExisting` / `SettingsService` / `ProfileService` / `NotificationPreferenceService`(服务接口定义,用于依赖注入解耦)
- Types:`AiProviderSummary`(V3.1 增强:新增 visibility/createdBy 字段)/ `AiProviderName` / `AiProviderVisibility`(V3.1 新增:'public' | 'private')/ `AiProviderExisting`(V3.1 增强:新增 visibility/createdBy 字段)/ `SettingsService` / `ProfileService` / `NotificationPreferenceService`(服务接口定义,用于依赖注入解耦)
**依赖关系**:
- 依赖:`shared/*`(含 `shared/lib/bcrypt-utils`)、`@/auth`、`messaging`(页面层通过 `SettingsService` 接口注入,组件层不直接 import)、`users`(页面层通过 `SettingsService` 接口注入)、`classes` / `homework` / `dashboard`(ProfileStudentOverview 异步组件获取学生概览数据)、`notifications`(页面层获取通知偏好)
@@ -1821,7 +1823,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
- `FlowEdge`:流程连线(教学节点 → 教学节点)
**导出函数**:
- Data-access(`data-access.ts`):`getLessonPlans` / `getLessonPlanById` / `createLessonPlan` / `updateLessonPlanContent` / `softDeleteLessonPlan` / `duplicateLessonPlan` / `getTemplateById` / `buildInitialContent` / `migrateV1ToV2` / `normalizeDocument`(v3 规范化,兼容 v1/v2 旧数据)/ `buildDefaultSkeleton`(v3 默认 10 节点骨架)/ `getTextbooksForPicker` / `getChaptersForPicker` / `findChapterById` / `publishLessonPlan`(V3 新增,设置 status=published)/ `unpublishLessonPlan`(V3 新增,设置 status=draft,仅 published 课案)
- Data-access(`data-access.ts`):`getLessonPlans`(V4:查询后按 textbookId+chapterId+creatorId 聚合版本,返回代表项 + versionCount + versions 摘要数组)/ `getLessonPlanById` / `createLessonPlan` / `updateLessonPlanContent` / `softDeleteLessonPlan` / `duplicateLessonPlan` / `getTemplateById` / `buildInitialContent` / `migrateV1ToV2` / `normalizeDocument`(v3 规范化,兼容 v1/v2 旧数据)/ `buildDefaultSkeleton`(v3 默认 10 节点骨架)/ `getTextbooksForPicker` / `getChaptersForPicker` / `findChapterById` / `publishLessonPlan`(V3 新增,设置 status=published)/ `unpublishLessonPlan`(V3 新增,设置 status=draft,仅 published 课案)
- Lib(`lib/document-migration.ts`):`defaultDataForType` / `migrateV1ToV2` / `migrateV2ToV3` / `normalizeDocument` / `buildInitialContent` / `buildDefaultSkeleton` / `isTextbookContentNode` / `isAnchorEdge` / `getAnchorsForNode` / `getActiveAnchorIds` / `getAnchorEdges`
- Lib(`lib/anchor-injector.ts`):`markdownToPlainText` / `injectPlaceholders` / `parseAnchoredText` / `toCircledNumber` / `getNextPointIndex` / `relocateAnchors` / `getAnchorColor`
- Lib(`lib/node-summary.ts`):`getNodeSummary` / `getTextbookContentSummary` / `getNodeColor` / `NODE_COLORS`
@@ -1876,53 +1878,82 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
> - **V3-9 新增路由**:`/student/lesson-plans`、`/student/lesson-plans/[planId]/view`、`/parent/lesson-plans`、`/parent/lesson-plans/[planId]/view`、`/admin/lesson-plans`、`/admin/lesson-plans/[planId]/view`
> - **V3-10 导航变更**:admin 导航新增「课案管理」(/admin/lesson-plans);student 导航新增「我的课案」(/student/lesson-plans);parent 导航新增「孩子课案」(/parent/lesson-plans)
> 架构变更(2026-06-24,V3 深度审计修复):
> - **P0-1 admin 直查 DB 修复**:`admin/lesson-plans/page.tsx` 移除 `import { db }` 和 `import { lessonPlans }`,改用 data-access 新增的 `getLessonPlanStats()` 函数(返回 total/published/draft/archived 统计)
> - **P0-2 publish-service 硬编码中文 + as 断言修复**:`publish-service.ts` 移除 `as ExerciseBlockData`/`as typeof validTypes[number]` 断言,改用 `lib/type-guards.ts` 的 `isExerciseBlockData`/`isValidQuestionType` 类型守卫;移除硬编码中文标题/描述,改为接受 `homeworkTitle`/`homeworkDescription` 参数由 actions 层 i18n 翻译后传入
> - **P0-3 duplicateLessonPlan 硬编码修复**:`duplicateLessonPlan` 接受 `duplicateSuffix` 参数(默认 " - Copy"),由 actions 层传入 `t("error.duplicateSuffix")`
> - **P0-4 schema Zod 错误消息 i18n 化**:`schema.ts` 所有错误消息从硬编码中文改为 i18n 键(如 `error.titleRequired`);新增 `lib/i18n-errors.ts`(`translateFieldErrors`/`safeParseWithI18n`)在 actions 层翻译 Zod 错误
> - **P0-5 loading/error 边界补全**:为 6 个路由新增 12 个 `loading.tsx` 和 `error.tsx` 文件(teacher/lesson-plans、teacher/lesson-plans/new、teacher/lesson-plans/[planId]/edit、admin/lesson-plans、admin/lesson-plans/[planId]/view、student/lesson-plans、student/lesson-plans/[planId]/view、parent/lesson-plans、parent/lesson-plans/[planId]/view)
> - **P1-2 组件完全通过 service 调用**:`lesson-plan-card.tsx`/`lesson-plan-list.tsx`/`lesson-plan-editor.tsx` 移除所有直接 `import { xxxAction } from "../actions"`,改为通过 `useLessonPlanContextSafe()` 获取 service 调用;`LessonPlanDataService` 接口扩展 5 个方法(getLessonPlanById/updateLessonPlan/saveLessonPlanVersion/publishLessonPlan/unpublishLessonPlan),`default-data-service.ts` 实现扩展
> - **P1-3 as 断言修复**:新增 `lib/type-guards.ts` 集中类型守卫(11 种 BlockData 类型守卫 + 节点/题目类型守卫);`block-registry.tsx` 所有 `as XxxBlockData` 替换为类型守卫;`node-edit-panel.tsx` 移除冗余 `as LessonPlanNode`(TypeScript 判别联合自动收窄);`node-editor.tsx` MiniMap `nodeColor` 使用类型守卫替代 `as { node?: AnyLessonPlanNode }`;`rf-mappers.ts` 移除冗余 `as TextbookContentNode`/`as LessonPlanNode`;`use-lesson-plan-editor.ts` `updateNode` patch 类型改为 `Omit<Partial<Block>, "type">` 防止类型变更
> - **P1-4 MiniMap 颜色硬编码修复**:`lesson-plan-readonly-view.tsx` MiniMap 移除硬编码 `#455a64`/`#1976d2`,改用 `getNodeColor(nodeData.type)` 与编辑器保持一致
> - **i18n 错误码扩展**:`zh-CN/lesson-preparation.json` 和 `en/lesson-preparation.json` 新增 `publish.homeworkTitle`/`publish.homeworkDescription`/`error.invalidQuestionType`/`error.duplicateSuffix`/`error.titleRequired`/`error.titleTooLong`/`error.templateRequired`/`error.invalidDate`/`error.classRequired` 等键
> 架构变更(2026-06-24,V3 深度审计修复续):
> - **P0 硬编码中文修复(6处)**:`actions.ts` 的 `getLessonPlanByIdAction`/`revertLessonPlanVersionAction` 错误消息改为 i18n 键(`t("error.notFound")`/`t("error.versionNotFound")`);`data-access-versions.ts` 的 `revertToVersion` 接受 `revertLabel` 参数替代硬编码 `` `回退到 v${versionNo}` ``;`lesson-plan-error-boundary.tsx` 重写为包装组件模式(内部类组件接受 `errorText`/`retryText` props,外部函数组件通过 `useTranslations` 注入 i18n 文案);`lesson-plan-provider.tsx` 的 `useLessonPlanContext` 错误消息改为英文
> - **P1 组件完全通过 service 调用(5个组件迁移)**:`version-history-drawer.tsx`/`template-picker.tsx`/`knowledge-point-picker.tsx`/`publish-homework-dialog.tsx`/`question-bank-picker.tsx` 移除所有直接 `import { xxxAction } from "../actions"`,改用 `useLessonPlanContextSafe()` 获取 service 调用;`LessonPlanDataService` 接口扩展 7 个方法(createLessonPlan/getTextbooksForPicker/getChaptersForPicker/getLessonPlanTemplates/getKnowledgePointOptions/publishLessonPlanHomework/getQuestions)+ 6 个导出类型(TextbookPickerOption/ChapterPickerOption/KnowledgePointOption/PublishHomeworkInput/QuestionPickerParams/QuestionPickerItem);`default-data-service.ts` 实现扩展;新增 `providers/lesson-plan-provider-setup.tsx`(页面层 Provider 设置包装组件,自动注入默认数据服务和角色配置);3 个页面(teacher/lesson-plans/page、teacher/lesson-plans/new/page、teacher/lesson-plans/[planId]/edit/page)用 `LessonPlanProviderSetup` 包裹;`question-bank-picker.tsx` 新增 `isQuestionType` 类型守卫替代 `as QuestionType` 断言
> - **P2 文件拆分**:`textbook-content-node.tsx` 从 578 行拆分为 3 个文件——主文件(471行)+ `anchor-node-selector.tsx`(AnchorNodeSelector 组件,62行)+ `textbook-segments.tsx`(renderSegments 函数,75行)
> - **P3 as 断言修复(8处)**:`textbook-content-node.tsx` 的 `as unknown as TextbookContentNodeProps` 替换为 `isTextbookContentNodePropsData` 类型守卫;7 个 block 组件(blackboard/import/exercise/objective/key-point/homework/reflection)的 select onChange `as` 断言替换为类型守卫——`lib/type-guards.ts` 新增 7 个字段值类型守卫(isBlackboardLayout/isImportMethod/isExercisePurpose/isObjectiveDimension/isKeyPointType/isHomeworkType/isReflectionAspect)
**文件清单**:
| 文件 | 职责 |
|------|------|
| `types.ts` | 类型定义(含 v1/v2/v3 文档类型、TextbookContentNode、LessonPlanNode、NodeAnchor、AnchorEdge、FlowEdge、11 种 BlockData 接口) |
| `constants.ts` | 常量定义 |
| `schema.ts` | Zod 验证 |
| `schema.ts` | Zod 验证(V3:错误消息改为 i18n 键,如 `error.titleRequired`) |
| `lib/type-guards.ts` | **集中类型守卫(V3 新增)**:11 种 BlockData 类型守卫(isRichTextBlockData/isTextStudyBlockData/isExerciseBlockData/isObjectiveBlockData/isKeyPointBlockData/isImportBlockData/isNewTeachingBlockData/isSummaryBlockData/isHomeworkBlockData/isBlackboardBlockData/isReflectionBlockData)+ 节点类型守卫(isTextbookContentNode/isLessonPlanNode)+ 题目类型守卫(isValidQuestionType)+ 基础类型守卫(isLessonPlanStatus/isTemplateType/isTemplateScope/isBlockType)+ **Block 字段值类型守卫(V3 续新增)**:isBlackboardLayout/isImportMethod/isExercisePurpose/isObjectiveDimension/isKeyPointType/isHomeworkType/isReflectionAspect(用于 select onChange 替代 `as` 断言) |
| `lib/i18n-errors.ts` | **Zod 错误 i18n 翻译辅助(V3 新增)**:`translateFieldErrors`(将 Zod fieldErrors 中的 i18n 键翻译为实际消息)/ `safeParseWithI18n`(安全解析 Zod 结果并返回带翻译的 ActionState 错误格式) |
| `lib/document-migration.ts` | **纯函数**:v1→v2(migrateV1ToV2)/ v2→v3(migrateV2ToV3)/ 规范化(normalizeDocument,兼容 v1/v2/v3)/ 初始内容(buildInitialContent)/ 默认骨架(buildDefaultSkeleton,10 节点 + 正文节点)/ defaultDataForType / 工具函数(isTextbookContentNode/isAnchorEdge/getAnchorsForNode/getActiveAnchorIds/getAnchorEdges) |
| `lib/anchor-injector.ts` | **纯函数**:锚点注入算法(markdownToPlainText/injectPlaceholders/parseAnchoredText/toCircledNumber/getNextPointIndex/relocateAnchors/getAnchorColor) |
| `lib/node-summary.ts` | **纯函数**:getNodeSummary(支持 11 种节点类型)+ getTextbookContentSummary + NODE_COLORS + getNodeColor |
| `lib/rf-mappers.ts` | **纯函数**:toRfNodes(支持 textbook_content 节点 + 锚点回调;V3:ctx 新增 anchorableNodes/onCreateNewNode 字段)/ toRfEdges(区分 anchor/flow 边透明度;V3:锚点边颜色使用 getNodeColor(anchor.nodeId) 替代硬编码,anchorId 存入 edge.data)/ fromRfEdges(V3:从 e.data.anchorId 读取 anchorId,回退到 className 判断) |
| `config/block-registry.tsx` | **配置驱动**:BLOCK_REGISTRY 注册表 + BlockRenderer(switch 渲染 11 种定制节点 + textbook_content) |
| `providers/lesson-plan-provider.tsx` | **Provider + Context(P1-5/P1-7/P2-4/V2-6)**:LessonPlanProvider 注入数据服务/角色配置/埋点;定义 LessonPlanDataService 接口、4 个角色配置(TEACHER/ADMIN/STUDENT/PARENT)、ROLE_CONFIGS 注册表、LessonPlanTracker 接口 + noopTracker;hooks:useLessonPlanContextSafe(返回 null 不抛错)/useLessonPlanContext/useRoleConfig/useLessonPlanService/useLessonPlanTracker/useLessonPlanTrackerSafe(V2-6 新增,返回 noopTracker 不抛错) |
| `services/default-data-service.ts` | **默认数据服务实现**:createDefaultDataService() 包装 Server Actions 为 LessonPlanDataService 实现,测试可替换为 mock |
| `data-access.ts` | 课案 CRUD + 模板查询(migrateV1ToV2/normalizeDocument/buildInitialContent 从 lib/ 导入并 re-export 保持向后兼容;buildScopeCondition 按 scope 类型精确过滤 P0-3;V2-1:抛出 `LessonPlanDataError` 错误码;V2-3:mapRowToLessonPlan/mapRowToListItem/mapRowToTemplate 显式映射 + isLessonPlanStatus/isTemplateType/isTemplateScope 类型守卫;V3:新增 publishLessonPlan/unpublishLessonPlan 函数) |
| `data-access-versions.ts` | 版本管理(创建/查询/回滚/清理;V2-3:mapRowToVersion 显式映射) |
| `data-access-templates.ts` | 个人模板 CRUD(V2-3:mapRowToTemplate 显式映射 + 类型守卫) |
| `data-access-knowledge.ts` | 按知识点/题目反查课案(V2-3:显式字段映射替代 `as unknown as`) |
| `actions.ts` | 课案 CRUD/版本/模板 Server Actions(V2-1:getTranslations i18n + 错误码捕获;V2-2:createLessonPlanAction 传入 translateTitle 翻译 SYSTEM_TEMPLATES) |
| `actions-publish.ts` | 发布作业 Server Action(V2-1:getTranslations i18n + PUBLISH_ERROR_KEY_MAP 错误码映射;V3:新增 publishLessonPlanAction/unpublishLessonPlanAction,requirePermission(LESSON_PLAN_PUBLISH)) |
| `actions-ai.ts` | AI 知识点建议 Server Action(V2-1:i18n + 错误码) |
| `actions-kp.ts` | 知识点选项 Server Action(V2-1:i18n + 错误码) |
| `publish-service.ts` | 发布作业服务(编排 homework/exams/classes,通过对方 data-access 调用,无直查跨模块表;V2-1:抛出 `PublishServiceError` 错误码;V2-3:显式字段映射替代 `as unknown as`) |
| `lib/rf-mappers.ts` | **纯函数**:toRfNodes(V3:移除冗余 `as` 断言,TypeScript 判别联合自动收窄)/ toRfEdges / fromRfEdges |
| `config/block-registry.tsx` | **配置驱动**:BLOCK_REGISTRY 注册表 + BlockRenderer(V3:使用 `lib/type-guards.ts` 类型守卫替代所有 `as XxxBlockData` 断言) |
| `providers/lesson-plan-provider.tsx` | **Provider + Context**:LessonPlanProvider 注入数据服务/角色配置/埋点;定义 LessonPlanDataService 接口(V3:扩展 5 个方法 getLessonPlanById/updateLessonPlan/saveLessonPlanVersion/publishLessonPlan/unpublishLessonPlan;**V3 续扩展 7 个方法 createLessonPlan/getTextbooksForPicker/getChaptersForPicker/getLessonPlanTemplates/getKnowledgePointOptions/publishLessonPlanHomework/getQuestions + 6 个导出类型**)、4 个角色配置、ROLE_CONFIGS 注册表、LessonPlanTracker 接口 + noopTracker;hooks:useLessonPlanContextSafe/useLessonPlanContext/useRoleConfig/useLessonPlanService/useLessonPlanTracker/useLessonPlanTrackerSafe |
| `providers/lesson-plan-provider-setup.tsx` | **页面层 Provider 设置包装组件(V3 续新增)**:LessonPlanProviderSetup 自动注入默认数据服务(createDefaultDataService)和角色配置(TEACHER_ROLE_CONFIG),3 个 teacher 页面用此组件包裹 |
| `services/default-data-service.ts` | **默认数据服务实现**:createDefaultDataService() 包装 Server Actions 为 LessonPlanDataService 实现(V3:实现扩展的 5 个方法;**V3 续扩展 7 个方法 createLessonPlan/getTextbooksForPicker/getChaptersForPicker/getLessonPlanTemplates/getKnowledgePointOptions/publishLessonPlanHomework/getQuestions**),测试可替换为 mock |
| `data-access.ts` | 课案 CRUD + 模板查询(V3:新增 `getLessonPlanStats()` 统计函数 + `LessonPlanStats` 接口;`duplicateLessonPlan` 接受 `duplicateSuffix` 参数消除硬编码) |
| `data-access-versions.ts` | 版本管理(创建/查询/回滚/清理) |
| `data-access-templates.ts` | 个人模板 CRUD |
| `data-access-knowledge.ts` | 按知识点/题目反查课案 |
| `actions.ts` | 课案 CRUD/版本/模板 Server Actions(V3:所有 action 使用 `translateFieldErrors` 翻译 Zod 错误;`duplicateLessonPlanAction` 传入 i18n 翻译的副本后缀;`getLessonPlanByIdAction` 返回类型改为 `ActionState<{ plan: LessonPlan }>`) |
| `actions-publish.ts` | 发布作业 Server Action(V3:actions 层注入 i18n 翻译的作业标题/描述/日期标签;新增 `INVALID_QUESTION_TYPE` 错误码映射) |
| `actions-ai.ts` | AI 知识点建议 Server Action |
| `actions-kp.ts` | 知识点选项 Server Action |
| `publish-service.ts` | 发布作业服务(V3:移除 `as` 断言改用类型守卫;移除硬编码中文,接受 `homeworkTitle`/`homeworkDescription` 参数;新增 `INVALID_QUESTION_TYPE` 错误码) |
| `ai-suggest.ts` | AI 知识点建议服务 |
| `seed-templates.ts` | 模板种子数据 |
| `hooks/use-lesson-plan-editor.ts` | 课案编辑器 Hook(基于 zustand,支持 nodes/edges/anchors 操作:addNode/updateNode/updateNodePosition/removeNode/connect/disconnect/setEdges/selectNode + 锚点操作 addAnchor/removeAnchor/updateAnchor + 正文节点操作 updateTextbookContent/getTextbookContentNode;实时拖动) |
| `components/lesson-plan-list.tsx` | 课案列表(i18n 已接入;V3:新增 viewMode prop,支持 teacher/student/parent/admin/gradeHead 多角色视图) |
| `components/lesson-plan-card.tsx` | 课案卡片(i18n 已接入;V2-6:duplicate/archive 调用 tracker.track;V3:新增 viewMode prop(teacher/student/parent/admin/gradeHead),动态跳转链接 + 发布/撤回按钮) |
| `components/lesson-plan-filters.tsx` | 课案筛选器(i18n 已接入;V2-5:3 个表单元素 label htmlFor 关联) |
| `components/lesson-plan-editor.tsx` | 课案编辑器(编排 NodeEditor + NodeEditPanel,i18n 已接入;V2-6:handleManualSave 调用 tracker.track;V3:顶部工具栏显示教材/章节标题指示器;V3:新增 initialStatus prop + 发布/撤回按钮(AlertDialog 确认)) |
| `components/lesson-plan-readonly-view.tsx` | **只读画布组件**(V3 新增):复用 React Flow(nodesDraggable=false, nodesConnectable=false),供学生/家长/管理员/教研组长查看已发布课案 |
| `components/node-editor.tsx` | **节点图画布**(React Flow,使用 lib/rf-mappers + lib/node-summary 纯函数,i18n 已接入;V2-4:MiniMap 复用 getNodeColor;V2-5:role=application + 键盘导航配置;V3:注册 textbook_content 节点类型 + 锚点回调 + 实时拖动) |
| `components/node-edit-panel.tsx` | **侧边内容编辑面板**(配置驱动渲染 Block,通过 BlockRenderer + LessonPlanErrorBoundary 包裹,i18n 已接入;V3:处理 textbook_content 节点,教学节点类型收窄;V3:选中 textbook_content 时显示操作提示 + 锚点列表(含删除功能)) |
| `components/nodes/lesson-node.tsx` | **自定义教学节点组件**(使用 lib/node-summary 的 getNodeSummary/getNodeColor,i18n 已接入) |
| `components/nodes/textbook-content-node.tsx` | **正文节点组件**(V3 新增):ReactMarkdown 渲染正文 + 锚点注入 + 文本选择(range 锚定)+ 点击位置(point 锚定)+ 缩放控制 + 锚点浮动菜单;V3:新增 props `anchorableNodes`, `onCreateNewNode`;AnchorNodeSelector 重写为节点列表+创建新节点选项 |
| `components/lesson-plan-error-boundary.tsx` | **错误边界**:LessonPlanErrorBoundary 类组件,支持 fallback 和 onError 回调 |
| `hooks/use-lesson-plan-editor.ts` | 课案编辑器 Hook(V3:`updateNode` patch 类型改为 `Omit<Partial<Block>, "type">` 防止类型变更) |
| `components/lesson-plan-list.tsx` | 课案列表(V3:完全通过 service 调用,移除直接 import `getLessonPlansAction`) |
| `components/lesson-plan-card.tsx` | 课案卡片(V3:完全通过 service 调用,移除直接 import actions) |
| `components/lesson-plan-filters.tsx` | 课案筛选器 |
| `components/lesson-plan-editor.tsx` | 课案编辑器(V3:完全通过 service 调用,移除直接 import 5 个 actions;所有操作改为 `service.updateLessonPlan`/`service.saveLessonPlanVersion`/`service.getLessonPlanById`/`service.publishLessonPlan`/`service.unpublishLessonPlan`) |
| `components/lesson-plan-readonly-view.tsx` | **只读画布组件**(V3:MiniMap 使用 `getNodeColor` 替代硬编码颜色) |
| `components/node-editor.tsx` | **节点图画布**(V3:MiniMap `nodeColor` 使用类型守卫替代 `as { node?: AnyLessonPlanNode }` 断言) |
| `components/node-edit-panel.tsx` | **侧边内容编辑面板**(V3:移除冗余 `as LessonPlanNode` 断言,TypeScript 判别联合自动收窄) |
| `components/nodes/lesson-node.tsx` | **自定义教学节点组件** |
| `components/nodes/textbook-content-node.tsx` | **正文节点组件**(V3 续:拆分为 3 文件,主文件 471 行;`as unknown as` 替换为 `isTextbookContentNodePropsData` 类型守卫) |
| `components/nodes/anchor-node-selector.tsx` | **锚点节点选择器(V3 续新增,从 textbook-content-node.tsx 抽取)**:渲染可锚定教学节点列表 + 关联到选中节点 + 创建新节点选项 |
| `components/nodes/textbook-segments.tsx` | **锚点段落渲染函数(V3 续新增,从 textbook-content-node.tsx 抽取)**:renderSegments 遍历 segments 数组渲染文本/区间锚点/点锚点 |
| `components/lesson-plan-error-boundary.tsx` | **错误边界**(V3 续:重写为包装组件模式——内部类组件接受 `errorText`/`retryText` props,外部函数组件通过 `useTranslations` 注入 i18n 文案) |
| `components/lesson-plan-skeleton.tsx` | **骨架屏**:VersionListSkeleton/QuestionBankSkeleton/KnowledgePointSkeleton/LessonPlanListSkeleton |
| `components/block-renderer.tsx` | ⚠️ @deprecated Block 渲染器(已被 NodeEditor 替代,保留向后兼容) |
| `components/template-picker.tsx` | 模板选择器(i18n 已接入;V2-6:create 调用 tracker.track;V3:加载并显示个人模板(调用 getLessonPlanTemplatesAction),分区显示系统/个人模板) |
| `components/version-history-drawer.tsx` | 版本历史抽屉(i18n 已接入;V2-6:revert 调用 tracker.track) |
| `components/knowledge-point-picker.tsx` | 知识点选择器(i18n 已接入) |
| `components/question-bank-picker.tsx` | 题库选择器(i18n 已接入) |
| `components/inline-question-editor.tsx` | 内联题目编辑器(i18n 已接入;V2-5:type/difficulty select label htmlFor 关联) |
| `components/publish-homework-dialog.tsx` | 发布作业对话框(i18n 已接入;V2-6:publish 调用 tracker.track) |
| `components/blocks/rich-text-block.tsx` | 富文本 Block(被 NodeEditPanel 复用,i18n 已接入) |
| `components/blocks/text-study-block.tsx` | 课文研读 Block(被 NodeEditPanel 复用,i18n 已接入) |
| `components/blocks/exercise-block.tsx` | 练习 Block(被 NodeEditPanel 复用,使用 router.refresh 替代 window.location.reload,i18n 已接入;V2-5:purpose select label 关联 + 题目列表 ul/li 语义化) |
| `components/blocks/reflection-block.tsx` | 反思 Block(被 NodeEditPanel 复用,i18n 已接入) |
| `components/template-picker.tsx` | 模板选择器(V3 续:完全通过 service 调用,移除直接 import 4 个 actions) |
| `components/version-history-drawer.tsx` | 版本历史抽屉(V3 续:完全通过 service 调用,移除直接 import 2 个 actions) |
| `components/knowledge-point-picker.tsx` | 知识点选择器(V3 续:完全通过 service 调用,移除直接 import actions-kp) |
| `components/question-bank-picker.tsx` | 题库选择器(V3 续:完全通过 service 调用,移除跨模块直接 import `@/modules/questions/actions`;新增 `isQuestionType` 类型守卫替代 `as QuestionType`) |
| `components/inline-question-editor.tsx` | 内联题目编辑器 |
| `components/publish-homework-dialog.tsx` | 发布作业对话框(V3 续:完全通过 service 调用,移除直接 import actions-publish) |
| `components/blocks/rich-text-block.tsx` | 富文本 Block |
| `components/blocks/text-study-block.tsx` | 课文研读 Block |
| `components/blocks/objective-block.tsx` | 教学目标 Block(V3 续:select onChange 使用 `isObjectiveDimension` 类型守卫替代 `as` 断言) |
| `components/blocks/key-point-block.tsx` | 重难点 Block(V3 续:select onChange 使用 `isKeyPointType` 类型守卫替代 `as` 断言) |
| `components/blocks/import-block.tsx` | 导入 Block(V3 续:select onChange 使用 `isImportMethod` 类型守卫替代 `as` 断言) |
| `components/blocks/new-teaching-block.tsx` | 新授 Block |
| `components/blocks/summary-block.tsx` | 总结 Block |
| `components/blocks/homework-block.tsx` | 作业 Block(V3 续:select onChange 使用 `isHomeworkType` 类型守卫替代 `as` 断言) |
| `components/blocks/blackboard-block.tsx` | 板书 Block(V3 续:select onChange 使用 `isBlackboardLayout` 类型守卫替代 `as` 断言) |
| `components/blocks/exercise-block.tsx` | 练习 Block(V3 续:select onChange 使用 `isExercisePurpose` 类型守卫替代 `as` 断言) |
| `components/blocks/reflection-block.tsx` | 反思 Block(V3 续:select onChange 使用 `isReflectionAspect` 类型守卫替代 `as` 断言) |
---
@@ -1977,28 +2008,36 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
| 文件 | 行数 | 职责 |
|------|------|------|
| `actions.ts` | ~180 | 9 个 Server Actions,全部使用 `requirePermission()` + `ActionState<T>` |
| `data-access.ts` | ~960 | 16 个数据访问函数 + 自动采集逻辑(SM-2 算法已提取到独立模块) |
| `data-access.ts` | ~1280 | 19 个数据访问函数 + 自动采集逻辑(SM-2 算法已提取到独立模块,支持 subjectId 过滤、章节维度、班级分组、学科概览) |
| `sm2-algorithm.ts` | ~180 | SM-2 间隔重复算法(独立纯函数模块,可替换,支持时间注入测试) |
| `sm2-algorithm.test.ts` | ~280 | SM-2 算法单元测试(39 个测试用例,覆盖所有函数和边界条件) |
| `schema.ts` | ~60 | 4 个 Zod 验证 schema |
| `types.ts` | ~120 | 6 个类型定义 + 状态映射常量 + 错误标签常量 |
| `types.ts` | ~245 | 11 个类型定义 + 状态映射常量 + 错误标签常量(新增 ChapterWeakness/ClassErrorOverview/SubjectErrorOverview) |
| `components/error-book-stats-cards.tsx` | ~80 | 5 个统计卡片(总数/待学习/学习中/已掌握/待复习) |
| `components/analytics-stats-cards.tsx` | ~110 | 教师/管理员分析视图 5 个统计卡片(覆盖学生/错题总数/平均掌握率/待复习/知识点数) |
| `components/error-book-filters.tsx` | ~100 | 筛选栏(搜索/状态/来源/待复习),使用 nuqs |
| `components/error-book-item-card.tsx` | ~150 | 错题卡片(预览/标签/笔记/掌握度/操作) |
| `components/review-buttons.tsx` | ~80 | 4 按钮复习面板(again/hard/good/easy) |
| `components/error-book-detail-dialog.tsx` | ~250 | 详情对话框(题目/答案/复习/笔记/历史) |
| `components/error-book-list.tsx` | ~60 | 网格列表 |
| `components/add-error-book-dialog.tsx` | ~180 | 手动添加对话框(题库选择 + 标签) |
| `components/class-error-overview.tsx` | ~200 | 班级错题概览(教师/管理员视图) |
| `components/subject-tabs.tsx` | ~95 | 学科切换 Tab(显示每个学科错题数概览,URL 参数持久化) |
| `components/class-filter.tsx` | ~85 | 班级筛选器(显示每个班级错题数和待复习数,URL 参数持久化) |
| `components/class-error-bar-chart.tsx` | ~115 | 班级错题数对比柱状图(recharts,tooltip 显示学生数/人均/掌握率) |
| `components/subject-distribution-chart.tsx` | ~110 | 学科错题分布柱状图(管理员视图,recharts) |
| `components/knowledge-point-weakness-chart.tsx` | ~150 | 知识点薄弱度横向柱状图(按掌握率红/黄/绿着色,显示章节归属) |
| `components/chapter-weakness-chart.tsx` | ~165 | 章节错题分布横向柱状图(哪些课在错,含 Top 3 薄弱知识点) |
| `components/grouped-student-error-table.tsx` | ~180 | 按班级分组的学生错题表格(可展开/折叠,显示每个学生的错题详情) |
| `components/class-error-overview.tsx` | ~200 | 班级错题概览(旧版,保留兼容) |
| `components/top-wrong-questions.tsx` | ~80 | 高频错题列表(Top 10) |
**路由清单**:
| 路由 | 文件 | 说明 |
|------|------|------|
| `/student/error-book` | `page.tsx` + `loading.tsx` + `error.tsx` | 学生错题本(统计/筛选/列表/手动添加/详情复习) |
| `/teacher/error-book` | `page.tsx` + `loading.tsx` + `error.tsx` | 教师错题分析(班级概览/薄弱知识点/学科分布/高频错题) |
| `/teacher/error-book` | `page.tsx` + `loading.tsx` + `error.tsx` | 教师错题分析(学科Tab/班级筛选/统计卡片/班级对比图/章节错题图/知识点薄弱度图/按班级分组学生表/高频错题) |
| `/parent/error-book` | `page.tsx` + `loading.tsx` + `error.tsx` | 家长错题本(子女错题统计/薄弱知识点/高频错题) |
| `/admin/error-book` | `page.tsx` + `loading.tsx` + `error.tsx` | 管理员错题分析(全校错题统计/薄弱知识点/学科分布/高频错题) |
| `/admin/error-book` | `page.tsx` + `loading.tsx` + `error.tsx` | 管理员错题分析(学科Tab/统计卡片/学科分布图/章节错题图/知识点薄弱度图/按班级分组学生Top50/高频错题) |
**数据库表**:
| 表 | 说明 |
@@ -2150,6 +2189,138 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
---
## 2.30 adaptive-practice(专项练习模块)— ✅ 新增(核心教学链路闭环)
**职责**:实现"错题集 → 知识点 → 专项出题 → 学生答题 → 自动判分 → 教师/年级主任宏观数据分析"的完整闭环。支持四种出题策略:错题变式、知识点专项、薄弱章节、AI 推荐。提供教师端与年级主任端的宏观数据分析工作台。
**架构定位**:
- 位于 `modules/` 层,严格遵循三层架构
- 通过 `data-access.ts` 提供 CRUD 操作,通过 `data-access-strategy.ts` 实现出题策略,通过 `data-access-analytics.ts` 提供教师/年级分析查询
- 跨模块通信:通过 `modules/error-book/data-access` 获取错题数据,通过 `modules/questions/data-access` 获取题目数据,通过 `modules/classes/data-access` 获取班级/学生数据,通过 `modules/users/data-access` 获取用户数据
- Server Actions 通过 `requirePermission()` 校验 `ADAPTIVE_PRACTICE_READ` / `ADAPTIVE_PRACTICE_MANAGE` 权限
**核心导出**:
| 类型 | 名称 | 文件 | 说明 |
|------|------|------|------|
| **DB Schema** | `practiceSessions` | `shared/db/schema.ts` | 练习会话表(学生 ID/类型/状态/统计) |
| **DB Schema** | `practiceAnswers` | `shared/db/schema.ts` | 练习答题记录表(含变式题支持) |
| **Server Actions** | `getPracticeSessionsAction` | `modules/adaptive-practice/actions.ts` | 获取练习列表(权限:ADAPTIVE_PRACTICE_READ) |
| **Server Actions** | `getPracticeSessionDetailAction` | `modules/adaptive-practice/actions.ts` | 获取练习详情(权限:ADAPTIVE_PRACTICE_READ) |
| **Server Actions** | `getPracticeStatsAction` | `modules/adaptive-practice/actions.ts` | 获取练习统计(权限:ADAPTIVE_PRACTICE_READ) |
| **Server Actions** | `createPracticeSessionAction` | `modules/adaptive-practice/actions.ts` | 创建练习会话(权限:ADAPTIVE_PRACTICE_MANAGE) |
| **Server Actions** | `submitPracticeAnswerAction` | `modules/adaptive-practice/actions.ts` | 提交答案+自动判分(权限:ADAPTIVE_PRACTICE_MANAGE) |
| **Server Actions** | `completePracticeSessionAction` | `modules/adaptive-practice/actions.ts` | 完成练习(权限:ADAPTIVE_PRACTICE_MANAGE) |
| **Server Actions** | `abandonPracticeSessionAction` | `modules/adaptive-practice/actions.ts` | 放弃练习(权限:ADAPTIVE_PRACTICE_MANAGE) |
| **Data Access** | `getPracticeSessions` / `getPracticeSessionById` | `data-access.ts` | 学生端 CRUD |
| **Data Access** | `createPracticeSession` | `data-access.ts` | 创建会话(事务:会话+答题记录) |
| **Data Access** | `submitPracticeAnswer` | `data-access.ts` | 提交答案+自动判分 |
| **Data Access** | `autoGradeAnswer` | `data-access.ts` | 自动判分逻辑(选择题/判断题自动,填空题返回 null) |
| **Strategy** | `selectQuestionsForPractice` | `data-access-strategy.ts` | 出题策略主入口 |
| **Strategy** | `selectForErrorVariant` | `data-access-strategy.ts` | 错题变式策略 |
| **Strategy** | `selectForKnowledgePoint` | `data-access-strategy.ts` | 知识点专项策略 |
| **Strategy** | `selectForWeakChapter` | `data-access-strategy.ts` | 薄弱章节策略 |
| **Strategy** | `selectForAiRecommended` | `data-access-strategy.ts` | AI 推荐策略 |
| **Analytics** | `getClassPracticeStats` | `data-access-analytics.ts` | 班级练习统计 |
| **Analytics** | `getClassStudentPracticeSummaries` | `data-access-analytics.ts` | 班级学生练习摘要 |
| **Analytics** | `getGradePracticeStats` | `data-access-analytics.ts` | 年级练习统计 |
| **Analytics** | `getPracticeTypeBreakdown` | `data-access-analytics.ts` | 练习类型分布 |
| **Analytics** | `getStudentsWithoutPractice` | `data-access-analytics.ts` | 识别未参与练习学生 |
| **Analytics** | `getTeacherClassPracticeOverviews` | `data-access-analytics.ts` | 教师所教班级练习概览 |
| **Analytics** | `getClassKnowledgePointWeakness` | `data-access-analytics.ts` | 班级知识点薄弱度(基于练习答题) |
| **Analytics** | `getGradeClassPracticeComparison` | `data-access-analytics.ts` | 年级各班级练习对比 |
| **Analytics** | `getClassLearningProfile` | `data-access-analytics.ts` | 班级综合学习画像(跨模块整合) |
| **Analytics** | `getGradePracticeOverview` | `data-access-analytics.ts` | 年级综合练习统计 |
| **Component** | `PracticeStarter` | `components/practice-starter.tsx` | 练习发起器(四种模式选择) |
| **Component** | `PracticeSessionView` | `components/practice-session-view.tsx` | 答题界面(逐题作答+自动判分) |
| **Component** | `PracticeHistory` | `components/practice-history.tsx` | 练习历史列表 |
| **Component** | `PracticeStatsCards` | `components/practice-stats-cards.tsx` | 学生端统计卡片 |
| **Component** | `PracticeOverviewStatsCards` | `components/practice-overview-stats-cards.tsx` | 教师/年级端统计卡片 — 新增 |
| **Component** | `ClassPracticeComparisonTable` | `components/class-practice-comparison-table.tsx` | 班级练习对比表格 — 新增 |
| **Component** | `PracticeTypeBreakdownChart` | `components/practice-type-breakdown-chart.tsx` | 练习类型分布柱状图 — 新增 |
| **Component** | `ClassKnowledgePointWeaknessChart` | `components/class-knowledge-point-weakness-chart.tsx` | 知识点薄弱度柱状图 — 新增 |
| **Component** | `StudentPracticeRankingTable` | `components/student-practice-ranking-table.tsx` | 学生练习排名表格 — 新增 |
| **Component** | `InactiveStudentsAlert` | `components/inactive-students-alert.tsx` | 未参与练习学生提醒 — 新增 |
**集成点**:
| 业务模块 | 集成组件 | 页面 | 说明 |
|---------|---------|------|------|
| error-book | `createPracticeSessionAction` | `student/error-book` 详情弹窗 | 错题详情页"发起变式练习"按钮 |
| student | `PracticeStarter` / `PracticeSessionView` | `student/practice` / `student/practice/[sessionId]` | 学生端练习入口与答题 |
| teacher | `PracticeOverviewStatsCards` 等 | `teacher/practice` | 教师端专项练习分析 — 新增 |
| management | `PracticeOverviewStatsCards` 等 | `management/grade/practice` | 年级主任宏观数据分析 — 新增 |
**依赖关系**:
- `modules/adaptive-practice` → `shared/db`(schema: practiceSessions/practiceAnswers/questions/knowledgePoints 等)
- `modules/adaptive-practice` → `shared/lib/auth-guard`(权限校验)
- `modules/adaptive-practice` → `shared/types/permissions`(权限常量)
- `modules/adaptive-practice` → `shared/types/action-state`(返回值类型)
- `modules/adaptive-practice` → `modules/classes/data-access`(getActiveStudentIdsByClassId/getClassNameById/getClassesByGradeId/getClassIdsByGradeIds/getStudentIdsByClassIds)
- `modules/adaptive-practice` → `modules/users/data-access`(getUserIdsByGradeId/getUserNamesByIds)
- `modules/adaptive-practice` → `modules/questions/data-access`(题目查询,出题策略使用)
- `modules/adaptive-practice` → `modules/error-book/data-access`(错题数据,错题变式策略使用)
- `app/(dashboard)/student/practice` → `modules/adaptive-practice`(学生端页面)
- `app/(dashboard)/teacher/practice` → `modules/adaptive-practice`(教师端分析页面)— 新增
- `app/(dashboard)/management/grade/practice` → `modules/adaptive-practice`(年级主任分析页面)— 新增
- `modules/error-book/components/error-book-detail-dialog` → `modules/adaptive-practice/actions`(变式练习入口)
**权限点**:
- `ADAPTIVE_PRACTICE_READ`:查看练习数据(admin/teacher/student/parent/grade_head/teaching_head 均有)
- `ADAPTIVE_PRACTICE_MANAGE`:创建/提交/完成/放弃练习(仅 student 有)
**自动判分逻辑**:
- `single_choice`:学生答案 ID 与正确答案 ID 完全匹配
- `multiple_choice`:学生答案 ID 集合与正确答案 ID 集合完全匹配(集合比对)
- `judgment`:学生布尔答案与正确布尔答案一致
- `text`(填空题):不自动判分,返回 `null`,待教师批阅
**出题策略**:
- `error_variant`:按难度升序选取原题(确保 AI 不可用时也能练习)
- `knowledge_point`:按知识点+难度筛选,随机抽取
- `weak_chapter`:排除已答题目,避免重复
- `ai_recommended`:从 AI 推荐知识点抽题
**文件清单**:
| 文件 | 行数 | 职责 |
|------|------|------|
| `modules/adaptive-practice/types.ts` | ~143 | 类型定义(PracticeType/PracticeSourceMeta 联合类型等) |
| `modules/adaptive-practice/schema.ts` | ~75 | Zod 验证 schema |
| `modules/adaptive-practice/data-access.ts` | ~470 | CRUD 操作+自动判分逻辑 |
| `modules/adaptive-practice/data-access-strategy.ts` | ~310 | 四种出题策略实现 |
| `modules/adaptive-practice/data-access-analytics.ts` | ~630 | 教师/年级宏观数据分析查询 |
| `modules/adaptive-practice/actions.ts` | ~230 | 7 个 Server Actions(含权限校验) |
| `modules/adaptive-practice/components/practice-starter.tsx` | ~270 | 练习发起器 |
| `modules/adaptive-practice/components/practice-session-view.tsx` | ~420 | 答题界面 |
| `modules/adaptive-practice/components/practice-history.tsx` | ~100 | 练习历史列表 |
| `modules/adaptive-practice/components/practice-stats-cards.tsx` | ~80 | 学生端统计卡片 |
| `modules/adaptive-practice/components/practice-overview-stats-cards.tsx` | ~95 | 教师/年级端统计卡片 — 新增 |
| `modules/adaptive-practice/components/class-practice-comparison-table.tsx` | ~95 | 班级练习对比表格 — 新增 |
| `modules/adaptive-practice/components/practice-type-breakdown-chart.tsx` | ~120 | 练习类型分布柱状图 — 新增 |
| `modules/adaptive-practice/components/class-knowledge-point-weakness-chart.tsx` | ~150 | 知识点薄弱度柱状图 — 新增 |
| `modules/adaptive-practice/components/student-practice-ranking-table.tsx` | ~130 | 学生练习排名表格 — 新增 |
| `modules/adaptive-practice/components/inactive-students-alert.tsx` | ~75 | 未参与练习学生提醒 — 新增 |
| `app/(dashboard)/student/practice/page.tsx` | - | 学生端练习列表页 |
| `app/(dashboard)/student/practice/[sessionId]/page.tsx` | - | 学生端答题页 |
| `app/(dashboard)/teacher/practice/page.tsx` | ~250 | 教师端专项练习分析页 — 新增 |
| `app/(dashboard)/teacher/practice/loading.tsx` | - | 教师端加载骨架 — 新增 |
| `app/(dashboard)/teacher/practice/error.tsx` | - | 教师端错误边界 — 新增 |
| `app/(dashboard)/management/grade/practice/page.tsx` | ~140 | 年级主任宏观数据分析页 — 新增 |
| `app/(dashboard)/management/grade/practice/loading.tsx` | - | 年级端加载骨架 — 新增 |
| `app/(dashboard)/management/grade/practice/error.tsx` | - | 年级端错误边界 — 新增 |
**i18n**:
- 翻译文件:`shared/i18n/messages/{locale}/practice.json`
- 命名空间:`practice`
- 键:`starter.*`、`session.*`、`result.*`、`history.*`、`stats.*`、`types.*`、`status.*`、`teacher.*`(含 overview/classComparison/typeBreakdown/knowledgePointWeakness/studentRanking/inactiveStudents)、`grade.*`(含 overview/classComparison)
**数据库表**:
- `practice_sessions`:练习会话表(id/studentId/subjectId/practiceType/sourceMeta/status/totalQuestions/answeredQuestions/correctCount/startedAt/completedAt)
- `practice_answers`:练习答题记录表(id/sessionId/studentId/questionId/variantContent/isVariant/orderIndex/status/studentAnswer/isCorrect/score/maxScore)
---
# 第三部分:已知架构问题和技术债
## 3.1 P0 严重问题(必须立即修复)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,195 @@
# 备课模块使用问题修复设计
> **日期**:2026-06-23
> **前置文档**:[2026-06-22-lesson-preparation-anchor-canvas-design.md](./2026-06-22-lesson-preparation-anchor-canvas-design.md)
> **审计来源**:深度分析备课模块现状后发现的使用问题
## 一、问题背景
备课模块 v3 锚点画布重构已完成,但实际使用中存在 15 个问题(3 个 P0 阻塞性 + 6 个 P1 严重 + 6 个 P2 一般),导致核心流程断裂、角色使用不完整。
### 核心闭环断裂
```
教师备课 → 发布课案 → 学生查看 → 家长查看
↑ ❌ P0-1 ❌ P0-2/3 ❌ P0-2/3
无"发布"action 无权限/路由 无权限/路由
```
## 二、功能定位
### 备课模块核心闭环
1. **教师备课**:选定教材+章节 → 锚点画布编辑 → 保存版本 → **发布**
2. **学生查看**:按班级可见 published 课案,只读画布视图
3. **家长查看**:通过孩子关系自动可见 published 课案
4. **教研监督**:教研组长/年级主任查看本年级教师备课(只读)
5. **管理员管理**:全校课案列表 + 统计
## 三、角色使用矩阵
| 角色 | 权限 | 路由 | 视图 |
|------|------|------|------|
| 教师 | CREATE/READ/UPDATE/DELETE/PUBLISH | `/teacher/lesson-plans` | 完整编辑画布 + 发布按钮 |
| 学生 | READ(仅 published) | `/student/lesson-plans/[planId]/view` | 只读画布视图 |
| 家长 | READ(仅孩子的 published) | `/parent/lesson-plans/[planId]/view` | 只读画布视图 |
| 管理员 | 全部 | `/admin/lesson-plans` | 全校课案列表 + 统计 |
| 教研组长 | READ(本年级教师) | `/grade-head/lesson-plans` | 本年级教师课案列表(只读) |
| 年级主任 | READ(本年级教师) | `/teaching-head/lesson-plans` | 同上 |
### 学生查看视图设计
**只读画布视图**:复用 React Flow,配置:
- `nodesDraggable={false}`
- `nodesConnectable={false}`
- `elementsSelectable={true}`(可点击查看节点详情)
- `panOnDrag={true}`、`zoomOnScroll={true}`(可缩放平移)
保留正文+节点+锚点的完整视觉关系,学生可缩放平移但不可编辑。
### 发布机制
- 发布到班级所有学生,家长通过孩子关系自动可见
- 状态流转:`draft` → `published`(发布)→ `draft`(撤回)或 `archived`(归档)
- 发布后学生/家长立即可见,撤回后立即不可见
## 四、修复范围(P0+P1 优先)
### P0 阻塞性问题
| 编号 | 问题 | 修复方案 |
|------|------|----------|
| P0-1 | 无"发布课案"action | 新增 `publishLessonPlanAction` / `unpublishLessonPlanAction` |
| P0-2 | 学生/家长无权限 | 给 student/parent 添加 `LESSON_PLAN_READ` 权限 |
| P0-3 | 无学生/家长/管理员路由 | 新增路由 + 导航入口 |
### P1 严重问题
| 编号 | 问题 | 修复方案 |
|------|------|----------|
| P1-1 | 锚点节点选择器占位 | 完善 `AnchorNodeSelector`,接收节点列表 + 实现"锚定到新节点" |
| P1-2 | 锚点偏移计算不可靠 | 改用 `markdownToPlainText` 搜索定位,支持跨段落 |
| P1-3 | 边回写丢失 anchorId | anchorId 存入 `edge.data`,`fromRfEdges` 从 data 读取 |
| P1-4 | 锚点边颜色无区分 | 使用 `getNodeColor(anchor.nodeId)` |
| P1-5 | 教研组长无权限 | 给 grade_head/teaching_head 添加 `LESSON_PLAN_READ` 权限 |
| P1-6 | 个人模板不显示 | TemplatePicker 调用 `getLessonPlanTemplatesAction` 合并显示 |
### P2 一般问题(本次顺带修复)
| 编号 | 问题 | 修复方案 |
|------|------|----------|
| P2-1 | 正文节点侧边面板提示误导 | 改为"请在画布上直接操作正文"或显示锚点列表 |
| P2-5 | 默认骨架节点位置重叠 | 增大列间距,避免与正文节点重叠 |
## 五、实施阶段
### 阶段 1:权限与路由基础(P0-2、P0-3、P1-5)
**权限修改**(`shared/lib/permissions.ts`):
- student 数组添加 `Permissions.LESSON_PLAN_READ`
- parent 数组添加 `Permissions.LESSON_PLAN_READ`
- grade_head 数组添加 `Permissions.LESSON_PLAN_READ`
- teaching_head 数组添加 `Permissions.LESSON_PLAN_READ`
**路由新增**:
- `app/(dashboard)/student/lesson-plans/page.tsx` — 课案列表(按班级 published)
- `app/(dashboard)/student/lesson-plans/[planId]/view/page.tsx` — 只读画布
- `app/(dashboard)/parent/lesson-plans/page.tsx` — 课案列表(按孩子 published)
- `app/(dashboard)/parent/lesson-plans/[planId]/view/page.tsx` — 只读画布
- `app/(dashboard)/admin/lesson-plans/page.tsx` — 全校课案列表 + 统计
- `app/(dashboard)/admin/lesson-plans/[planId]/view/page.tsx` — 查看任意课案
**导航入口**(`shared/lib/navigation.ts` 或 `modules/layout/config/navigation.ts`):
- student 导航添加"我的课案"
- parent 导航添加"孩子课案"
- admin 导航添加"课案管理"
**只读画布组件**:
- 新建 `components/lesson-plan-readonly-view.tsx`
- 复用 React Flow,配置只读模式
- 复用 `toRfNodes` / `toRfEdges` 渲染
### 阶段 2:发布机制(P0-1)
**data-access 新增**(`data-access.ts`):
```typescript
export async function publishLessonPlan(planId: string, userId: string): Promise<void>
export async function unpublishLessonPlan(planId: string, userId: string): Promise<void>
```
**actions 新增**(`actions.ts`):
```typescript
export async function publishLessonPlanAction(planId: string): Promise<ActionState<string>>
export async function unpublishLessonPlanAction(planId: string): Promise<ActionState<string>>
```
**UI 修改**:
- `LessonPlanEditor` 工具栏添加"发布"/"撤回发布"按钮
- `LessonPlanCard` 添加"发布"/"撤回"操作菜单项
- 发布时显示确认对话框
### 阶段 3:锚点交互完善(P1-1、P1-2、P1-3、P1-4)
**P1-1 完善 AnchorNodeSelector**:
- 接收 `doc.nodes` 列表(过滤掉 textbook_content)
- 渲染节点下拉选择器(显示节点标题 + 颜色标识)
- 实现"锚定到新节点":弹出节点类型选择 → 创建节点 → 自动锚定
**P1-2 修复锚点偏移计算**:
- `handleMouseUp` 改用 `Range.toString()` 获取选中文本
- 在 `markdownToPlainText(content)` 中搜索定位 start/end 偏移
- 支持跨段落、跨行内元素的选择
**P1-3 修复 fromRfEdges anchorId 丢失**:
- `toRfEdges` 时将 `anchorId` 存入 `edge.data.anchorId`
- `fromRfEdges` 从 `e.data?.anchorId` 读取
**P1-4 修复锚点边颜色**:
- `rf-mappers.ts` 导入 `getNodeColor`
- `toRfEdges` 中 `stroke: getNodeColor(anchor.nodeId)`
### 阶段 4:模板与体验(P1-6 + P2)
**P1-6 TemplatePicker 显示个人模板**:
- 初始化时调用 `getLessonPlanTemplatesAction()`
- 合并系统模板和个人模板展示
- 个人模板标记"个人"徽章
**P2-1 修复正文节点侧边面板**:
- `NodeEditPanel` 选中 textbook_content 时显示锚点列表
- 或显示提示"请在画布上直接操作正文,点击正文选中文本可创建锚点"
**P2-5 优化默认骨架节点位置**:
- 增大列间距,左列 x=80,右列 x=900
- 正文节点居中 (500, 250)
## 六、数据模型变更
无数据模型变更。发布机制仅修改 `lessonPlans.status` 字段(已存在)。
## 七、i18n 键新增
### zh-CN / en grades 命名空间(lesson-preparation.json)
- `action.publish` / `action.unpublish`
- `action.publishConfirm` / `action.unpublishConfirm`
- `action.publishSuccess` / `action.unpublishSuccess`
- `status.published` / `status.draft` / `status.archived`
- `readonly.title` / `readonly.hint`
- `admin.title` / `admin.stats` / `admin.totalPlans` / `admin.publishedPlans`
- `gradeHead.title`
- `anchor.selectNode` / `anchor.createNode` / `anchor.nodeTypePrompt`
## 八、架构文档同步
- `004_architecture_impact_map.md`:补充路由、权限、新组件
- `005_architecture_data.json`:补充 routes、permissions、exports
## 九、验证标准
- `npx tsc --noEmit` 零错误
- `npm run lint` 零错误
- 教师可发布课案,学生/家长可查看 published 课案
- 教研组长可查看本年级教师备课
- 锚点交互完整可用(选中文本 → 选择节点 → 创建锚点)
- 锚点边颜色与关联节点一致