feat: 新增备课模块并修复全模块 P0/P1/P2 缺陷
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
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
主要变更: - 新增 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)
This commit is contained in:
@@ -390,6 +390,27 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- `validatePassword()` / `isAccountLocked()` / `rateLimit()` — 安全策略
|
||||
- `exportToExcel()` / `parseExcel()` / `generateTemplate()` — Excel 工具
|
||||
- `cn()` / `formatDate()` / `formatFileSize()` — 通用工具
|
||||
- `getInitials(name)` / `formatDateForFile(d?)` — 通用工具(P1-c / P1-a 重构新增:从 parent/lib/utils.ts、grades/export-button.tsx 等多处重复实现抽取)
|
||||
- `downloadBase64File(base64, filename, mimeType?)` / `downloadBlob(blob, filename)` — 客户端文件下载(P1-c 重构新增:从 grades/export-button、users/user-import-dialog、audit/audit-log-export-button 三处重复实现抽取,位于 `lib/download.ts`)
|
||||
|
||||
**共享组件导出**(P0-b / P1-a / P1-b / P1-c / P2-a / P2-b / P3-a / P3-b / P3-c / P3-d 重构新增,按类别组织):
|
||||
|
||||
| 类别 | 组件 | 文件 | 用途 | 消费方数量 |
|
||||
|------|------|------|------|-----------|
|
||||
| **UI 组件** | `StatCard` | `components/ui/stat-card.tsx` | 统计卡片(标题+数值+图标+描述+跳转+骨架屏) | 8 个(P1-a) |
|
||||
| **UI 组件** | `StatItem` | `components/ui/stat-item.tsx` | 紧凑统计项(label+icon+value+hint,用于统计面板网格) | 8 个(P1-a) |
|
||||
| **UI 组件** | `ChipNav` | `components/ui/chip-nav.tsx` | 芯片导航组(通过 URL search params 切换筛选维度,Link 跳转) | 3 个(P1-b) |
|
||||
| **UI 组件** | `PageHeader` | `components/ui/page-header.tsx` | 页面头部(标题+描述+icon+actions,响应式布局) | 2 个(P2-b: profile/page.tsx, settings/security/page.tsx) |
|
||||
| **UI 组件** | `FilterBar` / `FilterSearchInput` / `FilterResetButton` | `components/ui/filter-bar.tsx` | 筛选栏容器+搜索框+重置按钮(统一布局壳,URL 状态由各模块处理) | 5 个(P3-b: exam/textbook/question/audit-log/login-log filters) |
|
||||
| **图表组件** | `ChartCardShell` | `components/charts/chart-card-shell.tsx` | 图表卡片外壳(Card+Header+EmptyState+Content 统一结构) | 8 个(P3-c) |
|
||||
| **图表组件** | `TrendLineChart` | `components/charts/trend-line-chart.tsx` | 趋势折线图(LineChart 统一配置,支持单/多系列) | 8 个(P3-c: grade-trend-chart 等) |
|
||||
| **图表组件** | `SimpleBarChart` | `components/charts/simple-bar-chart.tsx` | 柱状图(BarChart 统一配置,支持单/多 Bar + Cell 分桶着色) | 8 个(P3-c: grade-distribution-chart 等) |
|
||||
| **图表组件** | `ComparisonRadarChart` | `components/charts/comparison-radar-chart.tsx` | 对比雷达图(RadarChart 统一配置,支持双 Radar 对比) | 8 个(P3-c: subject-comparison-chart, mastery-radar-chart 等) |
|
||||
| **课表组件** | `ScheduleList` / `ScheduleListItem` | `components/schedule/schedule-list.tsx` | 课表列表+列表项(课程+时间+地点+班级徽章,separator/card 两种变体) | 3 个(P3-a: student-today-schedule-card, child-schedule-card, student-schedule-view) |
|
||||
| **题库组件** | `QuestionBankFilters` | `components/question/question-bank-filters.tsx` | 题库筛选栏(搜索+题型+难度,default/compact 两种布局) | 2 个(P3-d: exam-assembly, question-bank-picker) |
|
||||
| **设置组件** | `SettingsView` | `modules/settings/components/settings-view.tsx` | 统一设置页布局(4 标签页:General/Notifications/Appearance/Security,角色差异通过 props 注入) | 3 个(P2-a: admin/teacher/student 设置页) |
|
||||
|
||||
> 注:`SettingsView` 位于 `modules/settings/components/`(非 shared 层),因仅被 settings 模块消费,未下沉到 shared。此处列出以完整反映本次重构的组件抽取范围。
|
||||
|
||||
**依赖关系**:
|
||||
- 被依赖方:**所有模块**依赖 shared
|
||||
@@ -431,9 +452,22 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| `lib/file-storage.ts` | - | 文件存储抽象 |
|
||||
| `hooks/use-permission.ts` | - | 客户端权限 Hook |
|
||||
| `components/ui/*` | 34 文件 | shadcn/ui 标准组件 |
|
||||
| `components/ui/stat-card.tsx` | 95 | StatCard 统计卡片(P1-a 新增) |
|
||||
| `components/ui/stat-item.tsx` | 38 | StatItem 紧凑统计项(P1-a 新增) |
|
||||
| `components/ui/chip-nav.tsx` | 78 | ChipNav 芯片导航(P1-b 新增) |
|
||||
| `components/ui/page-header.tsx` | 44 | PageHeader 页面头部(P2-b 新增,含 icon 属性) |
|
||||
| `components/ui/filter-bar.tsx` | 124 | FilterBar + FilterSearchInput + FilterResetButton(P3-b 新增) |
|
||||
| `components/charts/chart-card-shell.tsx` | 90 | ChartCardShell 图表卡片外壳(P3-c 新增) |
|
||||
| `components/charts/trend-line-chart.tsx` | 153 | TrendLineChart 趋势折线图(P3-c 新增) |
|
||||
| `components/charts/simple-bar-chart.tsx` | 162 | SimpleBarChart 柱状图(P3-c 新增) |
|
||||
| `components/charts/comparison-radar-chart.tsx` | 143 | ComparisonRadarChart 对比雷达图(P3-c 新增) |
|
||||
| `components/schedule/schedule-list.tsx` | 112 | ScheduleList + ScheduleListItem 课表列表(P3-a 新增) |
|
||||
| `components/question/question-bank-filters.tsx` | 137 | QuestionBankFilters 题库筛选栏(P3-d 新增) |
|
||||
| `lib/download.ts` | 47 | downloadBase64File + downloadBlob 客户端下载工具(P1-c 新增) |
|
||||
| `lib/utils.ts` | - | 通用工具(P1-a/P1-c 新增 getInitials + formatDateForFile) |
|
||||
| `components/onboarding-gate.tsx` | 312 | 引导流程(业务泄漏) |
|
||||
| `components/global-search.tsx` | 221 | 全局搜索(业务泄漏) |
|
||||
| `types/permissions.ts` | 92 | 54 个权限点常量 |
|
||||
| `types/permissions.ts` | 157 | 61 个权限点常量 + Role/DataScope/AuthContext 类型 |
|
||||
|
||||
---
|
||||
|
||||
@@ -445,6 +479,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- Actions:`createExamAction` / `createAiExamAction` / `previewAiExamAction` / `regenerateAiQuestionAction` / `updateExamAction` / `deleteExamAction` / `duplicateExamAction` / `getExamPreviewAction` / `getSubjectsAction` / `getGradesAction`(✅ P1-2 已修复:actions 层不再直接访问 DB,全部下沉到 data-access)
|
||||
- Data-access:`getExams` / `getExamById` / `persistExamDraft` / `persistAiGeneratedExamDraft` / `buildExamDescription` / `resolveSubjectGradeNames` / `getExamCreatorId` / `updateExamWithQuestions` / `deleteExamById` / `duplicateExam` / `getExamPreview` / `getExamSubjects` / `getExamGrades`(后 7 个为 P1-2 新增)
|
||||
- AI Pipeline:`generateAiCreateDraftFromSource` / `generateAiPreviewData` / `regenerateAiQuestionByInstruction`
|
||||
- Utils:`normalizeStructure`(v3 新增:将持久化的 `exam.structure` unknown JSON 运行时校验并归一化为类型安全的 `ExamNode[]`,类型守卫模式无 `as` 断言,从 `teacher/exams/[id]/build/page.tsx` 提取)
|
||||
|
||||
**依赖关系**:
|
||||
- 依赖:`shared/*`、`@/auth`、`questions`(✅ P0-1 已修复:通过 data-access.createQuestionWithRelations)、`classes`(✅ P0-2 已修复:通过 data-access.getClassGradeIdsByClassIds)、`school`(✅ P1-1 已修复:通过 school data-access.getSubjectOptions/getGradeOptions)
|
||||
@@ -466,6 +501,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| `data-access.ts` | 473 | 考试 CRUD(含 P1-2 新增 7 个写/查询函数,P0-1/P0-2 已修复:通过 questions/classes data-access 跨模块通信) |
|
||||
| `types.ts` | 31 | 类型定义 |
|
||||
| `hooks/use-exam-preview.ts` | 295 | 预览 Hook |
|
||||
| `utils/normalize-structure.ts` | 57 | v3 新增:exam.structure 运行时校验与归一化(从 build/page.tsx 提取) |
|
||||
| `components/*` | 18 文件 | 考试表单/组卷/预览组件 |
|
||||
|
||||
---
|
||||
@@ -601,12 +637,12 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
|
||||
**导出函数**:
|
||||
- Actions:`createTeacherClassAction` / `updateTeacherClassAction` / `deleteTeacherClassAction` / `createAdminClassAction` / `updateAdminClassAction` / `deleteAdminClassAction` / `createGradeClassAction` / `updateGradeClassAction` / `deleteGradeClassAction`
|
||||
- Data-access:`getAdminClasses` / `getTeacherClasses` / `getGradeManagedClasses` / `getStudentClasses` / `getClassDetails` / `getClassStudents` / `getClassSchedule` / `getClassHomeworkInsights` / `getGradeHomeworkInsights` / `getStudentsSubjectScores` / `verifyTeacherOwnsClass`(✅ P0-5 已修复:classSchedule 写函数 createClassScheduleItem/updateClassScheduleItem/deleteClassScheduleItem 已迁移至 scheduling/data-access-class-schedule.ts,classes 模块仅保留 classSchedule 读函数;✅ P2 已修复:`getAccessibleClassIdsForTeacher` 使用 `Promise.all` 并行化 ownedIds 与 assignedIds 查询)
|
||||
- Data-access:`getAdminClasses` / `getTeacherClasses` / `getGradeManagedClasses` / `getStudentClasses` / `getClassDetails` / `getClassStudents` / `getClassSchedule` / `getClassHomeworkInsights` / `getGradeHomeworkInsights` / `getStudentsSubjectScores` / `verifyTeacherOwnsClass` / `getTeacherIdsByClassIds`(获取多个班级的所有教师 ID:班主任 + 任课教师,跨模块接口,供 messaging 模块调用)(✅ P0-5 已修复:classSchedule 写函数 createClassScheduleItem/updateClassScheduleItem/deleteClassScheduleItem 已迁移至 scheduling/data-access-class-schedule.ts,classes 模块仅保留 classSchedule 读函数;✅ P2 已修复:`getAccessibleClassIdsForTeacher` 使用 `Promise.all` 并行化 ownedIds 与 assignedIds 查询)
|
||||
- Schema:`CreateTeacherClassSchema` / `UpdateTeacherClassSchema` / `DeleteTeacherClassSchema` / `CreateAdminClassSchema` / `UpdateAdminClassSchema` / `DeleteAdminClassSchema` / `CreateGradeClassSchema` / `UpdateGradeClassSchema` / `DeleteGradeClassSchema` / `CreateClassScheduleItemSchema` / `UpdateClassScheduleItemSchema` / `DeleteClassScheduleItemSchema` / `EnrollStudentByEmailSchema`
|
||||
|
||||
**依赖关系**:
|
||||
- 依赖:`shared/*`、`@/auth`、`school`(✅ P1-1 已修复:通过 school data-access.isGradeHead/isGradeManager/findGradeIdByHeadAndName)、`homework`(✅ P0-7 已修复:通过 `homework/data-access-classes` 暴露的函数获取作业数据,不再直查 homework/exams 表)
|
||||
- 被依赖:`exams`/`homework`/`grades`/`attendance`/`scheduling`/`dashboard`(通过 data-access,P0-4 已修复)/`parent`/`course-plans`/`users`(✅ P1-1 已修复:8+ 处直查 classes 表改为通过 classes data-access)
|
||||
- 被依赖:`exams`/`homework`/`grades`/`attendance`/`scheduling`/`dashboard`(通过 data-access,P0-4 已修复)/`parent`/`course-plans`/`users`(✅ P1-1 已修复:8+ 处直查 classes 表改为通过 classes data-access)/`messaging`(通过 data-access.getTeacherIdsByClassIds/getStudentActiveClassId,支持学生/家长给班级教师发消息)
|
||||
|
||||
**已知问题**:
|
||||
- ✅ P0-1 已修复:`data-access.ts` 已拆分为 5 个文件(data-access/data-access-stats/data-access-schedule/data-access-students/data-access-admin),所有文件均 ≤800 行
|
||||
@@ -787,11 +823,11 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
|
||||
**导出函数**:
|
||||
- Actions:`sendMessageAction` / `getMessagesAction` / `getMessageAction` / `deleteMessageAction` / `getNotificationsAction` / `markNotificationReadAction` / `markAllNotificationsReadAction` / `getNotificationPreferencesAction` / `updateNotificationPreferencesAction`
|
||||
- Data-access:`getMessages` / `getMessageById` / `getMessageThread` / `createMessage` / `markMessageAsRead` / `deleteMessage` / `getUnreadMessageCount` / `getRecipients`(通知 CRUD 通过 re-export 从 notifications 模块重导出,保持向后兼容)
|
||||
- Notification-preferences:re-export shim(实际逻辑在 `notifications/preferences.ts`)
|
||||
- Data-access:`getMessages` / `getMessageById` / `getMessageThread` / `createMessage` / `markMessageAsRead` / `deleteMessage` / `getUnreadMessageCount` / `getRecipients`(按 DataScope 过滤可发送对象:class_taught 教师→学生、grade_managed 年级管理员→教师/学生、all 管理员、class_members 学生→自己班级的任课教师/班主任、children 家长→孩子的班主任/任课教师;通过 classes data-access.getTeacherIdsByClassIds/getStudentActiveClassId 获取班级教师 ID)(通知 CRUD 通过 re-export 从 notifications 模块重导出,保持向后兼容)
|
||||
- Notification-preferences:~~re-export shim(实际逻辑在 `notifications/preferences.ts`)~~ ✅ P0-b 已修复:`notification-preferences.ts` 文件已删除(通知模块去重),消费方改为直接从 `@/modules/notifications/preferences` 导入 `getNotificationPreferences` / `upsertNotificationPreferences`
|
||||
|
||||
**依赖关系**:
|
||||
- 依赖:`shared/*`、`@/auth`、`notifications`(✅ P0-4 / P1-5 已修复:通过 `sendNotification` dispatcher 发送通知,通知 CRUD 和偏好已迁移至 notifications 模块)
|
||||
- 依赖:`shared/*`、`@/auth`、`notifications`(✅ P0-4 / P1-5 已修复:通过 `sendNotification` dispatcher 发送通知,通知 CRUD 和偏好已迁移至 notifications 模块)、`classes`(通过 data-access.getTeacherIdsByClassIds/getStudentActiveClassId 获取班级教师 ID,支持学生 class_members 和家长 children 数据范围)、`users`(通过 data-access.getUserNamesByIds 获取用户显示名称)
|
||||
- 被依赖:`notifications`(✅ 已消除反向依赖)、`settings`(通知偏好表单)、`layout`(通知下拉)
|
||||
|
||||
**已知问题**:
|
||||
@@ -802,13 +838,14 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- ✅ P1 已修复:~~`markMessageAsReadAction` / `deleteMessageAction` / `getMessageDetailAction` 缺少 Zod 校验~~ 已添加 `MessageIdSchema` 校验 messageId 参数
|
||||
- ✅ P1 已修复:~~`updateNotificationPreferencesAction` 缺少 Zod 校验~~ 已添加 `UpdateNotificationPreferencesSchema` 校验 8 个布尔字段
|
||||
- ✅ P2 已修复:`data-access.ts` 中 3 处 `or(...)!` 非空断言清理为安全守卫(条件 push)
|
||||
- ✅ P0-b 已修复:~~`notification-preferences.ts` re-export shim 文件~~ 已删除(通知模块去重),8 个消费方改为直接从 `@/modules/notifications/preferences` 导入 `getNotificationPreferences` / `upsertNotificationPreferences`,消除 messaging 模块对通知偏好的冗余 re-export 层
|
||||
|
||||
**文件清单**:
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `actions.ts` | 276 | 9 个 Server Action(通知相关 Action 委托 notifications 模块) |
|
||||
| `data-access.ts` | 199 | 私信 CRUD + re-export 通知 CRUD(向后兼容) |
|
||||
| `notification-preferences.ts` | 11 | re-export shim(实际逻辑在 notifications/preferences.ts) |
|
||||
| ~~`notification-preferences.ts`~~ | ~~11~~ | ~~re-export shim~~ ✅ P0-b 已删除(消费方改为直接从 notifications/preferences 导入) |
|
||||
| `schema.ts` | 41 | 私信发送校验 + messageId 校验 + 通知偏好更新校验 |
|
||||
| `types.ts` | 72 | 私信类型 + re-export 通知类型(向后兼容) |
|
||||
|
||||
@@ -918,6 +955,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
**已知问题**:
|
||||
- ✅ P2-13 已修复:~~所有函数 try-catch 吞错误返回空数组/null~~ 所有 catch 块已添加 `console.error` 输出错误上下文
|
||||
- ✅ P2 已修复:`getFileAttachmentsWithFilters` 中 `or(...)!` 非空断言清理为安全守卫
|
||||
- ✅ P2 已修复:~~`getFileAttachmentsWithFilters` 中 `conditions` 隐式 `any[]`~~ 改为显式 `SQL[]` 类型标注
|
||||
- ⚠️ P2:无 `actions.ts`,data-access 被路由直接调用
|
||||
- ✅ 职责单一,不跨模块查询
|
||||
|
||||
@@ -962,22 +1000,28 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
**职责**:家长视角的子女数据聚合与展示。
|
||||
|
||||
**导出函数**:
|
||||
- Data-access:`getChildren` / `getChildBasicInfo` / `getChildDashboardData`(✅ P2 已修复:`getChildBasicInfo` 使用 `Promise.all` 并行化 gradeOptions 与 classId 查询,并添加 `ChildBasicInfo` 显式返回类型;`getChildBasicInfo` 使用 `React.cache()` 包装实现请求级 memoization)
|
||||
- Data-access:`getChildren` / `getChildBasicInfo` / `getChildDashboardData` / `getParentDashboardData` / `verifyParentChildRelation`(✅ P2 已修复:`getChildBasicInfo` 使用 `Promise.all` 并行化 gradeName 与 activeClass 查询;新增 `verifyParentChildRelation` 同时按 parentId + studentId 过滤,防止跨家庭信息泄露;新增 `getStudentActiveClass` 一次 JOIN 返回 classId + className;新增 `getGradeNameById` 替代全量 `getGradeOptions`)
|
||||
|
||||
**依赖关系**:
|
||||
- 依赖:`shared/*`、`@/auth`、`classes`(合理)、`homework`(合理)、`grades`(合理)
|
||||
- 依赖:`shared/*`、`@/auth`、`classes`(合理)、`homework`(合理)、`grades`(合理)、`users`(合理)、`school`(合理)
|
||||
- 被依赖:无
|
||||
|
||||
**已知问题**:
|
||||
- ✅ P2 已修复:~~`getChildBasicInfo` 多次串行查询,可优化为 join~~ 改为使用 `Promise.all` 并行化 gradeOptions 与 classId 查询
|
||||
- ✅ P1 已修复:~~`app/(dashboard)/parent/children/[studentId]/page.tsx` 直接访问 DB(违反三层架构)~~ 改为调用 `verifyParentChildRelation` data-access 函数
|
||||
- ✅ P1 已修复:~~权限校验未加 parentId 条件,存在信息泄露风险~~ `verifyParentChildRelation` 同时按 parentId + studentId 过滤
|
||||
- ✅ P2 已修复:~~`getChildBasicInfo` 多次串行查询~~ 改为 `Promise.all` 并行化,并使用 `getStudentActiveClass` 一次 JOIN
|
||||
- ✅ P2 已修复:~~`getGradeOptions` 全量查询效率低~~ 改为 `getGradeNameById` 按 ID 查询
|
||||
- ✅ P2 已修复:~~`buildHomeworkSummary` 中 `[...assignments].sort()` 不必要拷贝~~ 改为 `toSorted()`
|
||||
- ✅ P2 已修复:~~`in7Days` 死代码~~ 已删除
|
||||
- ✅ 职责单一,正确复用其他模块 data-access
|
||||
|
||||
**文件清单**:
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `data-access.ts` | 234 | 子女关系 + 仪表盘数据聚合 |
|
||||
| `types.ts` | 57 | 类型定义 |
|
||||
| `components/*` | 7 文件 | 子女卡片/详情/仪表盘 |
|
||||
| `data-access.ts` | 227 | 子女关系 + 仪表盘数据聚合 + 关系校验 |
|
||||
| `types.ts` | 67 | 类型定义(含 JSDoc) |
|
||||
| `lib/utils.ts` | 7 | 模块共享工具函数(getInitials) |
|
||||
| `components/*` | 8 文件 | 子女卡片/详情/仪表盘/共享数据页 |
|
||||
|
||||
---
|
||||
|
||||
@@ -987,24 +1031,29 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
|
||||
**导出函数**:
|
||||
- Actions:`getElectiveCoursesAction` / `createElectiveCourseAction` / `updateElectiveCourseAction` / `deleteElectiveCourseAction` / `getStudentSelectionsAction` / `selectCourseAction` / `dropCourseAction` / `runLotteryAction` / `getAvailableCoursesForStudentAction`
|
||||
- Data-access:`getElectiveCourses` / `getElectiveCourseById` / `createElectiveCourse` / `updateElectiveCourse` / `deleteElectiveCourse` / `selectCourse` / `dropCourse` / `runLottery` / `getStudentSelections` / `getAvailableCoursesForStudent`
|
||||
- Data-access:`getElectiveCourses` / `getElectiveCourseById` / `createElectiveCourse` / `updateElectiveCourse` / `deleteElectiveCourse` / `openSelection` / `closeSelection` / `buildCourseSelect` / `mapCourseRow` / `resolveCourseDisplayNames` / `CourseCoreRow`(P3 新增导出,供 data-access-selections 复用)
|
||||
- Data-access-operations:`selectCourse` / `dropCourse` / `runLottery`
|
||||
- Data-access-selections:`getCourseSelections` / `getStudentSelections` / `getStudentGradeId` / `getAvailableCoursesForStudent`
|
||||
|
||||
**依赖关系**:
|
||||
- 依赖:`shared/*`、`@/auth`
|
||||
- 依赖:`shared/*`、`@/auth`、`school`(✅ P3 已修复:通过 school data-access.getSubjectOptions/getGradeOptions 获取科目/年级名称,不再直查 subjects/grades 表)、`users`(✅ P3 已修复:通过 users data-access.getUserNamesByIds 获取教师姓名,不再直查 users 表)、`classes`(通过 classes data-access.getStudentActiveGradeId 获取学生年级)
|
||||
- 被依赖:无
|
||||
|
||||
**已知问题**:
|
||||
- ⚠️ P1:`data-access.ts` 与 `data-access-selections.ts` 重复定义 `mapCourseRow`/`buildCourseSelect`(60 行重复)
|
||||
- ⚠️ P2:`runLottery` 使用 `Math.random()`,结果不可复现
|
||||
- ⚠️ P2:`selectCourse` FCFS 模式存在并发超卖风险
|
||||
- ✅ P1 已修复:~~`buildCourseSelect` 跨模块 join users/subjects/grades 表~~ 改为只查 electiveCourses 表,通过 `resolveCourseDisplayNames` 调用 school/users data-access 获取显示名称
|
||||
- ✅ P1 已修复:~~`getSubjectOptions` 本地直查 subjects 表且与 school 模块重复~~ 删除本地实现,改用 `school/data-access.getSubjectOptions`
|
||||
- ✅ P1 已修复:~~`selectCourse`/`dropCourse` 缺事务包裹~~ 改为 `db.transaction` 包裹,FCFS 模式下使用 `FOR UPDATE` 行锁防止并发超卖
|
||||
- ✅ P2 已修复:~~`mapCourseRow` 在 data-access.ts 与 data-access-selections.ts 重复定义~~ 抽取到 data-access.ts 统一导出,data-access-selections.ts 复用
|
||||
- ✅ P2 已修复:~~`runLottery` 使用 `sort(() => Math.random() - 0.5)` 有偏 shuffle~~ 改为 Fisher-Yates 无偏洗牌算法
|
||||
- ✅ P2 已修复:~~`selectCourse` FCFS 并发超卖风险~~ 使用 `db.transaction` + `.for("update")` 行锁
|
||||
- ✅ 权限校验完整(ELECTIVE_MANAGE/SELECT/READ)
|
||||
|
||||
**文件清单**:
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `actions.ts` | 304 | 11 个 Server Action |
|
||||
| `data-access.ts` | 242 | 课程 CRUD + scope 过滤 |
|
||||
| `data-access-operations.ts` | 217 | 选课操作(select/drop/lottery) |
|
||||
| `data-access.ts` | 250 | 课程 CRUD + scope 过滤 + 共享映射函数(P3 重构:移除跨模块 join,通过 school/users data-access 获取显示名称) |
|
||||
| `data-access-operations.ts` | 245 | 选课操作(select/drop/lottery,P3 重构:事务包裹 + FOR UPDATE 锁 + Fisher-Yates 洗牌) |
|
||||
| `data-access-selections.ts` | 189 | 选课记录查询 |
|
||||
| `schema.ts` | 132 | Zod 校验 |
|
||||
| `types.ts` | 108 | 类型定义 + 标签常量 |
|
||||
@@ -1028,6 +1077,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- ✅ P0-6 已修复:~~事件上报存在 Server Action 与 REST API 双通道重复~~ 删除 `/api/proctoring/event` REST 路由(移至 deletes/),Server Action `recordProctoringEventAction` 为唯一规范路径
|
||||
- ✅ P1-1 已修复:~~跨模块直查 `exams`/`examSubmissions`/`users`~~ 改为通过 exams/users data-access 函数获取数据
|
||||
- ✅ P2 已修复:`actions.ts` 不再直接 import `db` 和 `examSubmissions`,submission 归属校验已下沉到 data-access;`recordProctoringEventAction` 改用 `requirePermission(EXAM_SUBMIT)` 并增加 `revalidatePath`
|
||||
- ✅ P2 已修复:~~`getStudentProctoringStatuses` 串行查询(getUserNamesByIds 后再查事件)~~ 改为 `Promise.all` 并行拉取学生姓名与事件记录
|
||||
|
||||
**文件清单**:
|
||||
| 文件 | 行数 | 职责 |
|
||||
@@ -1057,7 +1107,10 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
|
||||
**已知问题**:
|
||||
- ✅ P1-1 已修复:~~`updateMasteryFromSubmission` 跨模块直查 4 张表(与 exams/homework/questions 紧耦合)~~ 改为调用 `exams/data-access.getExamSubmissionWithAnswers` 和 `questions/data-access.getKnowledgePointsForQuestions`
|
||||
- ⚠️ P2:`data-access-reports.ts` 有未使用代码(`round2`)
|
||||
- ✅ P2 已修复:~~`data-access-reports.ts` 有未使用代码(`round2` + `void round2`)~~ 已删除死代码
|
||||
- ✅ P2 已修复:~~`updateMasteryFromSubmission` 循环内串行 await upsert~~ 改为 `Promise.all` 并行执行所有 upsert
|
||||
- ✅ P2 已修复:~~`getClassMasterySummary` 串行查询(className → studentIds → userMap → masteryRows)~~ 改为两组 `Promise.all` 并行(className+studentIds,userMap+masteryRows)
|
||||
- ✅ P2 已修复:~~`getDiagnosticReports` 中 `conditions` 隐式 `any[]`~~ 改为显式 `SQL[]` 类型标注
|
||||
- ⚠️ P2:班级报告将生成者 ID 存入 `studentId` 字段(schema 设计缺陷 workaround)
|
||||
- ✅ 与 grades 模块无职责重叠(grades 管分数,diagnostic 管知识点掌握度)
|
||||
|
||||
@@ -1081,6 +1134,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- Actions:`getAiProvidersAction` / `createAiProviderAction` / `updateAiProviderAction` / `deleteAiProviderAction` / `testAiProviderAction`
|
||||
- Actions-password:`changePasswordAction`(✅ P1 已修复:使用 `requirePermission(USER_PROFILE_UPDATE)` + Zod 校验 + DB 操作下沉到 data-access)
|
||||
- Data-access:`getAiProviderSummaries` / `countDefaultAiProviders` / `getAiProviderForUpdate` / `updateAiProvider` / `createAiProvider` / `getUserPasswordHash` / `getPasswordSecurityByUserId` / `updateUserPassword` / `upsertPasswordSecurityOnPasswordChange`(P1 新增,从 actions 下沉)
|
||||
- Components:`SettingsView`(P2-a 新增:统一设置页布局,消除 admin/teacher/student 三个设置视图的重复布局;4 标签页 General/Notifications/Appearance/Security,角色差异通过 `description` / `backHref` / `generalExtra` 三个 props 注入;3 个消费方:admin/teacher/student 设置页)
|
||||
- Types:`AiProviderSummary` / `AiProviderName` / `AiProviderExisting`(P1 新增,从 actions.ts 迁出)
|
||||
|
||||
**依赖关系**:
|
||||
@@ -1092,6 +1146,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- ✅ P1 已修复:~~无 `data-access.ts`,`actions.ts` 直接使用 `db`~~ 新建 `data-access.ts`,所有 DB 操作已下沉
|
||||
- ✅ P1 已修复:~~`changePasswordAction` 使用 `requireAuth()` 无 Zod 校验~~ 改为 `requirePermission(USER_PROFILE_UPDATE)` + `ChangePasswordSchema` Zod 校验 + 并行查询优化
|
||||
- ✅ P2 已修复:`actions-password.ts` 删除本地 `normalizeBcryptHash`,统一复用 `shared/lib/bcrypt-utils.normalizeBcryptHash`,消除重复代码
|
||||
- ✅ P2-a 已修复:~~admin/teacher/student 三个设置视图重复布局~~ 新增 `SettingsView` 统一设置页布局(4 标签页 + 角色差异通过 props 注入),3 个设置页改为消费 `SettingsView`
|
||||
- ⚠️ P2:`notification-preferences-form.tsx` 跨模块 UI 依赖
|
||||
- ✅ 密码修改有速率限制
|
||||
- ✅ AI Provider 操作有 `AI_CONFIGURE` 权限校验
|
||||
@@ -1103,6 +1158,7 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| `actions-password.ts` | 107 | 修改密码(P1 已修复:requirePermission + Zod + data-access) |
|
||||
| `data-access.ts` | 175 | AI Provider CRUD + 密码修改 DB 操作(P1 新增) |
|
||||
| `types.ts` | 16 | 类型定义(P1 新增,AiProviderSummary 等) |
|
||||
| `components/settings-view.tsx` | 117 | SettingsView 统一设置页布局(P2-a 新增,4 标签页 + props 注入角色差异) |
|
||||
| `components/*` | 8 文件 | 通用设置 + AI 配置 + 密码 + 主题 + 通知偏好 |
|
||||
|
||||
---
|
||||
@@ -1167,22 +1223,47 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
**已知问题**:
|
||||
- ⚠️ P2:与 classes 模块的 `schedule-view.tsx`/`schedule-filters.tsx` 可能功能重叠
|
||||
- ✅ 纯 UI 模块,数据由页面通过 classes data-access 获取
|
||||
- ✅ 认证模式已统一:所有 student 页面使用 `getCurrentStudentUser()`(users 模块)或 `getAuthContext()`(shared 模块),不再直接调用 `auth()` 或 `getDemoStudentUser()`
|
||||
|
||||
**文件清单**:
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `components/student-courses-view.tsx` | 学生课程视图 |
|
||||
| `components/student-courses-view.tsx` | 学生课程视图(含 `ClassCard` memo 组件 + 加入班级表单,使用 `useTransition`) |
|
||||
| `components/student-schedule-filters.tsx` | 课表筛选器 |
|
||||
| `components/student-schedule-view.tsx` | 学生课表视图 |
|
||||
|
||||
**路由文件清单**(`app/(dashboard)/student/`):
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `dashboard/page.tsx` + `loading.tsx` | 学生仪表盘 + 骨架屏 |
|
||||
| `attendance/page.tsx` + `loading.tsx` | 学生考勤 + 骨架屏 |
|
||||
| `diagnostic/page.tsx` + `loading.tsx` | 学情诊断 + 骨架屏 |
|
||||
| `elective/page.tsx` + `loading.tsx` | 选课中心 + 骨架屏 |
|
||||
| `grades/page.tsx` + `loading.tsx` | 我的成绩 + 骨架屏 |
|
||||
| `learning/assignments/page.tsx` + `loading.tsx` | 作业列表(含 `AssignmentCard` 组件)+ 骨架屏 |
|
||||
| `learning/assignments/[assignmentId]/page.tsx` + `loading.tsx` | 作业作答/复习 + 骨架屏 |
|
||||
| `learning/courses/page.tsx` + `loading.tsx` | 课程列表 + 骨架屏 |
|
||||
| `learning/textbooks/page.tsx` + `loading.tsx` | 教材列表 + 骨架屏 |
|
||||
| `learning/textbooks/[id]/page.tsx` + `loading.tsx` | 教材阅读 + 骨架屏 |
|
||||
| `schedule/page.tsx` + `loading.tsx` | 课表 + 骨架屏 |
|
||||
| `error.tsx` | 路由组错误边界(提供"重试"按钮) |
|
||||
|
||||
---
|
||||
|
||||
## 2.27 lesson-preparation(备课模块)
|
||||
|
||||
**职责**:教师备课,基于教材章节创建课案(Block 编辑器),支持模板、版本管理、知识点标注、题目创建/拉取、作业发布。
|
||||
**职责**:教师备课,基于教材章节创建课案(**节点图编辑器 React Flow**),支持模板、版本管理、知识点标注、题目创建/拉取、作业发布。
|
||||
|
||||
> 架构变更(2026-06-21):编辑器从列表式(BlockRenderer + @dnd-kit)升级为节点图式(NodeEditor + @xyflow/react)。数据结构从 v1(blocks 数组)升级到 v2(nodes + edges 节点图),旧数据通过 `migrateV1ToV2()` 自动迁移。
|
||||
|
||||
**数据结构**:
|
||||
- v1(已废弃,仅向后兼容读取):`{ version: 1, blocks: Block[] }`
|
||||
- v2(当前):`{ version: 2, nodes: LessonPlanNode[]; edges: LessonPlanEdge[] }`
|
||||
- `LessonPlanNode`:`Block` + `position: { x, y }`(画布坐标)
|
||||
- `LessonPlanEdge`:`{ id, source, target, sourceHandle?, targetHandle? }`(节点间连线)
|
||||
|
||||
**导出函数**:
|
||||
- Data-access(`data-access.ts`):`getLessonPlans` / `getLessonPlanById` / `createLessonPlan` / `updateLessonPlanContent` / `softDeleteLessonPlan` / `duplicateLessonPlan` / `getTemplateById` / `buildInitialContent`
|
||||
- Data-access(`data-access.ts`):`getLessonPlans` / `getLessonPlanById` / `createLessonPlan` / `updateLessonPlanContent` / `softDeleteLessonPlan` / `duplicateLessonPlan` / `getTemplateById` / `buildInitialContent` / `migrateV1ToV2`(v1→v2 迁移:blocks 数组转换为 nodes + 线性 edges)/ `normalizeDocument`(规范化:确保 content 为 v2 格式,兼容旧数据)
|
||||
- Data-access-versions(`data-access-versions.ts`):`getLessonPlanVersions` / `createLessonPlanVersion` / `getVersionContent` / `revertToVersion` / `pruneAutoVersions`
|
||||
- Data-access-templates(`data-access-templates.ts`):`getLessonPlanTemplates` / `saveAsTemplate` / `deletePersonalTemplate`
|
||||
- Data-access-knowledge(`data-access-knowledge.ts`):`getLessonPlansByKnowledgePoint` / `getLessonPlansByQuestion`
|
||||
@@ -1191,21 +1272,23 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
- Actions:`getLessonPlansAction` / `getLessonPlanByIdAction` / `createLessonPlanAction` / `updateLessonPlanAction` / `saveLessonPlanVersionAction` / `getLessonPlanVersionsAction` / `revertLessonPlanVersionAction` / `deleteLessonPlanAction` / `duplicateLessonPlanAction` / `getLessonPlanTemplatesAction` / `saveAsTemplateAction` / `deleteTemplateAction` / `suggestKnowledgePointsAction` / `publishLessonPlanHomeworkAction` / `getKnowledgePointOptionsAction`
|
||||
|
||||
**依赖关系**:
|
||||
- 依赖:`shared/*`、`@/auth`、`shared/lib/ai`、`textbooks`(只读章节/知识点树)、`questions`(创建/查询题目)、`exams`(创建 exam 草稿)、`homework`(创建作业下发)、`classes`(查询教师班级)、`files`(附件)
|
||||
- 依赖:`shared/*`、`@/auth`、`shared/lib/ai`、`@xyflow/react`(节点图编辑器)、`textbooks`(只读章节/知识点树)、`questions`(创建/查询题目)、`exams`(创建 exam 草稿)、`homework`(创建作业下发)、`classes`(查询教师班级)、`files`(附件)
|
||||
- 被依赖:无
|
||||
|
||||
**已知问题**:
|
||||
- ✅ 通过对方 data-access 调用跨模块数据,无直查跨模块表
|
||||
- ✅ data-access 按职责拆分为 4 个文件(data-access/data-access-versions/data-access-templates/data-access-knowledge)
|
||||
- ✅ actions 按职责拆分为 4 个文件(actions/actions-publish/actions-ai/actions-kp)
|
||||
- ✅ 编辑器架构升级:NodeEditor(React Flow 画布)+ NodeEditPanel(侧边内容编辑面板)+ LessonNode(自定义节点组件),支持节点拖拽、连线、画布缩放
|
||||
- ⚠️ `block-renderer.tsx` 标记为 @deprecated(已被 NodeEditor 替代,保留用于向后兼容)
|
||||
|
||||
**文件清单**:
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `types.ts` | 类型定义 |
|
||||
| `types.ts` | 类型定义(含 v1/v2 文档类型、LessonPlanNode、LessonPlanEdge) |
|
||||
| `constants.ts` | 常量定义 |
|
||||
| `schema.ts` | Zod 验证 |
|
||||
| `data-access.ts` | 课案 CRUD + 模板查询 + 初始内容构建 |
|
||||
| `data-access.ts` | 课案 CRUD + 模板查询 + 初始内容构建 + v1→v2 迁移(migrateV1ToV2 / normalizeDocument) |
|
||||
| `data-access-versions.ts` | 版本管理(创建/查询/回滚/清理) |
|
||||
| `data-access-templates.ts` | 个人模板 CRUD |
|
||||
| `data-access-knowledge.ts` | 按知识点/题目反查课案 |
|
||||
@@ -1216,22 +1299,25 @@ src/auth.ts ──▶ import { ... } from "@/shared/lib/permissions"
|
||||
| `publish-service.ts` | 发布作业服务(编排 homework/exams/classes) |
|
||||
| `ai-suggest.ts` | AI 知识点建议服务 |
|
||||
| `seed-templates.ts` | 模板种子数据 |
|
||||
| `hooks/use-lesson-plan-editor.ts` | 课案编辑器 Hook |
|
||||
| `hooks/use-lesson-plan-editor.ts` | 课案编辑器 Hook(基于 zustand,支持 nodes/edges 操作:addNode/updateNode/updateNodePosition/removeNode/connect/disconnect/setEdges/selectNode) |
|
||||
| `components/lesson-plan-list.tsx` | 课案列表 |
|
||||
| `components/lesson-plan-card.tsx` | 课案卡片 |
|
||||
| `components/lesson-plan-filters.tsx` | 课案筛选器 |
|
||||
| `components/lesson-plan-editor.tsx` | 课案编辑器 |
|
||||
| `components/block-renderer.tsx` | Block 渲染器 |
|
||||
| `components/lesson-plan-editor.tsx` | 课案编辑器(编排 NodeEditor + NodeEditPanel) |
|
||||
| `components/node-editor.tsx` | **节点图画布**(React Flow,自定义 LessonNode,支持拖拽/连线/缩放) |
|
||||
| `components/node-edit-panel.tsx` | **侧边内容编辑面板**(选中节点后编辑标题/数据) |
|
||||
| `components/nodes/lesson-node.tsx` | **自定义节点组件**(按 BlockType 显示图标/颜色,含 Handle 连接点) |
|
||||
| `components/block-renderer.tsx` | ⚠️ @deprecated Block 渲染器(已被 NodeEditor 替代,保留向后兼容) |
|
||||
| `components/template-picker.tsx` | 模板选择器 |
|
||||
| `components/version-history-drawer.tsx` | 版本历史抽屉 |
|
||||
| `components/knowledge-point-picker.tsx` | 知识点选择器 |
|
||||
| `components/question-bank-picker.tsx` | 题库选择器 |
|
||||
| `components/inline-question-editor.tsx` | 内联题目编辑器 |
|
||||
| `components/publish-homework-dialog.tsx` | 发布作业对话框 |
|
||||
| `components/blocks/rich-text-block.tsx` | 富文本 Block |
|
||||
| `components/blocks/text-study-block.tsx` | 课文研读 Block |
|
||||
| `components/blocks/exercise-block.tsx` | 练习 Block |
|
||||
| `components/blocks/reflection-block.tsx` | 反思 Block |
|
||||
| `components/blocks/rich-text-block.tsx` | 富文本 Block(被 NodeEditPanel 复用) |
|
||||
| `components/blocks/text-study-block.tsx` | 课文研读 Block(被 NodeEditPanel 复用) |
|
||||
| `components/blocks/exercise-block.tsx` | 练习 Block(被 NodeEditPanel 复用) |
|
||||
| `components/blocks/reflection-block.tsx` | 反思 Block(被 NodeEditPanel 复用) |
|
||||
|
||||
---
|
||||
|
||||
@@ -1417,9 +1503,9 @@ shared/lib/{audit-logger, change-logger, auth-guard} → @/auth → shared/lib/*
|
||||
| P2-10 | school 模块审计日志不一致(仅 school 实体记录) | school |
|
||||
| ~~P2-11~~ | ~~`announcements` 死代码 `void wasPublished`~~ ✅ 已修复(代码中已不存在) | announcements |
|
||||
| ~~P2-12~~ | ~~`announcements` 权限模式不一致(requireAuth vs requirePermission)~~ ✅ 已修复 | announcements |
|
||||
| P2-13 | `files` try-catch 吞错误 | files |
|
||||
| P2-14 | `elective` runLottery 使用 Math.random | elective |
|
||||
| P2-15 | `elective` selectCourse FCFS 并发超卖风险 | elective |
|
||||
| P2-13 | ~~`files` try-catch 吞错误~~ ✅ 已修复(所有 catch 块已添加 console.error;conditions 隐式 any[] 改为 SQL[]) | files |
|
||||
| ~~P2-14~~ | ~~`elective` runLottery 使用 Math.random~~ ✅ 已修复(改为 Fisher-Yates 无偏洗牌) | elective |
|
||||
| ~~P2-15~~ | ~~`elective` selectCourse FCFS 并发超卖风险~~ ✅ 已修复(db.transaction + FOR UPDATE 行锁) | elective |
|
||||
| P2-16 | `diagnostic` 班级报告 studentId 字段复用 | diagnostic |
|
||||
| ~~P2-17~~ | ~~`layout` 用权限反推角色~~ ✅ 已修复(`app-sidebar.tsx` 改用 `hasRole()` 判断角色) | layout |
|
||||
| ~~P2-18~~ | ~~`scheduling/actions.ts` 末尾 re-export data-access~~ ✅ 已修复(移除 re-export,4 个页面改为从 `data-access` 导入) | scheduling |
|
||||
@@ -1491,7 +1577,7 @@ shared/lib/{audit-logger, change-logger, auth-guard} → @/auth → shared/lib/*
|
||||
| **grades** | ✅ | ✅ | ✅外键 | ✅外键 | - | - | ✅data-access | ✅data-access | - | ✅data-access | - | - | - | - | - |
|
||||
| **dashboard** | ✅ | ✅ | ✅data-access | ✅data-access | ✅data-access | ✅data-access | ✅data-access | - | - | ✅data-access | - | - | - | - | - |
|
||||
| **users** | ✅ | ✅ | - | - | - | - | ✅data-access | - | - | - | - | - | - | - | - |
|
||||
| **messaging** | ✅ | ✅ | - | - | - | - | - | - | - | - | - | - | ✅dispatcher | - | - |
|
||||
| **messaging** | ✅ | ✅ | - | - | - | - | ✅data-access | - | - | - | - | - | ✅dispatcher | - | - |
|
||||
| **notifications** | ✅ | ✅ | - | - | - | - | ✅data-access | - | - | - | - | - | - | - | - |
|
||||
| **attendance** | ✅ | ✅ | - | - | - | - | ✅data-access | - | - | - | - | - | - | - | - |
|
||||
| **scheduling** | ✅ | ✅ | - | - | - | - | ✅data-access | - | - | ✅data-access | - | - | - | - | - |
|
||||
@@ -1538,7 +1624,7 @@ shared/lib/{audit-logger, change-logger, auth-guard} → @/auth → shared/lib/*
|
||||
6. 在 `auth-guard.ts` 中通过 `classSubjectTeachers` 查询教师关联的 classIds,构建 `DataScope.class_taught`
|
||||
|
||||
### `permission`
|
||||
1. 由 `shared/types/permissions.ts` 的 `Permissions` 常量定义(54 个权限点)
|
||||
1. 由 `shared/types/permissions.ts` 的 `Permissions` 常量定义(61 个权限点)
|
||||
2. 在 `shared/lib/permissions.ts` 中通过 `ROLE_PERMISSIONS` 映射角色到权限列表
|
||||
3. 在 `auth.ts` JWT callback 中通过 `resolvePermissions(roleNames)` 合并多角色权限,存入 JWT
|
||||
4. 在 `proxy.ts` middleware 中通过 `token.permissions` 检查路由访问权限
|
||||
@@ -1620,6 +1706,11 @@ formatFileSize(bytes: number): string
|
||||
// shared/lib/utils.ts
|
||||
cn(...inputs: ClassValue[]): string
|
||||
formatDate(date: string | Date, locale?: string): string
|
||||
getSearchParam(params: SearchParams, key: string): string | undefined
|
||||
formatNumber(v: number | null | undefined, digits?: number): string
|
||||
|
||||
// shared/lib/search-params.ts (re-export from utils.ts)
|
||||
getParam(params: SearchParams, key: string): string | undefined // = getSearchParam
|
||||
```
|
||||
|
||||
### 业务模块核心 Actions
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"generatedAt": "2026-06-17",
|
||||
"formatVersion": "1.1",
|
||||
"rule": "每次文件修改后须同步更新本文件",
|
||||
"lastUpdate": "P2-6/P2-7/P2-8/P2-11/P2-17/P2-18 已修复:proxy.ts 改用 Permissions 常量替代硬编码字符串;useA11yId 文件已不存在(use-aria-live.ts 已在 hooks/ 目录);schema.ts 分节编号重新编号为连续 1-24,消除 8b/14b/乱序问题;announcements 死代码 void wasPublished 已不存在;layout/app-sidebar.tsx 改用 hasRole() 判断角色,不再用权限反推角色;scheduling/actions.ts 移除末尾 re-export data-access,4 个页面改为从 data-access 直接导入;P1-1 已修复:所有跨模块直查已改为通过对方 data-access 接口(homework/grades/parent/diagnostic/elective/proctoring/notifications/scheduling/classes 模块);P0-2 已修复:shared/lib ↔ auth 循环依赖已解决,新增 shared/lib/session.ts 单一入口;P1-6 已修复:http-utils.ts 统一 IP/UA 提取;P0-1/P0-2/P0-3/P0-4/P0-5/P0-7/P0-8 已修复;P1-2 已修复:actions 层 DB 操作下沉到 data-access;P2-2/P2-3/P2-12/P2-20 已修复"
|
||||
"lastUpdate": "P0-b/P1-a/P1-b/P1-c/P2-a/P2-b/P3-a/P3-b/P3-c/P3-d 共享组件抽取重构已同步:新增 shared 层 UI 组件(StatCard/StatItem/ChipNav/PageHeader/FilterBar+FilterSearchInput+FilterResetButton)、图表组件(ChartCardShell/TrendLineChart/SimpleBarChart/ComparisonRadarChart)、课表组件(ScheduleList+ScheduleListItem)、题库组件(QuestionBankFilters)、工具函数(downloadBase64File/downloadBlob/getInitials/formatDateForFile);新增 settings 模块 SettingsView 组件;删除 messaging/notification-preferences.ts(P0-b 通知模块去重,re-export shim 已移除,消费方改为直接从 notifications/preferences 导入);P2-6/P2-7/P2-8/P2-11/P2-17/P2-18 已修复:proxy.ts 改用 Permissions 常量替代硬编码字符串;useA11yId 文件已不存在(use-aria-live.ts 已在 hooks/ 目录);schema.ts 分节编号重新编号为连续 1-24,消除 8b/14b/乱序问题;announcements 死代码 void wasPublished 已不存在;layout/app-sidebar.tsx 改用 hasRole() 判断角色,不再用权限反推角色;scheduling/actions.ts 移除末尾 re-export data-access,4 个页面改为从 data-access 直接导入;P1-1 已修复:所有跨模块直查已改为通过对方 data-access 接口(homework/grades/parent/diagnostic/elective/proctoring/notifications/scheduling/classes 模块);P0-2 已修复:shared/lib ↔ auth 循环依赖已解决,新增 shared/lib/session.ts 单一入口;P1-6 已修复:http-utils.ts 统一 IP/UA 提取;P0-1/P0-2/P0-3/P0-4/P0-5/P0-7/P0-8 已修复;P1-2 已修复:actions 层 DB 操作下沉到 data-access;P2-2/P2-3/P2-12/P2-20 已修复"
|
||||
},
|
||||
"architectureOverview": {
|
||||
"layers": [
|
||||
@@ -231,6 +231,7 @@
|
||||
"FILE_READ",
|
||||
"COURSE_PLAN_READ",
|
||||
"ATTENDANCE_READ",
|
||||
"MESSAGE_SEND",
|
||||
"MESSAGE_READ",
|
||||
"MESSAGE_DELETE",
|
||||
"DIAGNOSTIC_READ",
|
||||
@@ -360,6 +361,37 @@
|
||||
"textbooks"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "getParam",
|
||||
"file": "lib/search-params.ts",
|
||||
"signature": "getParam(params: SearchParams, key: string): string | undefined",
|
||||
"params": {
|
||||
"params": "Next.js searchParams 对象",
|
||||
"key": "参数键名"
|
||||
},
|
||||
"purpose": "规范化 Next.js 15+ searchParams 访问(string | string[] | undefined → string | undefined),re-export 自 utils.ts 的 getSearchParam",
|
||||
"deps": [
|
||||
"shared/lib/utils.getSearchParam"
|
||||
],
|
||||
"usedBy": [
|
||||
"teacher/attendance/page.tsx",
|
||||
"teacher/attendance/sheet/page.tsx",
|
||||
"teacher/attendance/stats/page.tsx",
|
||||
"teacher/classes/schedule/page.tsx",
|
||||
"teacher/classes/students/page.tsx",
|
||||
"teacher/course-plans/page.tsx",
|
||||
"teacher/diagnostic/page.tsx",
|
||||
"teacher/elective/page.tsx",
|
||||
"teacher/exams/all/page.tsx",
|
||||
"teacher/grades/page.tsx",
|
||||
"teacher/grades/analytics/page.tsx",
|
||||
"teacher/grades/entry/page.tsx",
|
||||
"teacher/grades/stats/page.tsx",
|
||||
"teacher/homework/assignments/page.tsx",
|
||||
"teacher/questions/page.tsx",
|
||||
"teacher/textbooks/page.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "parseAiChatPayload",
|
||||
"file": "lib/ai/payload-parser.ts",
|
||||
@@ -795,6 +827,55 @@
|
||||
"usedBy": [
|
||||
"待扩展"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "getInitials",
|
||||
"file": "lib/utils.ts",
|
||||
"signature": "getInitials(name: string | null | undefined): string",
|
||||
"purpose": "从用户姓名提取首字母缩写(最多2字符,用于头像 fallback)",
|
||||
"deps": [],
|
||||
"usedBy": [
|
||||
"shared/components/ui/avatar.tsx",
|
||||
"modules/dashboard/components/*-dashboard/*-header.tsx",
|
||||
"modules/parent/components/child-card.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "formatDateForFile",
|
||||
"file": "lib/utils.ts",
|
||||
"signature": "formatDateForFile(d?: Date): string",
|
||||
"purpose": "格式化日期为 YYYY-MM-DD 用于文件名(P1-c/P2-c 重构:从 grades/export.ts、audit/actions.ts、api/export/route.ts、users/actions.ts 四处重复实现抽取)",
|
||||
"deps": [],
|
||||
"usedBy": [
|
||||
"grades/actions.exportGradesAction",
|
||||
"audit/actions.exportAuditLogsAction",
|
||||
"api/export/route",
|
||||
"users/actions.exportUsersAction"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "downloadBase64File",
|
||||
"file": "lib/download.ts",
|
||||
"signature": "downloadBase64File(base64: string, filename: string, mimeType?: string): void",
|
||||
"purpose": "客户端下载 Base64 编码文件(默认 MIME 为 Excel xlsx),P1-c 重构从 grades/export-button、users/user-import-dialog 两处重复实现抽取",
|
||||
"deps": [
|
||||
"shared/lib/download.downloadBlob"
|
||||
],
|
||||
"usedBy": [
|
||||
"grades/components/export-button.tsx",
|
||||
"users/components/user-import-dialog.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "downloadBlob",
|
||||
"file": "lib/download.ts",
|
||||
"signature": "downloadBlob(blob: Blob, filename: string): void",
|
||||
"purpose": "客户端下载 Blob 对象(创建临时 URL + a 标签点击 + revoke),P1-c 重构从 audit/audit-log-export-button 抽取",
|
||||
"deps": [],
|
||||
"usedBy": [
|
||||
"shared/lib/download.downloadBase64File",
|
||||
"audit/components/audit-log-export-button.tsx"
|
||||
]
|
||||
}
|
||||
],
|
||||
"hooks": [
|
||||
@@ -940,6 +1021,261 @@
|
||||
"usedBy": [
|
||||
"settings/components/notification-preferences-form.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "StatCard",
|
||||
"file": "components/ui/stat-card.tsx",
|
||||
"props": "{ title, value, icon?, description?, color?, highlight?, href?, isLoading?, valueClassName? }",
|
||||
"purpose": "统计卡片(标题+数值+图标+描述+跳转+骨架屏),P1-a 重构从 8 处重复实现抽取(teacher/student/admin dashboard stats、class-overview-stats、grade insights 等)",
|
||||
"internalDeps": [
|
||||
"Card",
|
||||
"CardHeader",
|
||||
"CardTitle",
|
||||
"CardContent",
|
||||
"Link",
|
||||
"cn",
|
||||
"Skeleton"
|
||||
],
|
||||
"usedBy": [
|
||||
"dashboard/components/teacher-dashboard/teacher-stats.tsx",
|
||||
"dashboard/components/student-dashboard/student-stats-grid.tsx",
|
||||
"dashboard/components/admin-dashboard/admin-dashboard.tsx",
|
||||
"classes/components/class-detail/class-overview-stats.tsx",
|
||||
"app/(dashboard)/admin/school/grades/insights/page.tsx",
|
||||
"app/(dashboard)/management/grade/insights/page.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "StatItem",
|
||||
"file": "components/ui/stat-item.tsx",
|
||||
"props": "{ label, value, icon?, hint?, valueClassName? }",
|
||||
"purpose": "紧凑统计项(label+icon+value+hint,用于统计面板网格),P1-a 重构从 attendance-stats-card、grade-stats-card 两处重复实现抽取",
|
||||
"internalDeps": [
|
||||
"cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"attendance/components/attendance-stats-card.tsx",
|
||||
"grades/components/grade-stats-card.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ChipNav",
|
||||
"file": "components/ui/chip-nav.tsx",
|
||||
"props": "{ options: ChipNavOption[], currentId, buildHref: (id) => string, size?: 'sm'|'xs', allOption?, className? }",
|
||||
"purpose": "芯片导航组(通过 URL search params 切换筛选维度,Link 跳转),P1-b 重构从 stats-class-selector、attendance-stats-class-selector、analytics-filters 三处重复实现抽取",
|
||||
"internalDeps": [
|
||||
"Link",
|
||||
"cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"grades/components/stats-class-selector.tsx",
|
||||
"attendance/components/attendance-stats-class-selector.tsx",
|
||||
"grades/components/analytics-filters.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PageHeader",
|
||||
"file": "components/ui/page-header.tsx",
|
||||
"props": "{ title, description?, icon?: ComponentType, actions?: ReactNode, className? }",
|
||||
"purpose": "页面头部(标题+描述+icon+actions,响应式布局:移动端纵向,桌面端横向),P2-b 重构从 admin-dashboard、profile/page、settings/security/page 三处内联头部抽取",
|
||||
"internalDeps": [
|
||||
"cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"dashboard/components/admin-dashboard/admin-dashboard.tsx",
|
||||
"app/(dashboard)/profile/page.tsx",
|
||||
"app/(dashboard)/settings/security/page.tsx",
|
||||
"modules/settings/components/settings-view.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FilterBar",
|
||||
"file": "components/ui/filter-bar.tsx",
|
||||
"props": "{ children, hasFilters?, onReset?, layout?: 'default'|'wrap'|'between', gapClassName?, className?, resetClassName? }",
|
||||
"purpose": "筛选栏容器(统一布局壳 + Reset 按钮),P3-b 重构从 exam/textbook/question/audit-log/login-log filters 五处重复布局抽取。URL 状态管理方式(nuqs/router/callback)由各模块自行处理",
|
||||
"internalDeps": [
|
||||
"Button",
|
||||
"cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"exams/components/exam-filters.tsx",
|
||||
"textbooks/components/textbook-filters.tsx",
|
||||
"questions/components/question-filters.tsx",
|
||||
"audit/components/audit-log-filters.tsx",
|
||||
"audit/components/login-log-filters.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FilterSearchInput",
|
||||
"file": "components/ui/filter-bar.tsx",
|
||||
"props": "{ value, onChange, placeholder?, className?, inputClassName? }",
|
||||
"purpose": "筛选栏搜索框(带 Search 图标的 Input),P3-b 重构从 exam/textbook/question filters 三处重复搜索框抽取",
|
||||
"internalDeps": [
|
||||
"Input",
|
||||
"Search (lucide-react)",
|
||||
"cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"exams/components/exam-filters.tsx",
|
||||
"textbooks/components/textbook-filters.tsx",
|
||||
"questions/components/question-filters.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FilterResetButton",
|
||||
"file": "components/ui/filter-bar.tsx",
|
||||
"props": "{ onClick, className? }",
|
||||
"purpose": "筛选栏重置按钮(Reset + X 图标),P3-b 重构从 6 个 filter 文件中重复的 Reset 按钮抽取",
|
||||
"internalDeps": [
|
||||
"Button",
|
||||
"X (lucide-react)",
|
||||
"cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"FilterBar(内部使用)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ChartCardShell",
|
||||
"file": "components/charts/chart-card-shell.tsx",
|
||||
"props": "{ title, description?, icon?, iconClassName?, titleClassName?, isEmpty?, emptyTitle?, emptyDescription?, emptyIcon?, emptyClassName?, children, className?, contentClassName? }",
|
||||
"purpose": "图表卡片外壳(Card + CardHeader + EmptyState + CardContent 统一结构),P3-c 重构从 8 个图表文件重复的 Card 包装抽取",
|
||||
"internalDeps": [
|
||||
"Card",
|
||||
"CardHeader",
|
||||
"CardTitle",
|
||||
"CardDescription",
|
||||
"CardContent",
|
||||
"EmptyState",
|
||||
"cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"grades/components/grade-trend-chart.tsx",
|
||||
"grades/components/grade-distribution-chart.tsx",
|
||||
"grades/components/class-comparison-chart.tsx",
|
||||
"grades/components/subject-comparison-chart.tsx",
|
||||
"dashboard/components/teacher-dashboard/teacher-grade-trends.tsx",
|
||||
"dashboard/components/student-dashboard/student-grades-card.tsx",
|
||||
"parent/components/child-grade-summary.tsx",
|
||||
"diagnostic/components/mastery-radar-chart.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "TrendLineChart",
|
||||
"file": "components/charts/trend-line-chart.tsx",
|
||||
"props": "{ data, series: TrendLineSeries[], xKey?, yDomain?, yTickFormatter?, xTickFormatter?, heightClassName?, margin?, yWidth?, tooltipClassName?, tooltipLabelKey?, className? }",
|
||||
"purpose": "趋势折线图(LineChart 统一配置:CartesianGrid + XAxis + YAxis + ChartTooltip + Line),P3-c 重构从 4 个 LineChart 文件(grade-trend-chart、teacher-grade-trends、student-grades-card、child-grade-summary)几乎逐行相同的配置抽取",
|
||||
"internalDeps": [
|
||||
"ChartContainer",
|
||||
"ChartTooltip",
|
||||
"ChartTooltipContent",
|
||||
"CartesianGrid",
|
||||
"Line",
|
||||
"LineChart",
|
||||
"XAxis",
|
||||
"YAxis",
|
||||
"cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"grades/components/grade-trend-chart.tsx",
|
||||
"dashboard/components/teacher-dashboard/teacher-grade-trends.tsx",
|
||||
"dashboard/components/student-dashboard/student-grades-card.tsx",
|
||||
"parent/components/child-grade-summary.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SimpleBarChart",
|
||||
"file": "components/charts/simple-bar-chart.tsx",
|
||||
"props": "{ data, bars: BarSeries[], xKey, yDomain?, yAllowDecimals?, yTickFormatter?, xTickFormatter?, xTruncateLength?, yWidth?, heightClassName?, margin?, showLegend?, tooltipClassName?, tooltipFormatter?, cellColors?, className? }",
|
||||
"purpose": "柱状图(BarChart 统一配置:CartesianGrid + XAxis + YAxis + ChartTooltip + Bar),P3-c 重构从 grade-distribution-chart(单 Bar + Cell 分桶着色)和 class-comparison-chart(多 Bar + Legend)抽取",
|
||||
"internalDeps": [
|
||||
"ChartContainer",
|
||||
"ChartTooltip",
|
||||
"ChartTooltipContent",
|
||||
"Bar",
|
||||
"BarChart",
|
||||
"CartesianGrid",
|
||||
"Legend",
|
||||
"XAxis",
|
||||
"YAxis",
|
||||
"Cell",
|
||||
"cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"grades/components/grade-distribution-chart.tsx",
|
||||
"grades/components/class-comparison-chart.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ComparisonRadarChart",
|
||||
"file": "components/charts/comparison-radar-chart.tsx",
|
||||
"props": "{ data, series: RadarSeries[], angleKey, angleTickFormatter?, angleTickFontSize?, domain?, tickCount?, showLegend?, heightClassName?, tooltipClassName?, className?, gridStrokeDasharray?, gridStrokeOpacity? }",
|
||||
"purpose": "对比雷达图(RadarChart 统一配置:PolarGrid + PolarAngleAxis + PolarRadiusAxis + ChartTooltip + Radar),P3-c 重构从 subject-comparison-chart(双 Radar:averageScore + passRate)和 mastery-radar-chart(双 Radar:student + classAverage,含条件 Legend)抽取",
|
||||
"internalDeps": [
|
||||
"ChartContainer",
|
||||
"ChartTooltip",
|
||||
"ChartTooltipContent",
|
||||
"PolarAngleAxis",
|
||||
"PolarGrid",
|
||||
"PolarRadiusAxis",
|
||||
"Radar",
|
||||
"RadarChart",
|
||||
"Legend",
|
||||
"cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"grades/components/subject-comparison-chart.tsx",
|
||||
"diagnostic/components/mastery-radar-chart.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ScheduleList",
|
||||
"file": "components/schedule/schedule-list.tsx",
|
||||
"props": "{ items: ScheduleListItemData[], variant?: 'separator'|'card', spacingClassName?, renderTrailing?, className? }",
|
||||
"purpose": "课表列表(课程+时间+地点+班级徽章),P3-a 重构从 student-today-schedule-card、child-schedule-card、student-schedule-view 三处逐行复制的列表项渲染抽取。支持 separator(分隔线)和 card(卡片)两种变体",
|
||||
"internalDeps": [
|
||||
"ScheduleListItem",
|
||||
"cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"dashboard/components/student-dashboard/student-today-schedule-card.tsx",
|
||||
"parent/components/child-schedule-card.tsx",
|
||||
"student/components/student-schedule-view.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ScheduleListItem",
|
||||
"file": "components/schedule/schedule-list.tsx",
|
||||
"props": "{ item: ScheduleListItemData, variant?: 'separator'|'card', trailing?, className? }",
|
||||
"purpose": "课表列表项(单条课程渲染:course + Clock + MapPin + Badge),P3-a 重构从 3 个课表文件中重复的列表项抽取",
|
||||
"internalDeps": [
|
||||
"Badge",
|
||||
"Clock (lucide-react)",
|
||||
"MapPin (lucide-react)",
|
||||
"cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"ScheduleList(内部使用)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "QuestionBankFilters",
|
||||
"file": "components/question/question-bank-filters.tsx",
|
||||
"props": "{ search, onSearchChange, type, onTypeChange, difficulty, onDifficultyChange, layout?: 'default'|'compact', className? }",
|
||||
"purpose": "题库筛选栏(搜索+题型+难度),P3-d 重构从 exam-assembly(compact 布局)和 question-bank-picker(default 布局,同时将原生 HTML input/select 迁移到 shadcn Input/Select)两处重复筛选栏抽取。状态管理方式由调用方自行处理",
|
||||
"internalDeps": [
|
||||
"Select",
|
||||
"SelectContent",
|
||||
"SelectItem",
|
||||
"SelectTrigger",
|
||||
"SelectValue",
|
||||
"FilterSearchInput",
|
||||
"cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"exams/components/exam-assembly.tsx",
|
||||
"lesson-preparation/components/question-bank-picker.tsx"
|
||||
]
|
||||
}
|
||||
],
|
||||
"constants": [
|
||||
@@ -1029,10 +1365,22 @@
|
||||
"所有actions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Role",
|
||||
"file": "types/permissions.ts",
|
||||
"definition": "Role = 'admin' | 'teacher' | 'student' | 'parent' | 'grade_head' | 'teaching_head'",
|
||||
"usedBy": [
|
||||
"auth-guard",
|
||||
"permissions",
|
||||
"proxy",
|
||||
"next-auth.d.ts",
|
||||
"use-permission"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DataScope",
|
||||
"file": "types/permissions.ts",
|
||||
"definition": "DataScope = { type: 'all' } | { type: 'owned'; userId: string } | { type: 'class_taught'; classIds: string[]; subjectIds?: string[] } | { type: 'grade_managed'; gradeIds: string[] } | { type: 'class_members' } | { type: 'children'; childrenIds: string[] }",
|
||||
"definition": "DataScope = { type: 'all' } | { type: 'owned'; userId: string } | { type: 'class_members'; classIds: string[] } | { type: 'grade_managed'; gradeIds: string[] } | { type: 'class_taught'; classIds: string[]; subjectIds?: string[] } | { type: 'children'; childrenIds: string[] }",
|
||||
"usedBy": [
|
||||
"auth-guard",
|
||||
"exams/data-access",
|
||||
@@ -1045,7 +1393,7 @@
|
||||
{
|
||||
"name": "AuthContext",
|
||||
"file": "types/permissions.ts",
|
||||
"definition": "AuthContext = { userId: string; roles: string[]; permissions: Permission[]; dataScope: DataScope }",
|
||||
"definition": "AuthContext = { userId: string; roles: Role[]; permissions: Permission[]; dataScope: DataScope }",
|
||||
"usedBy": [
|
||||
"auth-guard",
|
||||
"所有调用requirePermission的Server Action"
|
||||
@@ -2676,6 +3024,22 @@
|
||||
"exam-form.tsx"
|
||||
]
|
||||
}
|
||||
],
|
||||
"utils": [
|
||||
{
|
||||
"name": "normalizeStructure",
|
||||
"file": "utils/normalize-structure.ts",
|
||||
"type": "function",
|
||||
"signature": "(nodes: unknown) => ExamNode[]",
|
||||
"purpose": "将持久化的 exam.structure(unknown JSON)运行时校验并归一化为类型安全的 ExamNode[](类型守卫模式,无 as 断言;递归处理 group children;保证 id 唯一)",
|
||||
"deps": [
|
||||
"@paralleldrive/cuid2.createId",
|
||||
"exams/components/assembly/selected-question-list.ExamNode"
|
||||
],
|
||||
"usedBy": [
|
||||
"teacher/exams/[id]/build/page.tsx"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -4260,6 +4624,14 @@
|
||||
"usedBy": [
|
||||
"grades/data-access-analytics"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "getTeacherIdsByClassIds",
|
||||
"signature": "(classIds: string[]) => Promise<string[]>",
|
||||
"purpose": "获取多个班级的所有教师 ID(班主任 + 任课教师,跨模块接口)",
|
||||
"usedBy": [
|
||||
"messaging/data-access.getRecipients"
|
||||
]
|
||||
}
|
||||
],
|
||||
"schema": [
|
||||
@@ -5369,8 +5741,8 @@
|
||||
{
|
||||
"name": "getAiProviderSummaries",
|
||||
"permission": "AI_CONFIGURE",
|
||||
"signature": "() => Promise<AiProviderSummary[]>",
|
||||
"purpose": "获取AI Provider列表(P1 已修复:DB 操作下沉到 data-access.getAiProviderSummaries)",
|
||||
"signature": "() => Promise<ActionState<AiProviderSummary[]>>",
|
||||
"purpose": "获取AI Provider列表(P1 已修复:DB 操作下沉到 data-access.getAiProviderSummaries;v3 已修复:返回值统一为 ActionState)",
|
||||
"deps": [
|
||||
"data-access.getAiProviderSummaries"
|
||||
]
|
||||
@@ -6499,13 +6871,14 @@
|
||||
"name": "getFileAttachmentsWithFilters",
|
||||
"signature": "(params: FileAttachmentQueryParams) => Promise<FileAttachment[]>",
|
||||
"file": "data-access.ts",
|
||||
"purpose": "按 mimeType(精确或前缀匹配)与 search(originalName/filename 模糊匹配)筛选文件列表,支持 limit/offset 分页",
|
||||
"purpose": "按 mimeType(精确或前缀匹配)与 search(originalName/filename 模糊匹配)筛选文件列表,支持 limit/offset 分页(v3 修复:conditions 显式标注 SQL[] 类型,消除隐式 any[])",
|
||||
"deps": [
|
||||
"shared.db",
|
||||
"shared.db.schema.fileAttachments",
|
||||
"drizzle-orm.like",
|
||||
"drizzle-orm.or",
|
||||
"drizzle-orm.and"
|
||||
"drizzle-orm.and",
|
||||
"drizzle-orm.SQL"
|
||||
],
|
||||
"usedBy": [
|
||||
"app/(dashboard)/admin/files/page.tsx"
|
||||
@@ -7234,11 +7607,12 @@
|
||||
"name": "formatDateForFile",
|
||||
"signature": "(d?: Date) => string",
|
||||
"file": "export.ts",
|
||||
"purpose": "格式化日期为 YYYY-MM-DD 用于文件名",
|
||||
"deps": [],
|
||||
"usedBy": [
|
||||
"actions.exportGradesAction"
|
||||
]
|
||||
"purpose": "⚠️ P1-c/P2-c 已迁移:本地实现已删除,改为从 @/shared/lib/utils 导入。此条目保留仅作历史记录",
|
||||
"deps": [
|
||||
"shared/lib/utils.formatDateForFile"
|
||||
],
|
||||
"usedBy": [],
|
||||
"migratedTo": "shared/lib/utils.formatDateForFile"
|
||||
}
|
||||
],
|
||||
"components": [
|
||||
@@ -7321,6 +7695,30 @@
|
||||
"recharts",
|
||||
"shared/components/ui/chart"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AnalyticsFilters",
|
||||
"file": "components/analytics-filters.tsx",
|
||||
"purpose": "成绩分析页筛选器(班级、科目、年级 Link 筛选按钮组,含 focus-visible 焦点样式)",
|
||||
"deps": [
|
||||
"next/link",
|
||||
"shared/lib/utils.cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"teacher/grades/analytics/page.tsx"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "StatsClassSelector",
|
||||
"file": "components/stats-class-selector.tsx",
|
||||
"purpose": "统计页班级+科目筛选器(Link 筛选按钮组,含 focus-visible 焦点样式)",
|
||||
"deps": [
|
||||
"next/link",
|
||||
"shared/lib/utils.cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"teacher/grades/stats/page.tsx"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8232,12 +8630,16 @@
|
||||
"name": "getRecipients",
|
||||
"signature": "(ctx: AuthContext) => Promise<RecipientOption[]>",
|
||||
"file": "data-access.ts",
|
||||
"purpose": "按 DataScope 过滤可发送对象列表:class_taught(教师→学生)、grade_managed(年级管理员→教师/学生)、all(管理员)、class_members(学生→自己班级的任课教师/班主任)、children(家长→孩子的班主任/任课教师)",
|
||||
"deps": [
|
||||
"shared.db",
|
||||
"shared.db.schema.users",
|
||||
"shared.db.schema.classEnrollments",
|
||||
"shared.db.schema.classes",
|
||||
"shared.db.schema.grades"
|
||||
"shared.db.schema.grades",
|
||||
"classes.data-access.getTeacherIdsByClassIds",
|
||||
"classes.data-access.getStudentActiveClassId",
|
||||
"users.data-access.getUserNamesByIds"
|
||||
],
|
||||
"usedBy": [
|
||||
"getRecipientsAction",
|
||||
@@ -9307,6 +9709,18 @@
|
||||
"name": "AttendanceRulesForm",
|
||||
"file": "components/attendance-rules-form.tsx",
|
||||
"purpose": "考勤规则配置表单(班级选择器、迟到/早退阈值、自动标记勾选)"
|
||||
},
|
||||
{
|
||||
"name": "AttendanceStatsClassSelector",
|
||||
"file": "components/attendance-stats-class-selector.tsx",
|
||||
"purpose": "考勤统计页班级筛选器(Link 筛选按钮组,含 focus-visible 焦点样式)",
|
||||
"deps": [
|
||||
"next/link",
|
||||
"shared/lib/utils.cn"
|
||||
],
|
||||
"usedBy": [
|
||||
"teacher/attendance/stats/page.tsx"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9961,7 +10375,7 @@
|
||||
{
|
||||
"name": "getStudentProctoringStatuses",
|
||||
"signature": "(examId: string) => Promise<StudentProctoringStatus[]>",
|
||||
"purpose": "获取所有学生监考状态",
|
||||
"purpose": "获取所有学生监考状态(v3 优化:Promise.all 并行执行 getUserNamesByIds 与事件聚合查询)",
|
||||
"usedBy": [
|
||||
"actions.getProctoringDashboardAction",
|
||||
"teacher/exams/[id]/proctoring/page.tsx"
|
||||
@@ -10102,7 +10516,7 @@
|
||||
"name": "updateMasteryFromSubmission",
|
||||
"signature": "(submissionId: string) => Promise<void>",
|
||||
"file": "data-access.ts",
|
||||
"purpose": "从提交答案更新掌握度(按知识点聚合正确率,onDuplicateKeyUpdate upsert)",
|
||||
"purpose": "从提交答案更新掌握度(按知识点聚合正确率,onDuplicateKeyUpdate upsert;v3 优化:Promise.all 并行执行多个知识点 upsert)",
|
||||
"deps": [
|
||||
"shared.db",
|
||||
"shared.db.schema.examSubmissions",
|
||||
@@ -10118,7 +10532,7 @@
|
||||
"name": "getClassMasterySummary",
|
||||
"signature": "(classId: string) => Promise<ClassMasterySummary | null>",
|
||||
"file": "data-access.ts",
|
||||
"purpose": "获取班级掌握度摘要(学生数、平均掌握度、知识点统计、需重点关注学生)",
|
||||
"purpose": "获取班级掌握度摘要(学生数、平均掌握度、知识点统计、需重点关注学生;v3 优化:两阶段 Promise.all 并行查询班级信息+学生 ID、用户名+掌握度)",
|
||||
"deps": [
|
||||
"shared.db",
|
||||
"shared.db.schema.classes",
|
||||
@@ -10182,7 +10596,7 @@
|
||||
"name": "getDiagnosticReports",
|
||||
"signature": "(filters: DiagnosticReportQueryParams) => Promise<DiagnosticReportWithDetails[]>",
|
||||
"file": "data-access-reports.ts",
|
||||
"purpose": "查询诊断报告列表(可按 studentId/reportType/status/period 过滤,含学生名和生成者名)",
|
||||
"purpose": "查询诊断报告列表(可按 studentId/reportType/status/period 过滤,含学生名和生成者名;v3 修复:conditions 显式标注 SQL[] 类型,移除 round2 死代码)",
|
||||
"deps": [
|
||||
"shared.db",
|
||||
"shared.db.schema.learningDiagnosticReports",
|
||||
@@ -10787,13 +11201,35 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "getSubjectOptions",
|
||||
"name": "buildCourseSelect",
|
||||
"file": "data-access.ts",
|
||||
"signature": "() => Promise<{id, name}[]>",
|
||||
"purpose": "获取学科选项(按 order, name 排序)",
|
||||
"signature": "() => query builder",
|
||||
"purpose": "构建 electiveCourses 表查询(仅查询本表字段,不跨表 JOIN;v3 重构:移除跨模块 LEFT JOIN,名称解析改由 resolveCourseDisplayNames 异步聚合)",
|
||||
"usedBy": [
|
||||
"admin/elective/create/page.tsx",
|
||||
"admin/elective/[id]/edit/page.tsx"
|
||||
"data-access.getElectiveCourses",
|
||||
"data-access.getElectiveCourseById",
|
||||
"data-access-selections.getAvailableCoursesForStudent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "mapCourseRow",
|
||||
"file": "data-access.ts",
|
||||
"signature": "(row: CourseCoreRow, display: {teacherName?, subjectName?, gradeName?}) => ElectiveCourseWithDetails",
|
||||
"purpose": "将核心行 + 显示名映射为 ElectiveCourseWithDetails(v3 抽取:消除 data-access 与 data-access-selections 重复代码)",
|
||||
"usedBy": [
|
||||
"data-access.getElectiveCourses",
|
||||
"data-access.getElectiveCourseById",
|
||||
"data-access-selections.getAvailableCoursesForStudent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "resolveCourseDisplayNames",
|
||||
"file": "data-access.ts",
|
||||
"signature": "(rows: CourseCoreRow[]) => Promise<{teacherName?, subjectName?, gradeName?}[]>",
|
||||
"purpose": "并行聚合教师名(users.getUserNamesByIds)、学科(school.getSubjectOptions)、年级(school.getGradeOptions),返回每行的显示名映射(v3 重构:替代跨模块 LEFT JOIN)",
|
||||
"usedBy": [
|
||||
"data-access.getElectiveCourses",
|
||||
"data-access.getElectiveCourseById"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -10838,7 +11274,7 @@
|
||||
"name": "runLottery",
|
||||
"file": "data-access-operations.ts",
|
||||
"signature": "(courseId: string) => Promise<{enrolled: number, waitlist: number}>",
|
||||
"purpose": "抽签录取(随机打乱 selected 记录,前 capacity 名 enrolled,其余 waitlist,课程 status=closed)",
|
||||
"purpose": "抽签录取(Fisher-Yates 无偏洗牌 selected 记录,前 capacity 名 enrolled,其余 waitlist,课程 status=closed;v3 修复:替换 sort(Math.random) 有偏洗牌)",
|
||||
"usedBy": [
|
||||
"actions.runLotteryAction"
|
||||
]
|
||||
@@ -10847,7 +11283,7 @@
|
||||
"name": "selectCourse",
|
||||
"file": "data-access-operations.ts",
|
||||
"signature": "(courseId: string, studentId: string, priority?: number) => Promise<{status: CourseSelectionStatus, message: string}>",
|
||||
"purpose": "学生选课(校验课程状态/时间窗口/重复选课;FCFS 模式即时 enrolled/waitlist,lottery 模式 selected)",
|
||||
"purpose": "学生选课(校验课程状态/时间窗口/重复选课;FCFS 模式即时 enrolled/waitlist,lottery 模式 selected;v3 修复:db.transaction 包裹 + .for('update') 锁课程行防 FCFS 超卖)",
|
||||
"usedBy": [
|
||||
"actions.selectCourseAction"
|
||||
]
|
||||
@@ -10856,7 +11292,7 @@
|
||||
"name": "dropCourse",
|
||||
"file": "data-access-operations.ts",
|
||||
"signature": "(courseId: string, studentId: string) => Promise<void>",
|
||||
"purpose": "学生退课(status=dropped;FCFS 模式自动递补 waitlist 首位)",
|
||||
"purpose": "学生退课(status=dropped;FCFS 模式自动递补 waitlist 首位;v3 修复:db.transaction 包裹 + .for('update') 锁课程行保证递补一致性)",
|
||||
"usedBy": [
|
||||
"actions.dropCourseAction"
|
||||
]
|
||||
@@ -10893,6 +11329,12 @@
|
||||
"file": "types.ts",
|
||||
"definition": "ElectiveCourse & { teacherName?, subjectName?, gradeName? }"
|
||||
},
|
||||
{
|
||||
"name": "CourseCoreRow",
|
||||
"type": "type",
|
||||
"file": "data-access.ts",
|
||||
"definition": "buildCourseSelect 返回行的推断类型(v3 新增:供 mapCourseRow/resolveCourseDisplayNames 共享)"
|
||||
},
|
||||
{
|
||||
"name": "CourseSelection",
|
||||
"type": "interface",
|
||||
@@ -11025,7 +11467,7 @@
|
||||
},
|
||||
"lesson_preparation": {
|
||||
"path": "src/modules/lesson-preparation",
|
||||
"description": "教师备课模块:基于教材章节创建课案(Block 编辑器),支持模板、版本管理、知识点标注、题目创建/拉取、作业发布",
|
||||
"description": "教师备课模块:基于教材章节创建课案(节点图编辑器 React Flow,v2 nodes+edges 数据结构),支持模板、版本管理、知识点标注、题目创建/拉取、作业发布。编辑器从列表式(BlockRenderer + @dnd-kit)升级为节点图式(NodeEditor + @xyflow/react),旧 v1 数据通过 migrateV1ToV2() 自动迁移",
|
||||
"exports": {
|
||||
"dataAccess": [
|
||||
{
|
||||
@@ -11046,7 +11488,7 @@
|
||||
{
|
||||
"name": "updateLessonPlanContent",
|
||||
"file": "data-access.ts",
|
||||
"purpose": "更新课案内容(Block JSON)"
|
||||
"purpose": "更新课案内容(v2 nodes+edges JSON)"
|
||||
},
|
||||
{
|
||||
"name": "softDeleteLessonPlan",
|
||||
@@ -11066,7 +11508,17 @@
|
||||
{
|
||||
"name": "buildInitialContent",
|
||||
"file": "data-access.ts",
|
||||
"purpose": "基于模板构建初始课案内容"
|
||||
"purpose": "基于模板构建初始课案内容(v2 nodes+edges)"
|
||||
},
|
||||
{
|
||||
"name": "migrateV1ToV2",
|
||||
"file": "data-access.ts",
|
||||
"purpose": "v1→v2 迁移:将旧 blocks 数组转换为 nodes + 线性 edges(节点按网格布局)"
|
||||
},
|
||||
{
|
||||
"name": "normalizeDocument",
|
||||
"file": "data-access.ts",
|
||||
"purpose": "规范化:确保 content 为 v2 格式,兼容旧 v1 数据(自动调用 migrateV1ToV2)"
|
||||
},
|
||||
{
|
||||
"name": "getLessonPlanVersions",
|
||||
@@ -11229,7 +11681,8 @@
|
||||
"homework",
|
||||
"classes",
|
||||
"files",
|
||||
"shared/lib/ai"
|
||||
"shared/lib/ai",
|
||||
"@xyflow/react"
|
||||
],
|
||||
"files": [
|
||||
"types.ts",
|
||||
@@ -11251,6 +11704,9 @@
|
||||
"components/lesson-plan-card.tsx",
|
||||
"components/lesson-plan-filters.tsx",
|
||||
"components/lesson-plan-editor.tsx",
|
||||
"components/node-editor.tsx",
|
||||
"components/node-edit-panel.tsx",
|
||||
"components/nodes/lesson-node.tsx",
|
||||
"components/block-renderer.tsx",
|
||||
"components/template-picker.tsx",
|
||||
"components/version-history-drawer.tsx",
|
||||
@@ -12028,6 +12484,7 @@
|
||||
"shared": [
|
||||
"db",
|
||||
"auth-guard.requireAuth",
|
||||
"auth-guard.getAuthContext",
|
||||
"db.schema.parentStudentRelations",
|
||||
"types"
|
||||
],
|
||||
@@ -12041,14 +12498,13 @@
|
||||
"classes": [
|
||||
"data-access.getStudentClasses",
|
||||
"data-access.getStudentSchedule",
|
||||
"data-access.getClassNameById",
|
||||
"data-access.getStudentActiveClassId"
|
||||
"data-access.getStudentActiveClass"
|
||||
],
|
||||
"grades": [
|
||||
"data-access.getStudentGradeSummary"
|
||||
],
|
||||
"school": [
|
||||
"data-access.getGradeOptions"
|
||||
"data-access.getGradeNameById"
|
||||
],
|
||||
"users": [
|
||||
"data-access.getUserBasicInfo",
|
||||
@@ -12060,7 +12516,8 @@
|
||||
"dependsOn": [
|
||||
"shared",
|
||||
"auth",
|
||||
"notifications"
|
||||
"notifications",
|
||||
"classes"
|
||||
],
|
||||
"uses": {
|
||||
"shared": [
|
||||
@@ -12087,6 +12544,10 @@
|
||||
"data-access.getUnreadNotificationCount (via re-export)",
|
||||
"preferences.getNotificationPreferences (via re-export)",
|
||||
"preferences.upsertNotificationPreferences (via re-export)"
|
||||
],
|
||||
"classes": [
|
||||
"data-access.getTeacherIdsByClassIds",
|
||||
"data-access.getStudentActiveClassId"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -12332,6 +12793,11 @@
|
||||
"files": [
|
||||
"data-access.createFileAttachment",
|
||||
"data-access.getFileAttachmentsByTarget"
|
||||
],
|
||||
"external": [
|
||||
"@xyflow/react(React Flow 节点图编辑器:ReactFlow/Background/Controls/MiniMap/Handle/applyNodeChanges/applyEdgeChanges)",
|
||||
"@paralleldrive/cuid2(节点 ID 生成)",
|
||||
"zustand(编辑器状态管理)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -12449,6 +12915,12 @@
|
||||
"type": "data-access",
|
||||
"description": "✅ P0-4 / P1-5 已修复:messaging 通过 sendNotification dispatcher 发送通知,通知 CRUD 和偏好通过 re-export 保持向后兼容"
|
||||
},
|
||||
{
|
||||
"from": "messaging",
|
||||
"to": "classes",
|
||||
"type": "data-access",
|
||||
"description": "getRecipients 通过 classes data-access.getTeacherIdsByClassIds / getStudentActiveClassId 获取班级教师 ID,支持 class_members(学生)和 children(家长)数据范围"
|
||||
},
|
||||
{
|
||||
"from": "classes",
|
||||
"to": "homework",
|
||||
@@ -13456,9 +13928,11 @@
|
||||
"component": "StudentDashboardView",
|
||||
"type": "server",
|
||||
"dataAccess": [
|
||||
"dashboard/data-access (student)",
|
||||
"homework/data-access.getStudentDashboardGrades",
|
||||
"classes/data-access.getStudentClasses"
|
||||
"users/data-access.getCurrentStudentUser",
|
||||
"classes/data-access.getStudentClasses",
|
||||
"classes/data-access.getStudentSchedule",
|
||||
"homework/data-access.getStudentHomeworkAssignments",
|
||||
"homework/data-access.getStudentDashboardGrades"
|
||||
],
|
||||
"permission": "homework:submit"
|
||||
},
|
||||
@@ -13467,13 +13941,14 @@
|
||||
"type": "server",
|
||||
"module": "homework",
|
||||
"dataAccess": [
|
||||
"users/data-access.getCurrentStudentUser",
|
||||
"homework/data-access.getStudentHomeworkAssignments"
|
||||
],
|
||||
"permission": "homework:submit"
|
||||
},
|
||||
"/student/learning/assignments/[assignmentId]": {
|
||||
"component": "学生作答/复习",
|
||||
"type": "client",
|
||||
"type": "server",
|
||||
"module": "homework",
|
||||
"actions": [
|
||||
"startHomeworkSubmissionAction",
|
||||
@@ -13481,6 +13956,7 @@
|
||||
"submitHomeworkAction"
|
||||
],
|
||||
"dataAccess": [
|
||||
"users/data-access.getCurrentStudentUser",
|
||||
"homework/data-access.getStudentHomeworkTakeData"
|
||||
],
|
||||
"permission": "homework:submit"
|
||||
@@ -13488,6 +13964,10 @@
|
||||
"/student/learning/courses": {
|
||||
"component": "StudentCoursesView",
|
||||
"type": "server",
|
||||
"dataAccess": [
|
||||
"users/data-access.getCurrentStudentUser",
|
||||
"classes/data-access.getStudentClasses"
|
||||
],
|
||||
"permission": "homework:submit"
|
||||
},
|
||||
"/student/learning/textbooks": {
|
||||
@@ -13495,18 +13975,20 @@
|
||||
"type": "server",
|
||||
"module": "textbooks",
|
||||
"dataAccess": [
|
||||
"users/data-access.getCurrentStudentUser",
|
||||
"textbooks/data-access.getTextbooks"
|
||||
],
|
||||
"permission": "textbook:read"
|
||||
},
|
||||
"/student/learning/textbooks/[id]": {
|
||||
"component": "学生教材阅读(只读)",
|
||||
"type": "client",
|
||||
"type": "server",
|
||||
"module": "textbooks",
|
||||
"dataAccess": [
|
||||
"users/data-access.getCurrentStudentUser",
|
||||
"textbooks/data-access.getTextbookById",
|
||||
"getChaptersByTextbookId",
|
||||
"getKnowledgePointsByTextbookId"
|
||||
"textbooks/data-access.getChaptersByTextbookId",
|
||||
"textbooks/data-access.getKnowledgePointsByTextbookId"
|
||||
],
|
||||
"permission": "textbook:read"
|
||||
},
|
||||
@@ -13515,6 +13997,8 @@
|
||||
"type": "server",
|
||||
"module": "classes",
|
||||
"dataAccess": [
|
||||
"users/data-access.getCurrentStudentUser",
|
||||
"classes/data-access.getStudentClasses",
|
||||
"classes/data-access.getStudentSchedule"
|
||||
],
|
||||
"permission": "homework:submit"
|
||||
@@ -13524,7 +14008,7 @@
|
||||
"type": "server",
|
||||
"module": "grades",
|
||||
"dataAccess": [
|
||||
"grades/actions.getStudentGradeSummaryAction"
|
||||
"grades/data-access.getStudentGradeSummary"
|
||||
],
|
||||
"permission": "grade_record:read"
|
||||
},
|
||||
@@ -13540,7 +14024,7 @@
|
||||
},
|
||||
"/student/diagnostic": {
|
||||
"component": "StudentDiagnosticView",
|
||||
"type": "client",
|
||||
"type": "server",
|
||||
"module": "diagnostic",
|
||||
"dataAccess": [
|
||||
"diagnostic/data-access.getStudentMasterySummary (ctx.userId)",
|
||||
|
||||
331
docs/feature/001_first_login_onboarding.md
Normal file
331
docs/feature/001_first_login_onboarding.md
Normal file
@@ -0,0 +1,331 @@
|
||||
# 首次登录引导(Onboarding)重大问题讨论 · v2
|
||||
|
||||
> 版本:**v2**(替代 v1,2026-06-18)
|
||||
> 状态:**讨论中,待决策**
|
||||
> 关联架构图:`docs/architecture/004_architecture_impact_map.md` §2.1 shared 层 / §3 已知问题 P2-4
|
||||
> 关联代码:
|
||||
> - [src/shared/components/onboarding-gate.tsx](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx)(312 行,未变)
|
||||
> - [src/app/api/onboarding/status/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/status/route.ts)(未变)
|
||||
> - [src/app/api/onboarding/complete/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts)(未变)
|
||||
> - [src/app/layout.tsx#L41](file:///e:/Desktop/CICD/src/app/layout.tsx#L41)(全局挂载点,未变)
|
||||
> - [src/auth.ts](file:///e:/Desktop/CICD/src/auth.ts)(jwt/session 回调,未注入 onboarded)
|
||||
> - [src/proxy.ts](file:///e:/Desktop/CICD/src/proxy.ts)(middleware,无 onboarding 拦截)
|
||||
|
||||
---
|
||||
|
||||
## 〇、v2 与 v1 的差异说明
|
||||
|
||||
经 git 核实(`git log` + `git status` + `git diff`),onboarding 相关代码自 v1 审查以来**零改动**:
|
||||
- `onboarding-gate.tsx`、`api/onboarding/*/route.ts`、`layout.tsx`、`auth.ts` 均无修改
|
||||
- 工作区改动集中在 `proxy.ts`(权限常量替换)、`schema.ts`(新增 lesson_plans 表)等与 onboarding 无关的文件
|
||||
|
||||
v2 在 v1 基础上**新增 9 项 v1 遗漏的问题**(标为「v2 新增」),其中含 2 项 P0 级越权漏洞。问题编号沿用 v1,新增项顺延。
|
||||
|
||||
---
|
||||
|
||||
## 一、背景与定位
|
||||
|
||||
按项目规则"先图后码",从架构影响地图定位 Onboarding 节点:
|
||||
|
||||
- **shared 层**:`components/onboarding-gate.tsx`(312 行)已被架构图标记 ⚠️ P2-4「业务逻辑泄漏到 shared」
|
||||
- **app 层**:`/api/onboarding/status`、`/api/onboarding/complete` 两条路由
|
||||
- **数据层**:`users.onboardedAt`([schema.ts:41](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L41))
|
||||
- **被调用模块**:`modules/classes/data-access.ts` 的 `enrollStudentByInvitationCode`(学生路径);教师路径**绕过** `enrollTeacherByInvitationCode` 直接写表
|
||||
|
||||
当前实现:全局 Dialog。`app/layout.tsx` 第 41 行无条件挂载 `<OnboardingGate />`,组件内 `useEffect` 拉取 `/api/onboarding/status`,`required === true` 时弹出不可关闭的 4 步 Dialog。
|
||||
|
||||
---
|
||||
|
||||
## 二、现状代码盘点
|
||||
|
||||
### 2.1 组件层(onboarding-gate.tsx)
|
||||
|
||||
| 步骤 | 标题 | 采集字段 | 备注 |
|
||||
|------|------|----------|------|
|
||||
| Step 0 | 角色选择 | role(student/teacher/parent) | admin 只读;其他角色用户可下拉**自选** |
|
||||
| Step 1 | 通用信息 | name / phone / address | 仅校验非空 |
|
||||
| Step 2 | 角色信息 | classCodes(学生/教师)、teacherSubjects(教师) | 可跳过;家长显示"暂不需要配置" |
|
||||
| Step 3 | 完成 | — | 调 `/api/onboarding/complete` 后跳 `/dashboard` |
|
||||
|
||||
角色推断逻辑([第 90-94 行](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L90-L94))用权限点反推角色。
|
||||
|
||||
### 2.2 API 层
|
||||
|
||||
- `GET /api/onboarding/status`:查 `users.onboardedAt` + 查 `usersToRoles` 推断角色
|
||||
- `POST /api/onboarding/complete`:update users → insert usersToRoles → 学生调 `enrollStudentByInvitationCode` → **教师直接 insert `classSubjectTeachers`** → 写 `onboardedAt`
|
||||
|
||||
### 2.3 关键表结构(v2 补充)
|
||||
|
||||
| 表 | 主键 | 影响 |
|
||||
|----|------|------|
|
||||
| `usersToRoles` | `(userId, roleId)` 联合主键([schema.ts:118](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L118)) | onDuplicateKeyUpdate 无法"替换"角色,只会新增行 → 追加角色 |
|
||||
| `classSubjectTeachers` | `(classId, subjectId)` 联合主键([schema.ts:364](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L364)) | 一个班级一个科目只有一位教师 → onDuplicateKeyUpdate 会**覆盖现有教师** |
|
||||
|
||||
---
|
||||
|
||||
## 三、重大问题清单(按风险分级)
|
||||
|
||||
### 🔴 P0 级:安全/合规/越权
|
||||
|
||||
#### P0-1 用户可自选角色(严重越权)
|
||||
- **位置**:[onboarding-gate.tsx:192-201](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L192-L201)、[complete/route.ts:32-35](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L32-L35)
|
||||
- **问题**:Step 0 允许任意登录用户自选 student/teacher/parent;`complete/route.ts` 直接信任前端 `body.role`。
|
||||
- **后果**:任何注册用户可自封 teacher 获得 `exam:create`、`homework:grade` 等权限。
|
||||
- **违反**:K12 行业铁律「角色由管理员预分配」、项目规则「Server Action 必须用 `requirePermission()`」。
|
||||
|
||||
#### P0-2 教师可绑定任意班级+科目
|
||||
- **位置**:[complete/route.ts:95-130](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L95-L130)
|
||||
- **问题**:教师通过 `classCodes`(6 位邀请码)可把自己写入任意班级的 `classSubjectTeachers`,`teacherSubjects` 由前端任意提交,服务端仅做"名称存在性"校验。
|
||||
- **后果**:教师可越权查看任意班级学生名单、成绩。
|
||||
|
||||
#### P0-3 无权限校验、无 Zod、无事务
|
||||
- **位置**:[complete/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts) 整文件
|
||||
- **问题**:仅检查 `auth()` 登录态,无 `requirePermission()`;用 `String(body.role ?? "")` 手动解析无 Zod(架构图 005 声称"validation: Zod schema"与实际不符);5 次独立 DB 写入无 `db.transaction()`;运行时 `db.insert(roles)` 创建角色记录([第 66-68 行](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L66-L68))属异常路径。
|
||||
|
||||
#### P0-4 教师可覆盖现有任课教师(v2 新增,严重破坏)
|
||||
- **位置**:[complete/route.ts:124-127](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L124-L127)
|
||||
- **问题**:`classSubjectTeachers` 主键为 `(classId, subjectId)`([schema.ts:364](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L364)),一个班级一个科目只有一位教师。onboarding 用 `onDuplicateKeyUpdate({ set: { teacherId: userId, ... } })`,**会直接覆盖该班级该科目已有的任课教师**。
|
||||
- **对比**:`modules/classes/data-access.ts` 的 `enrollTeacherByInvitationCode`([第 637 行](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L637))有完整校验 `if (mapping?.teacherId && mapping.teacherId !== tid) throw new Error("Subject already assigned")`,且只认领 `teacherId IS NULL` 的空缺位置([第 657 行](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L657))。onboarding **绕过了该函数**,直接 insert。
|
||||
- **后果**:任何自封教师的人可抢占全校任意班级的任课位置,踢掉真实任课教师,篡改任课关系。
|
||||
- **违反**:项目规则「modules 之间通过对方 data-access 通信,不直接查询对方 DB 表」。
|
||||
|
||||
#### P0-5 角色追加越权(v2 新增)
|
||||
- **位置**:[complete/route.ts:82-87](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L82-L87)
|
||||
- **问题**:`usersToRoles` 主键为 `(userId, roleId)` 联合主键([schema.ts:118](file:///e:/Desktop/CICD/src/shared/db/schema.ts#L118))。`db.insert(usersToRoles).values({ userId, roleId }).onDuplicateKeyUpdate({ set: { roleId } })` 中,`set roleId` 无意义(roleId 已是要插入的值)。当用户已有其他 roleId 时,此操作**新增一行**而非替换——即**追加角色记录**。
|
||||
- **后果**:学生自选 teacher 角色后,给自己追加一条 teacher 角色行;`auth.ts` 的 `resolvePermissions(allRoles)` 会合并所有角色权限([auth.ts:131](file:///e:/Desktop/CICD/src/auth.ts#L131)),学生因此获得 teacher 全部权限。结合 P0-1,这是完整的权限提升链。
|
||||
- **修复方向**:onboarding 不应写 `usersToRoles`,角色分配由管理员后台处理。
|
||||
|
||||
### 🟠 P1 级:架构违规
|
||||
|
||||
#### P1-1 shared 层反向承载领域逻辑
|
||||
- **位置**:[onboarding-gate.tsx](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx) 整文件
|
||||
- **问题**:位于 `shared/components/`,含角色判断、班级代码、教师科目配置等强领域逻辑,通过 fetch 调用业务 API。
|
||||
- **违反**:项目规则「shared 不得反向依赖 @/auth、@/proxy 或任何 modules/*」。
|
||||
- **架构图标记**:004 文档 §2.1 已标记 P2-4。
|
||||
|
||||
#### P1-2 app 层 API 直接跨模块写表
|
||||
- **位置**:[complete/route.ts:6](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L6)
|
||||
- **问题**:直接 import 并写入 `classes`、`classSubjectTeachers`、`subjects` 表,绕过 `modules/classes` 的 data-access 与权限校验。
|
||||
- **违反**:项目规则「app 只能调用 modules 的 Server Actions 和 data-access」「modules 之间通过对方 data-access 通信」。
|
||||
|
||||
#### P1-3 角色推断双源不一致
|
||||
- **位置**:[status/route.ts:29-41](file:///e:/Desktop/CICD/src/app/api/onboarding/status/route.ts#L29-L41) vs [onboarding-gate.tsx:90-94](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L90-L94)
|
||||
- **问题**:status API 用 `roles.name` 推断(含 `grade_head/teaching_head → teacher` 归一化),组件用权限点重新推断,两套逻辑可能不一致。
|
||||
|
||||
#### P1-4 auth.ts 未注入 onboarded 状态(v2 新增)
|
||||
- **位置**:[auth.ts:122-177](file:///e:/Desktop/CICD/src/auth.ts#L122-L177) jwt/session 回调
|
||||
- **问题**:jwt 回调每次刷新都查 `users.name` + `usersToRoles` + `roles` 三张表([第 143-153 行](file:///e:/Desktop/CICD/src/auth.ts#L143-L153)),但**只读 `name`,未读 `onboardedAt`**,token 里永远没有 onboarding 状态。
|
||||
- **后果链**:
|
||||
1. `proxy.ts`(middleware)用 `getToken` 读 token,无法判断 onboarded → 无法做重定向拦截
|
||||
2. `status/route.ts` 必须每次查库判断 `required` → 性能损耗
|
||||
3. 客户端无法从 `session.user` 读取 onboarded → 必须额外 fetch
|
||||
4. `onFinish` 调 `update()` 后,token 刷新但 onboarded 仍未注入 → 即便有 middleware 也拦不住
|
||||
- **修复方向**:jwt 回调 `columns: { name: true, onboardedAt: true }`,注入 `token.onboarded = !!fresh.onboardedAt`;session 回调暴露 `session.user.onboarded`。
|
||||
|
||||
#### P1-5 onboarding 绕过 classes 模块封装(v2 新增)
|
||||
- **位置**:[complete/route.ts:95-130](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L95-L130)
|
||||
- **问题**:`modules/classes/data-access.ts` 已提供 `enrollTeacherByInvitationCode`([第 589 行](file:///e:/Desktop/CICD/src/modules/classes/data-access.ts#L589)),含「教师身份校验」「科目已分配校验」「只认领空缺位置」等安全逻辑。onboarding **未调用它**,而是直接 insert `classSubjectTeachers`,绕过全部校验。
|
||||
- **后果**:与 P0-4 叠加,形成完整越权路径。
|
||||
- **违反**:项目规则「modules 之间通过对方 data-access 通信」。
|
||||
|
||||
### 🟡 P2 级:用户体验与可访问性
|
||||
|
||||
#### P2-1 全局 Dialog 模式缺陷
|
||||
- 不可关闭(`canClose = !required`);刷新丢步;无独立 URL;首屏无骨架屏;`useEffect` 拉取期间闪烁。
|
||||
- **对比**:业界主流(Auth.js 官方、Clerk、Vercel 模板)均采用独立路由 `/onboarding` + middleware 重定向。
|
||||
|
||||
#### P2-2 表单校验粗糙
|
||||
- 电话仅校验非空(无手机号格式);姓名/地址无长度限制;班级代码无格式预校验。
|
||||
|
||||
#### P2-3 国际化与可访问性
|
||||
- 中英文混合("Role"、"Select role" 英文);Dialog 缺 `aria-describedby`;进度条无 `aria-valuenow`。
|
||||
|
||||
#### P2-4 进度条与步骤不一致
|
||||
- admin 跳过 Step 2,但进度条仍渲染 4 段,Step 2 永远亮起。
|
||||
|
||||
#### P2-5 完成跳转硬编码 /dashboard(v2 新增)
|
||||
- **位置**:[onboarding-gate.tsx:154](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L154)
|
||||
- **问题**:`router.push("/dashboard")` 硬编码,但 [proxy.ts:23-30](file:///e:/Desktop/CICD/src/proxy.ts#L23-L30) 的 `resolveDefaultPath` 按角色返回 `/admin/dashboard`、`/teacher/dashboard`、`/student/dashboard`、`/parent/dashboard`。
|
||||
- **后果**:非 admin 用户完成 onboarding 后跳 `/dashboard`(不存在),被 proxy 权限检查拦截后重定向,体验为"完成→闪跳→再跳"。
|
||||
|
||||
#### P2-6 家长角色推断死锁(v2 新增)
|
||||
- **位置**:[onboarding-gate.tsx:90-94](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L90-L94)
|
||||
- **问题**:
|
||||
```ts
|
||||
const isTeacher = permissions.includes(EXAM_CREATE)
|
||||
const isStudent = permissions.includes(HOMEWORK_SUBMIT) && !permissions.includes(EXAM_CREATE)
|
||||
const isParent = !EXAM_CREATE && !HOMEWORK_SUBMIT && permissions.includes(EXAM_READ)
|
||||
```
|
||||
- `isTeacher` 先判断且包含 `EXAM_READ`(teacher 有 EXAM_READ),家长条件 `!EXAM_CREATE && EXAM_READ` 与 teacher 重叠
|
||||
- 实际角色权限映射中,parent 是否有 `EXAM_READ` 存疑;若 parent 无 `EXAM_READ`,则 `isParent` 永远为 false → 家长在 Step 2 看到"暂不需要配置"的分支永远不触发,可能落到空白页
|
||||
- **后果**:家长角色无法被正确识别,Step 2 渲染异常。
|
||||
|
||||
#### P2-7 学生注册无错误处理(v2 新增)
|
||||
- **位置**:[complete/route.ts:89-93](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L89-L93)
|
||||
- **问题**:`enrollStudentByInvitationCode` 会 throw(如无效邀请码),但无 try/catch。一个无效码导致整个请求 500,而前面的 `update users` 已执行(无事务)→ 用户 name/phone 已更新但 `onboardedAt` 仍为 null → 下次登录反复弹窗且数据不一致。
|
||||
|
||||
#### P2-8 useEffect 依赖导致重复弹窗(v2 新增)
|
||||
- **位置**:[onboarding-gate.tsx:45-68](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L45-L68)
|
||||
- **问题**:useEffect 依赖 `[status, session?.user?.name]`。`auth.ts` jwt 回调每次刷新会重读 `users.name` 并写入 token([auth.ts:158](file:///e:/Desktop/CICD/src/auth.ts#L158)),若 name 变化(如管理员改了用户名),session.user.name 变化触发 useEffect 重新拉取 status → 可能重复弹窗。
|
||||
|
||||
#### P2-9 不可关闭 Dialog 的冗余 effect(v2 新增)
|
||||
- **位置**:[onboarding-gate.tsx:70-74](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L70-L74)
|
||||
- **问题**:
|
||||
```ts
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
if (!required) return
|
||||
setOpen(true) // 冗余:open 已为 true
|
||||
}, [open, required])
|
||||
```
|
||||
此 effect 在 open 被 Dialog 的 `onOpenChange` 关闭时强制重开,实现"不可关闭"。但逻辑脆弱:若 required 在异步中变化,可能产生状态竞态。应改为在 `onOpenChange` 中直接判断 `if (!canClose) return`。
|
||||
|
||||
---
|
||||
|
||||
## 四、业界大仓(Monorepo)解决方案引用
|
||||
|
||||
### 4.1 Auth.js v5 官方推荐
|
||||
|
||||
- **状态标记**:`users.onboardedAt` + `jwt`/`session` 回调注入;完成时调 `update()` 刷新 token。
|
||||
- **强制方式**:**middleware 重定向**到独立 `/onboarding` 路由。在 `proxy.ts`(Next.js 16 的 middleware)用 `getToken` 读取 `onboarded`,未完成且非白名单路径 → `NextResponse.redirect('/onboarding')`。
|
||||
- **结论**:客户端 Dialog 仅适合"非阻塞偏好补全";强制 onboarding 应等同未登录处理。
|
||||
|
||||
### 4.2 商业方案(Clerk / Supabase / Auth0)共性
|
||||
|
||||
三段式:**metadata 标记 + 强制重定向独立路由 + 服务端 Action 校验**。
|
||||
- 角色等敏感字段放服务端可写的 metadata,**禁止前端自写**。
|
||||
- onboarding 完成回调必须由服务端 Action 写入,前端不能直接改。
|
||||
|
||||
### 4.3 shadcn/ui 生态
|
||||
|
||||
- 官方无内置 Stepper,但 `examples/forms` 与 `blocks` 范式明确:**独立路由页面 + `<Form>`(react-hook-form + zod)+ 父组件持 step state**。
|
||||
- 每步独立 zod schema 渐进式校验,最后一步汇总写入。
|
||||
|
||||
### 4.4 企业级 K12 教务系统(PowerSchool / Veracross / 国内智慧校园)
|
||||
|
||||
**铁律:角色由管理员预分配,用户不可自选。**
|
||||
|
||||
| 角色 | 首次登录采集字段 | 角色来源 |
|
||||
|------|------------------|----------|
|
||||
| 学生 | 学号(预分配不可改)、姓名、性别、出生日期、家长联系方式、紧急联系人 | 管理员批量导入 |
|
||||
| 教师 | 工号(预分配)、姓名、所教科目、任教班级、办公室、联系电话、学历资质 | 教务处预分配 |
|
||||
| 家长 | 与学生关系、学生学号(通过 **Access ID + Access Password** 绑定)、本人姓名、电话、邮箱 | 学校发放凭证,家长绑定子女 |
|
||||
| 管理员 | 工号、姓名、职务、管理范围 | 学校 IT 创建 |
|
||||
|
||||
### 4.5 Monorepo(turborepo / nx)惯例
|
||||
|
||||
- 跨模块"流程型"功能(onboarding、setup-wizard)作为**独立 module**,而非塞进 shared。
|
||||
- nx feature-shell 模式:onboarding 作为 `feature-onboarding` library,依赖 `data-access-user`、`data-access-class`。
|
||||
- Vercel 自家项目:`app/(app)/onboarding/[[...step]]/page.tsx` 路由组 + `modules/onboarding/` 模块。
|
||||
|
||||
---
|
||||
|
||||
## 五、重构方案建议(待讨论)
|
||||
|
||||
### 5.1 目标架构
|
||||
|
||||
```
|
||||
app/
|
||||
├─ (auth)/login/ # 登录页(proxy 白名单)
|
||||
├─ (onboarding)/onboarding/ # 新增独立路由
|
||||
│ └─ page.tsx # 服务端组件,读 session.onboarded 决定渲染
|
||||
└─ proxy.ts # 增强:未 onboarded 时重定向
|
||||
|
||||
modules/onboarding/ # 新建模块
|
||||
├─ actions.ts # completeOnboardingAction(Server Action + requirePermission)
|
||||
├─ data-access.ts # 仅操作 users.onboardedAt
|
||||
├─ schema.ts # Zod:name/phone/address/classCodes
|
||||
├─ types.ts
|
||||
└─ components/
|
||||
├─ OnboardingStepper.tsx
|
||||
├─ RoleConfirmStep.tsx # 只读展示管理员分配的角色
|
||||
├─ ProfileStep.tsx # 姓名/电话/住址
|
||||
└─ BindingStep.tsx # 学生:确认班级;教师:确认任课;家长:绑定子女
|
||||
|
||||
shared/
|
||||
└─ components/onboarding-gate.tsx # 删除
|
||||
```
|
||||
|
||||
### 5.2 关键改动点
|
||||
|
||||
1. **auth.ts 回调注入 onboarded**(P1-4):jwt 回调 `columns: { name: true, onboardedAt: true }`,`token.onboarded = !!fresh.onboardedAt`;session 回调暴露 `session.user.onboarded`。
|
||||
2. **proxy.ts 增加 onboarding 拦截**:读 `token.onboarded`,未完成且路径不在白名单(`/login`、`/api/auth`、`/onboarding`、静态资源)→ 重定向 `/onboarding`。
|
||||
3. **删除 `shared/components/onboarding-gate.tsx`**,从 `app/layout.tsx` 移除挂载。
|
||||
4. **新建 `modules/onboarding/`**,承载所有领域逻辑。
|
||||
5. **新建 `app/(onboarding)/onboarding/page.tsx`** 独立路由。
|
||||
6. **删除 `app/api/onboarding/*/route.ts`**,改为 `modules/onboarding/actions.ts` 的 Server Action。
|
||||
7. **角色只读化**(P0-1/P0-5):Step 0 改为"角色确认"——只读展示 `usersToRoles` 中的角色,用户不可改;**onboarding 不写 `usersToRoles`**。
|
||||
8. **班级绑定改造**(P0-2/P0-4/P1-5):
|
||||
- 学生:仅"确认"管理员预分配的班级,或输入邀请码(调 `enrollStudentByInvitationCode`)
|
||||
- 教师:**必须调 `enrollTeacherByInvitationCode`**(含"Subject already assigned"校验),禁止直接 insert;理想方案是仅"确认"管理员预分配
|
||||
- 家长:输入"子女学号 + 绑定码"绑定子女(参考 PowerSchool Access ID 模式)
|
||||
9. **事务化**(P0-3/P2-7):`completeOnboardingAction` 用 `db.transaction()` 包裹所有写入,`onboardedAt` 在事务最后写入。
|
||||
10. **Zod 校验**(P0-3):`onboardingSchema`,phone 用 `z.string().regex(/^1\d{10}$/)`,name `z.string().min(1).max(50)`,address `z.string().max(200).optional()`。
|
||||
11. **完成跳转修正**(P2-5):用 `resolveDefaultPath(roles)` 替代硬编码 `/dashboard`。
|
||||
12. **角色推断统一**(P1-3/P2-6):删除组件内的权限点反推逻辑,统一从 `session.user.roles`(auth.ts 已注入)读取。
|
||||
|
||||
### 5.3 迁移兼容
|
||||
|
||||
- 已 onboarded 用户(`onboardedAt` 非空)不受影响,proxy 直接放行。
|
||||
- 未 onboarded 用户下次登录被重定向到 `/onboarding`(而非弹 Dialog)。
|
||||
- 无需数据迁移,`users.onboardedAt` 字段保留。
|
||||
|
||||
---
|
||||
|
||||
## 六、待决策的开放问题
|
||||
|
||||
### Q1:角色分配策略
|
||||
- **方案 A**(推荐,符合 K12 铁律):onboarding 中角色完全只读,由管理员后台预分配;用户无法改变角色。
|
||||
- **方案 B**:保留角色选择,但服务端校验"用户已有该角色"才允许(即只能从已有角色中选主角色)。
|
||||
- **方案 C**:暂不改动角色选择,仅修复其他问题。
|
||||
|
||||
### Q2:教师任课关系绑定
|
||||
- **方案 A**(推荐):onboarding 中教师**仅确认**管理员预分配的任课关系,不自填班级代码。
|
||||
- **方案 B**:保留自填邀请码,但**必须调 `enrollTeacherByInvitationCode`**(含"Subject already assigned"校验),禁止直接 insert。
|
||||
- **方案 C**:完全移除 onboarding 中的班级绑定,统一由管理员后台处理。
|
||||
|
||||
### Q3:家长绑定子女方式
|
||||
- **方案 A**(推荐,PowerSchool 模式):家长输入"子女学号 + 学校发放的 6 位绑定码"。
|
||||
- **方案 B**:家长输入"子女学号 + 子女生日"作为验证。
|
||||
- **方案 C**:暂不实现家长绑定,由管理员后台预绑定。
|
||||
|
||||
### Q4:onboarding 路由形态
|
||||
- **方案 A**(推荐):单页 `/onboarding` + 客户端 stepper(步骤状态用 query param 持久化)。
|
||||
- **方案 B**:嵌套路由 `/onboarding/role`、`/onboarding/profile`、`/onboarding/binding`(每步独立 Server Action)。
|
||||
- **方案 C**:保留全局 Dialog,仅修复安全与架构问题。
|
||||
|
||||
### Q5:实施范围
|
||||
- **方案 A**:一次性完成 P0 + P1 + P2 全部整改。
|
||||
- **方案 B**(推荐):先做 P0(安全/越权)+ P1(架构),P2(UX)后续迭代。
|
||||
- **方案 C**:仅做 P0 紧急修复,P1/P2 列入 backlog。
|
||||
|
||||
### Q6:auth.ts jwt 回调性能(v2 新增)
|
||||
jwt 回调每次刷新查 3 张表([auth.ts:143-153](file:///e:/Desktop/CICD/src/auth.ts#L143-L153))。注入 onboarded 可复用此次查库,但是否同步优化为「仅在登录时全量查、刷新时轻量查」?
|
||||
- **方案 A**:复用现有查库,只加 `onboardedAt` 字段(最小改动)。
|
||||
- **方案 B**:重构为登录时全量、刷新时只查 `onboardedAt`(优化性能)。
|
||||
|
||||
---
|
||||
|
||||
## 七、附录:问题与代码位置速查
|
||||
|
||||
| 编号 | 问题 | 代码位置 | 风险 | v2 新增 |
|
||||
|------|------|----------|------|---------|
|
||||
| P0-1 | 用户自选角色 | [onboarding-gate.tsx:192-201](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L192-L201) | 🔴 | |
|
||||
| P0-2 | 教师绑任意班级 | [complete/route.ts:95-130](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L95-L130) | 🔴 | |
|
||||
| P0-3 | 无权限校验/Zod/事务 | [complete/route.ts](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts) 整文件 | 🔴 | |
|
||||
| P0-4 | 教师覆盖现有任课教师 | [complete/route.ts:124-127](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L124-L127) | 🔴 | ✅ |
|
||||
| P0-5 | 角色追加越权 | [complete/route.ts:82-87](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L82-L87) | 🔴 | ✅ |
|
||||
| P1-1 | shared 反向承载领域逻辑 | [onboarding-gate.tsx](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx) 整文件 | 🟠 | |
|
||||
| P1-2 | app 层跨模块写表 | [complete/route.ts:6](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L6) | 🟠 | |
|
||||
| P1-3 | 角色推断双源不一致 | [status/route.ts:29-41](file:///e:/Desktop/CICD/src/app/api/onboarding/status/route.ts#L29-L41) vs [onboarding-gate.tsx:90-94](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L90-L94) | 🟠 | |
|
||||
| P1-4 | auth 未注入 onboarded | [auth.ts:143-153](file:///e:/Desktop/CICD/src/auth.ts#L143-L153) | 🟠 | ✅ |
|
||||
| P1-5 | 绕过 classes 模块封装 | [complete/route.ts:95-130](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L95-L130) | 🟠 | ✅ |
|
||||
| P2-1 | 全局 Dialog 缺陷 | [app/layout.tsx:41](file:///e:/Desktop/CICD/src/app/layout.tsx#L41) | 🟡 | |
|
||||
| P2-2 | 表单校验粗糙 | [onboarding-gate.tsx:88](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L88) | 🟡 | |
|
||||
| P2-3 | i18n/a11y | [onboarding-gate.tsx:188-194](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L188-L194) | 🟡 | |
|
||||
| P2-4 | 进度条与步骤不一致 | [onboarding-gate.tsx:179-184](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L179-L184) | 🟡 | |
|
||||
| P2-5 | 完成跳转硬编码 /dashboard | [onboarding-gate.tsx:154](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L154) | 🟡 | ✅ |
|
||||
| P2-6 | 家长角色推断死锁 | [onboarding-gate.tsx:90-94](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L90-L94) | 🟡 | ✅ |
|
||||
| P2-7 | 学生注册无错误处理 | [complete/route.ts:89-93](file:///e:/Desktop/CICD/src/app/api/onboarding/complete/route.ts#L89-L93) | 🟡 | ✅ |
|
||||
| P2-8 | useEffect 重复弹窗 | [onboarding-gate.tsx:45-68](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L45-L68) | 🟡 | ✅ |
|
||||
| P2-9 | 冗余不可关闭 effect | [onboarding-gate.tsx:70-74](file:///e:/Desktop/CICD/src/shared/components/onboarding-gate.tsx#L70-L74) | 🟡 | ✅ |
|
||||
412
docs/feature/f_bk.md
Normal file
412
docs/feature/f_bk.md
Normal file
@@ -0,0 +1,412 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html class="light" lang="en"><head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Chinese Education Suite - Text Study</title>
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||||
<script id="tailwind-config">
|
||||
tailwind.config = {
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {
|
||||
"colors": {
|
||||
"surface-container-highest": "#e4e2e2",
|
||||
"error-container": "#ffdad6",
|
||||
"primary-fixed": "#d4e3ff",
|
||||
"primary": "#005dac",
|
||||
"secondary-fixed-dim": "#ffb786",
|
||||
"primary-fixed-dim": "#a5c8ff",
|
||||
"secondary": "#964900",
|
||||
"primary-container": "#1976d2",
|
||||
"surface-container-low": "#f5f3f3",
|
||||
"on-tertiary-fixed": "#002204",
|
||||
"on-surface-variant": "#414752",
|
||||
"on-surface": "#1b1c1c",
|
||||
"tertiary": "#0d6c1e",
|
||||
"on-primary-fixed-variant": "#004786",
|
||||
"inverse-surface": "#303031",
|
||||
"secondary-container": "#fc820c",
|
||||
"on-secondary-fixed-variant": "#723600",
|
||||
"surface": "#fbf9f8",
|
||||
"error": "#ba1a1a",
|
||||
"background": "#fbf9f8",
|
||||
"on-primary-fixed": "#001c3a",
|
||||
"on-secondary-fixed": "#311300",
|
||||
"on-primary-container": "#fffdff",
|
||||
"outline-variant": "#c1c6d4",
|
||||
"on-tertiary": "#ffffff",
|
||||
"tertiary-fixed": "#9df898",
|
||||
"on-error-container": "#93000a",
|
||||
"surface-variant": "#e4e2e2",
|
||||
"tertiary-container": "#2f8635",
|
||||
"surface-container-lowest": "#ffffff",
|
||||
"on-tertiary-fixed-variant": "#005312",
|
||||
"surface-tint": "#005faf",
|
||||
"on-background": "#1b1c1c",
|
||||
"surface-bright": "#fbf9f8",
|
||||
"outline": "#717783",
|
||||
"on-tertiary-container": "#fdfff7",
|
||||
"inverse-primary": "#a5c8ff",
|
||||
"on-secondary-container": "#5e2c00",
|
||||
"on-secondary": "#ffffff",
|
||||
"surface-dim": "#dbdad9",
|
||||
"surface-container": "#efeded",
|
||||
"secondary-fixed": "#ffdcc6",
|
||||
"on-primary": "#ffffff",
|
||||
"on-error": "#ffffff",
|
||||
"tertiary-fixed-dim": "#82db7e",
|
||||
"surface-container-high": "#e9e8e7",
|
||||
"inverse-on-surface": "#f2f0f0"
|
||||
},
|
||||
"borderRadius": {
|
||||
"DEFAULT": "0.125rem",
|
||||
"lg": "0.25rem",
|
||||
"xl": "0.5rem",
|
||||
"full": "0.75rem"
|
||||
},
|
||||
"spacing": {
|
||||
"xl": "32px",
|
||||
"sidebar_width": "80px",
|
||||
"sidebar_width_hover": "280px",
|
||||
"grid_columns": "12",
|
||||
"gutter": "24px",
|
||||
"2xl": "48px",
|
||||
"xs": "8px",
|
||||
"md": "16px",
|
||||
"lg": "24px",
|
||||
"sm": "12px",
|
||||
"base": "4px"
|
||||
},
|
||||
"fontFamily": {
|
||||
"title-lg": ["Inter"],
|
||||
"display-lg": ["Inter"],
|
||||
"code-md": ["JetBrains Mono"],
|
||||
"body-lg": ["Inter"],
|
||||
"headline-lg-mobile": ["Inter"],
|
||||
"headline-md": ["Inter"],
|
||||
"label-md": ["Inter"],
|
||||
"headline-lg": ["Inter"],
|
||||
"title-md": ["Inter"],
|
||||
"body-md": ["Inter"]
|
||||
},
|
||||
"fontSize": {
|
||||
"title-lg": ["20px", { "lineHeight": "28px", "fontWeight": "600" }],
|
||||
"display-lg": ["48px", { "lineHeight": "56px", "letterSpacing": "-0.02em", "fontWeight": "700" }],
|
||||
"code-md": ["14px", { "lineHeight": "20px", "fontWeight": "400" }],
|
||||
"body-lg": ["16px", { "lineHeight": "26px", "fontWeight": "400" }],
|
||||
"headline-lg-mobile": ["24px", { "lineHeight": "32px", "fontWeight": "600" }],
|
||||
"headline-md": ["24px", { "lineHeight": "32px", "fontWeight": "600" }],
|
||||
"label-md": ["12px", { "lineHeight": "16px", "letterSpacing": "0.05em", "fontWeight": "500" }],
|
||||
"headline-lg": ["32px", { "lineHeight": "40px", "letterSpacing": "-0.01em", "fontWeight": "600" }],
|
||||
"title-md": ["16px", { "lineHeight": "24px", "fontWeight": "600" }],
|
||||
"body-md": ["14px", { "lineHeight": "22px", "fontWeight": "400" }]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.text-reading-chinese {
|
||||
font-family: "KaiTi", "STKaiti", serif;
|
||||
line-height: 2.2;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.annotation-highlight-yellow {
|
||||
background-color: rgba(252, 130, 12, 0.2);
|
||||
border-bottom: 2px dashed #fc820c;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.annotation-highlight-yellow.active {
|
||||
background-color: rgba(252, 130, 12, 0.4);
|
||||
box-shadow: 0 0 0 2px rgba(252, 130, 12, 0.5);
|
||||
}
|
||||
.annotation-highlight-green {
|
||||
background-color: rgba(47, 134, 53, 0.15);
|
||||
border-bottom: 2px dashed #2f8635;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.annotation-highlight-green.active {
|
||||
background-color: rgba(47, 134, 53, 0.3);
|
||||
box-shadow: 0 0 0 2px rgba(47, 134, 53, 0.5);
|
||||
}
|
||||
.floating-toolbar {
|
||||
box-shadow: 0px 4px 12px rgba(0,0,0,0.08);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.node-canvas-bg {
|
||||
background-image: radial-gradient(var(--tw-colors-outline-variant) 1px, transparent 1px);
|
||||
background-size: 24px 24px;
|
||||
background-position: -12px -12px;
|
||||
}
|
||||
|
||||
.sidebar-collapsed {
|
||||
width: 80px;
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.sidebar-collapsed:hover {
|
||||
width: 280px;
|
||||
}
|
||||
.sidebar-collapsed .sidebar-text {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.sidebar-collapsed:hover .sidebar-text {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
.sidebar-collapsed .sidebar-header-compact {
|
||||
display: flex;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.sidebar-collapsed:hover .sidebar-header-compact {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
.sidebar-collapsed .sidebar-header-full {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.sidebar-collapsed:hover .sidebar-header-full {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
|
||||
.connection-line {
|
||||
stroke-dasharray: 6 6;
|
||||
animation: dash 20s linear infinite;
|
||||
}
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: -100;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-background text-on-background antialiased font-body-md overflow-hidden flex">
|
||||
<!-- SideNavBar (Shared Component) - Collapsed by Default -->
|
||||
<nav class="bg-surface-container-low border-r border-outline-variant fixed left-0 top-0 h-full sidebar-collapsed flex flex-col z-40 overflow-hidden shadow-[4px_0_12px_rgba(0,0,0,0.05)]">
|
||||
<!-- Header Profile/Brand Area -->
|
||||
<div class="p-lg border-b border-outline-variant min-h-[140px] flex flex-col justify-center">
|
||||
<!-- Compact Header (Icon only) -->
|
||||
<div class="sidebar-header-compact justify-center items-center h-full">
|
||||
<img class="w-10 h-10 rounded-full object-cover" data-alt="A small, professional portrait avatar of an elementary school teacher, wearing a neat blouse, softly lit with a friendly expression. The background is a clean, bright, out-of-focus classroom setting. Light, optimistic, modern educational aesthetic." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCPm6ajyPls5KuN3NyxyIsYDJjwr4nGreE-xX_wJmhJXxEoloRJliDYKXVHc7pGX7V0JzgMspJ1gypOgUa9gsueX9F6-v1Nyq-yoOajjl5IkVUK-EcPVN1I_QOnZDkoyS-bKMM6bqTmwvjNT-Qeg3ZCLwAbQIVkGSlqmQcXG5XlZ3oHBtVYgcYOZpbEMegS75pxILeSysUPGRhfOxl3LerA0SoAsTgOTo6nIq7AcBzAmmGN_Qjst-6n5EeWdIni83vKOeYjHpOPyuc"/>
|
||||
</div>
|
||||
<!-- Full Header -->
|
||||
<div class="sidebar-header-full flex-col gap-sm">
|
||||
<div class="flex items-center gap-sm">
|
||||
<img class="w-10 h-10 rounded-full object-cover shrink-0" data-alt="A small, professional portrait avatar of an elementary school teacher, wearing a neat blouse, softly lit with a friendly expression. The background is a clean, bright, out-of-focus classroom setting. Light, optimistic, modern educational aesthetic." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCPm6ajyPls5KuN3NyxyIsYDJjwr4nGreE-xX_wJmhJXxEoloRJliDYKXVHc7pGX7V0JzgMspJ1gypOgUa9gsueX9F6-v1Nyq-yoOajjl5IkVUK-EcPVN1I_QOnZDkoyS-bKMM6bqTmwvjNT-Qeg3ZCLwAbQIVkGSlqmQcXG5XlZ3oHBtVYgcYOZpbEMegS75pxILeSysUPGRhfOxl3LerA0SoAsTgOTo6nIq7AcBzAmmGN_Qjst-6n5EeWdIni83vKOeYjHpOPyuc"/>
|
||||
<div class="sidebar-text">
|
||||
<h2 class="font-headline-md text-headline-md font-bold text-primary">Lesson Planner</h2>
|
||||
<p class="font-body-md text-body-md text-on-surface-variant">Primary Chinese</p>
|
||||
</div>
|
||||
</div>
|
||||
<button class="mt-md bg-primary-container text-on-primary-container font-label-md text-label-md py-sm px-md rounded-lg flex items-center justify-center gap-xs hover:opacity-90 transition-opacity sidebar-text w-full">
|
||||
<span class="material-symbols-outlined text-[18px]">add</span>
|
||||
New Lesson Plan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Navigation Links -->
|
||||
<div class="flex-1 overflow-y-auto py-md">
|
||||
<ul class="flex flex-col gap-base px-sm">
|
||||
<!-- Text Study (ACTIVE) -->
|
||||
<li>
|
||||
<a class="flex items-center gap-sm px-md py-sm rounded-lg text-primary font-bold border-l-4 border-primary bg-primary-container/10 transition-transform duration-150" href="#">
|
||||
<span class="material-symbols-outlined shrink-0" style="font-variation-settings: 'FILL' 1;">book</span>
|
||||
<span class="font-title-md text-title-md sidebar-text">Text Study</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Objectives (INACTIVE) -->
|
||||
<li>
|
||||
<a class="flex items-center gap-sm px-md py-sm rounded-lg text-on-surface-variant hover:bg-surface-container-highest transition-colors" href="#">
|
||||
<span class="material-symbols-outlined shrink-0">target</span>
|
||||
<span class="font-title-md text-title-md sidebar-text">Objectives</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Teaching Process (INACTIVE) -->
|
||||
<li>
|
||||
<a class="flex items-center gap-sm px-md py-sm rounded-lg text-on-surface-variant hover:bg-surface-container-highest transition-colors" href="#">
|
||||
<span class="material-symbols-outlined shrink-0">school</span>
|
||||
<span class="font-title-md text-title-md sidebar-text">Teaching Process</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Blackboard Design (INACTIVE) -->
|
||||
<li>
|
||||
<a class="flex items-center gap-sm px-md py-sm rounded-lg text-on-surface-variant hover:bg-surface-container-highest transition-colors" href="#">
|
||||
<span class="material-symbols-outlined shrink-0">draw</span>
|
||||
<span class="font-title-md text-title-md sidebar-text">Blackboard Design</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Resources (INACTIVE) -->
|
||||
<li>
|
||||
<a class="flex items-center gap-sm px-md py-sm rounded-lg text-on-surface-variant hover:bg-surface-container-highest transition-colors" href="#">
|
||||
<span class="material-symbols-outlined shrink-0">folder_open</span>
|
||||
<span class="font-title-md text-title-md sidebar-text">Resources</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Homework (INACTIVE) -->
|
||||
<li>
|
||||
<a class="flex items-center gap-sm px-md py-sm rounded-lg text-on-surface-variant hover:bg-surface-container-highest transition-colors" href="#">
|
||||
<span class="material-symbols-outlined shrink-0">assignment</span>
|
||||
<span class="font-title-md text-title-md sidebar-text">Homework</span>
|
||||
</a>
|
||||
</li>
|
||||
<!-- Preview (INACTIVE) -->
|
||||
<li>
|
||||
<a class="flex items-center gap-sm px-md py-sm rounded-lg text-on-surface-variant hover:bg-surface-container-highest transition-colors" href="#">
|
||||
<span class="material-symbols-outlined shrink-0">visibility</span>
|
||||
<span class="font-title-md text-title-md sidebar-text">Preview</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<!-- Main Content Wrapper -->
|
||||
<div class="ml-[80px] flex-1 flex flex-col h-screen overflow-hidden bg-background">
|
||||
<!-- TopNavBar (Shared Component) -->
|
||||
<header class="bg-surface border-b border-outline-variant flex justify-between items-center h-16 px-lg shrink-0 z-10 relative">
|
||||
<!-- Left: Brand/Context -->
|
||||
<div class="flex items-center gap-md">
|
||||
<h1 class="font-title-lg text-title-lg font-black text-primary">Chinese Education Suite</h1>
|
||||
<div class="h-6 w-px bg-outline-variant mx-sm"></div>
|
||||
<div class="flex items-center gap-xs">
|
||||
<span class="font-title-md text-title-md text-on-surface">《秋天》 (Autumn)</span>
|
||||
<span class="bg-surface-container-high text-on-surface-variant font-label-md text-label-md px-2 py-1 rounded">Grade 1</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Center: Nav Links -->
|
||||
<nav class="hidden lg:flex gap-lg h-full absolute left-1/2 -translate-x-1/2">
|
||||
<a class="flex items-center h-full font-body-md text-body-md text-on-surface-variant hover:text-primary border-b-2 border-transparent transition-colors" href="#">Curriculum</a>
|
||||
<a class="flex items-center h-full font-body-md text-body-md text-on-surface-variant hover:text-primary border-b-2 border-transparent transition-colors" href="#">Standards</a>
|
||||
<a class="flex items-center h-full font-body-md text-body-md text-on-surface-variant hover:text-primary border-b-2 border-transparent transition-colors" href="#">Analytics</a>
|
||||
</nav>
|
||||
<!-- Right: Actions -->
|
||||
<div class="flex items-center gap-md ml-auto">
|
||||
<div class="relative">
|
||||
<span class="material-symbols-outlined absolute left-sm top-1/2 -translate-y-1/2 text-outline text-[20px]">search</span>
|
||||
<input class="pl-xl pr-sm py-1.5 bg-surface-container-low border border-outline-variant rounded-full font-body-md text-body-md focus:outline-none focus:border-primary focus:ring-2 focus:ring-primary/10 transition-all w-48" placeholder="Search..." type="text"/>
|
||||
</div>
|
||||
<button class="bg-secondary-container text-on-secondary-container font-label-md text-label-md py-1.5 px-md rounded-full border border-secondary-container hover:bg-transparent transition-colors flex items-center gap-xs">
|
||||
<span class="material-symbols-outlined text-[16px]">smart_toy</span>
|
||||
AI Assistant
|
||||
</button>
|
||||
<button class="text-primary font-label-md text-label-md hover:opacity-80 transition-opacity">Export Plan</button>
|
||||
<div class="flex items-center gap-xs text-on-surface-variant">
|
||||
<button class="p-xs rounded-full hover:bg-surface-container-highest transition-colors"><span class="material-symbols-outlined text-[20px]">notifications</span></button>
|
||||
<button class="p-xs rounded-full hover:bg-surface-container-highest transition-colors"><span class="material-symbols-outlined text-[20px]">settings</span></button>
|
||||
</div>
|
||||
<img class="w-8 h-8 rounded-full border border-outline-variant" data-alt="A small circular avatar of a user, standard blank profile icon style, subtle grey tones on a white background." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAoTWwwvka05iqtq0cMgF0dpJUpK_48qzYYStPnxDXFahYje8tyCmqaSyBF3jwLqLg6BmaRQJYOnQ40GhsX4wZWX5tHGYz7gRT_E_rPjuD9kzSG5A9wXmc1bbSwiuQ1GAGmL-C7lP5P3fuO5jGFNyQdLwxROqRD5LOpj0zGvcVpEKC7w8XAywqptBTED0cyde1nOpxiCtuap-NzXBMuj-smrxOzXEaGlY4Z98u_OqHKFk6xgSRW4BoqmDk5-tlmDuv-6qyz_4S-Vqc"/>
|
||||
</div>
|
||||
</header>
|
||||
<!-- Workspace Layout - Integrated Canvas -->
|
||||
<main class="flex-1 flex overflow-hidden relative bg-surface-container-low node-canvas-bg cursor-grab active:cursor-grabbing">
|
||||
<!-- Dynamic Connecting Lines (SVG Layer) -->
|
||||
<svg class="absolute inset-0 pointer-events-none w-full h-full" style="z-index: 10;">
|
||||
<path class="connection-line" d="M 580 320 C 650 320, 700 250, 750 250" fill="none" opacity="1" stroke="#fc820c" stroke-dasharray="6 6" stroke-width="2"></path>
|
||||
<path class="connection-line" d="M 520 450 C 600 450, 700 390, 750 390" fill="none" opacity="0.2" stroke="#2f8635" stroke-dasharray="6 6" stroke-width="2"></path>
|
||||
</svg>
|
||||
<!-- Integrated Document Container -->
|
||||
<div class="absolute left-12 top-12 bottom-12 w-[600px] bg-surface-container-lowest border border-outline-variant rounded-xl shadow-md overflow-y-auto z-20 cursor-text">
|
||||
<!-- Floating Toolbar (Attached to document) -->
|
||||
<div class="sticky top-6 left-1/2 -translate-x-1/2 w-max floating-toolbar bg-surface/95 border border-outline-variant rounded-full px-md py-sm flex items-center gap-sm z-30 mb-8 mt-6 mx-auto">
|
||||
<button class="p-xs text-primary rounded hover:bg-primary/10 transition-colors tooltip-trigger" title="Highlight">
|
||||
<span class="material-symbols-outlined text-[20px]">format_ink_highlighter</span>
|
||||
</button>
|
||||
<button class="p-xs text-on-surface-variant rounded hover:bg-surface-container-highest transition-colors tooltip-trigger" title="Underline">
|
||||
<span class="material-symbols-outlined text-[20px]">format_underlined</span>
|
||||
</button>
|
||||
<button class="p-xs text-on-surface-variant rounded hover:bg-surface-container-highest transition-colors tooltip-trigger" title="Add Note">
|
||||
<span class="material-symbols-outlined text-[20px]">add_comment</span>
|
||||
</button>
|
||||
<div class="w-px h-5 bg-outline-variant mx-xs"></div>
|
||||
<button class="w-5 h-5 rounded-full bg-secondary-container border border-outline-variant hover:scale-110 transition-transform ring-2 ring-offset-1 ring-secondary-container"></button>
|
||||
<button class="w-5 h-5 rounded-full bg-tertiary-container border border-outline-variant hover:scale-110 transition-transform"></button>
|
||||
<button class="w-5 h-5 rounded-full bg-primary border border-outline-variant hover:scale-110 transition-transform"></button>
|
||||
</div>
|
||||
<!-- Text Document -->
|
||||
<div class="px-2xl pb-2xl">
|
||||
<h2 class="text-center font-headline-lg text-headline-lg mb-xl text-on-surface">《秋天》</h2>
|
||||
<div class="text-reading-chinese text-[28px] text-on-surface">
|
||||
<p class="mb-lg indent-8">
|
||||
天气凉了,树叶黄了,一片片叶子从树上落下来。
|
||||
<span class="relative inline-block cursor-pointer group">
|
||||
<span class="annotation-highlight-yellow active px-1 rounded" id="highlight-1">天空那么蓝,那么高。</span>
|
||||
<!-- Connection Anchor -->
|
||||
</span></p><div class="absolute -right-2 top-1/2 w-2 h-2 rounded-full bg-secondary-container opacity-100"></div>
|
||||
<p></p>
|
||||
<p class="mb-lg indent-8">
|
||||
一群大雁往南飞,一会儿排成个“人”字,一会儿排成个“一”字。
|
||||
<span class="relative inline-block cursor-pointer group">
|
||||
<span class="annotation-highlight-green px-1 rounded" id="highlight-2">啊!秋天来了!</span>
|
||||
<!-- Connection Anchor -->
|
||||
</span></p><div class="absolute -right-2 top-1/2 w-2 h-2 rounded-full bg-tertiary-container opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
||||
<p></p>
|
||||
</div>
|
||||
<!-- UI Hint for Interaction -->
|
||||
<div class="mt-2xl pt-lg border-t border-outline-variant/30 flex items-center justify-center gap-sm text-on-surface-variant/70 font-label-md text-sm">
|
||||
<span class="material-symbols-outlined text-[18px]">lightbulb</span>
|
||||
<span>Select text and hold <kbd class="bg-surface-container px-1.5 py-0.5 rounded border border-outline-variant font-code-md text-xs">Shift</kbd> to create a new node</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Nodes on Canvas (Positioned to the right) -->
|
||||
<div class="absolute top-[200px] left-[750px] w-[280px] bg-surface rounded-lg border-2 border-secondary-container shadow-md transition-shadow cursor-pointer group z-20" id="node-1">
|
||||
<div class="bg-secondary-fixed-dim/30 px-3 py-2 rounded-t-sm border-b border-secondary-container/50 flex justify-between items-center">
|
||||
<span class="text-on-secondary-fixed-variant font-label-md text-label-md font-bold">Language Feature</span>
|
||||
<button class="text-on-secondary-fixed-variant hover:text-on-surface transition-colors"><span class="material-symbols-outlined text-[16px]">more_horiz</span></button>
|
||||
</div>
|
||||
<div class="p-4 relative">
|
||||
<!-- Connection Anchor -->
|
||||
<div class="absolute -left-2 top-[30px] w-4 h-4 rounded-full bg-surface border-2 border-secondary-container"></div>
|
||||
<p class="font-body-md text-on-surface text-sm italic mb-3">"天空那么蓝,那么高。"</p>
|
||||
<div class="text-xs text-on-surface-variant border-t border-outline-variant/30 pt-2">
|
||||
<span class="font-bold text-on-surface">Note:</span> Focus on repetition of "那么".
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute top-[350px] left-[750px] w-[280px] bg-surface rounded-lg border border-tertiary-container/50 shadow-sm transition-all hover:shadow-md hover:border-tertiary-container cursor-pointer group z-20 opacity-80 hover:opacity-100" id="node-2">
|
||||
<div class="bg-tertiary-fixed-dim/20 px-3 py-2 rounded-t-sm border-b border-outline-variant flex justify-between items-center">
|
||||
<span class="text-on-tertiary-fixed-variant font-label-md text-label-md">Action Suggestion</span>
|
||||
<button class="text-outline hover:text-on-surface transition-colors"><span class="material-symbols-outlined text-[16px]">more_horiz</span></button>
|
||||
</div>
|
||||
<div class="p-4 relative">
|
||||
<!-- Connection Anchor -->
|
||||
<div class="absolute -left-2 top-[30px] w-4 h-4 rounded-full bg-surface border-2 border-outline-variant group-hover:border-tertiary-container transition-colors"></div>
|
||||
<p class="font-body-md text-on-surface text-sm italic">"啊!秋天来了!"</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Floating Detail/Parameter Panel (Right side) -->
|
||||
<div class="absolute top-md right-md w-[320px] bg-surface/90 backdrop-blur-md rounded-2xl p-lg shadow-[0_8px_32px_rgba(0,0,0,0.08)] border border-outline-variant/50 flex flex-col gap-md z-30">
|
||||
<div class="flex justify-between items-start">
|
||||
<span class="bg-secondary-fixed-dim/40 text-on-secondary-fixed-variant font-label-md text-label-md px-2 py-1 rounded-sm border border-secondary-container/20">Language Feature</span>
|
||||
<button class="text-outline hover:text-on-surface transition-colors"><span class="material-symbols-outlined text-[18px]">close</span></button>
|
||||
</div>
|
||||
<div class="bg-surface-container-lowest p-3 rounded-lg border border-outline-variant/50">
|
||||
<p class="font-body-md text-body-md text-on-surface italic">"天空那么蓝,那么高。"</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-xs">
|
||||
<label class="font-label-md text-label-md text-on-surface-variant uppercase tracking-wider">Instructional Notes</label>
|
||||
<p class="font-body-md text-body-md text-on-surface">Focus on the repetition of "那么" (so) to emphasize the vastness of the autumn sky. Guide students to read with a prolonged, airy tone.</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-xs mt-auto pt-sm border-t border-outline-variant/30">
|
||||
<label class="font-label-md text-label-md text-on-surface-variant uppercase tracking-wider">Tags</label>
|
||||
<div class="flex gap-xs flex-wrap">
|
||||
<span class="bg-surface-container border border-outline-variant text-on-surface-variant font-label-md text-[10px] px-2 py-1 rounded-full">朗读指导</span>
|
||||
<button class="bg-transparent border border-dashed border-outline-variant text-on-surface-variant hover:text-primary hover:border-primary font-label-md text-[10px] px-2 py-1 rounded-full transition-colors flex items-center gap-1">
|
||||
<span class="material-symbols-outlined text-[12px]">add</span> Add Tag
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body></html>
|
||||
608
docs/feature/f_bk_design.md
Normal file
608
docs/feature/f_bk_design.md
Normal file
@@ -0,0 +1,608 @@
|
||||
# 备课模块(lesson-preparation)设计文档
|
||||
|
||||
> 配套原型:`docs/feature/f_bk.md`
|
||||
> 架构依据:`docs/architecture/004_architecture_impact_map.md`、`005_architecture_data.json`
|
||||
> 编写日期:2026-06-18
|
||||
> 范围:**P0 地基 + P1 联动**(P2 协作 / P3 AI 与学情回看留作后续 spec)
|
||||
|
||||
---
|
||||
|
||||
## 0. 关键决策摘要
|
||||
|
||||
| 决策项 | 最终选择 | 理由 |
|
||||
|--------|----------|------|
|
||||
| 本次 spec 范围 | P0 + P1 | 构成"备课→出题→下发"最小可用闭环,体量适中 |
|
||||
| 编辑器形态 | Block 编辑器为主 | 与蓝图文字一致;设计稿的"课文+节点画布"作为 `text_study` block 的内部交互 |
|
||||
| Block 存储模型 | 方案 A:JSON 文档 + 版本快照表 | 与现有 `questions.content` / `homeworkAssignments.structure` 的 JSON 模式一致;为 P2 批注 / P3 AI 重写预留稳定 blockId 锚点;零跨模块 DB 访问 |
|
||||
| 知识点同步 | P1 仅"关联已有 + AI 推荐",不回写教材树 | 避免 P1 引入审核流拖慢闭环;回写留作后续 spec |
|
||||
| 作业发布闭环 | 复用 exam 中转 | 课案练习块 → 打包成 exam 草稿 → 调用现有 `createHomeworkAssignmentAction` 下发;零 schema 侵入、溯源清晰(作业→exam→课案) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 模块定位与边界
|
||||
|
||||
### 1.1 模块名
|
||||
|
||||
`lesson-preparation`(目录 `src/modules/lesson-preparation/`),中文"备课"。
|
||||
|
||||
### 1.2 与 `course-plans` 的关系(互补,不合并)
|
||||
|
||||
| 模块 | 粒度 | 回答的问题 |
|
||||
|------|------|-----------|
|
||||
| `course-plans` | 学期/周宏观排课(totalHours/weeklyHours/week/topic) | "这学期每周教什么" |
|
||||
| `lesson-preparation` | 具体一节课的教学设计(目标/重难点/导入/新授/练习/作业…) | "这节课怎么教" |
|
||||
|
||||
软关联:课案可记录来源 `coursePlanItemId`(可空,无强外键),便于"周计划→具体课案"下钻。
|
||||
|
||||
### 1.3 依赖关系(严格三层架构,零跨模块直查)
|
||||
|
||||
- 依赖 `shared/*`、`@/auth`
|
||||
- 通过对方 data-access 通信(不直接查询对方表):
|
||||
- `textbooks` — 只读章节树 / 知识点树
|
||||
- `questions` — 创建题目(含知识点关联)、查询题目
|
||||
- `exams` — 创建 exam 草稿(用于发布中转)
|
||||
- `homework` — 创建作业下发到班级
|
||||
- `classes` — 查询教师班级(用于下发目标选择)
|
||||
- `files` — 附件引用
|
||||
- 被依赖:P0/P1 阶段无被依赖方
|
||||
|
||||
---
|
||||
|
||||
## 2. 数据模型(新增 3 张表)
|
||||
|
||||
### 2.1 `lesson_plans`(课案主表)
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | id | PK | CUID2 |
|
||||
| title | varchar(255) | notNull | 课案标题 |
|
||||
| textbookId | varchar(128) | FK→textbooks, nullable | 教材(允许非教材备课) |
|
||||
| chapterId | varchar(128) | FK→chapters, nullable | 章节 |
|
||||
| coursePlanItemId | varchar(128) | nullable, 无 FK | 软关联课程计划项 |
|
||||
| subjectId | varchar(128) | FK→subjects, nullable | 学科 |
|
||||
| gradeId | varchar(128) | FK→grades, nullable | 年级 |
|
||||
| templateId | varchar(128) | nullable | 使用的模板 ID |
|
||||
| templateName | varchar(100) | nullable | 模板名快照(防模板改名) |
|
||||
| content | json | notNull | block 文档 JSON(见 §3) |
|
||||
| status | varchar(50) | default 'draft' | `draft`/`published`/`archived` |
|
||||
| creatorId | varchar(128) | FK→users, notNull | 创建者 |
|
||||
| lastSavedAt | timestamp | nullable | 最后自动保存时间 |
|
||||
| createdAt | timestamp | defaultNow | |
|
||||
| updatedAt | timestamp | defaultNow onUpdateNow | |
|
||||
|
||||
索引:`creatorIdx(creatorId)`、`statusIdx(status)`、`textbookChapterIdx(textbookId, chapterId)`、`subjectGradeIdx(subjectId, gradeId)`
|
||||
|
||||
### 2.2 `lesson_plan_versions`(版本快照表)
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | id | PK | |
|
||||
| planId | varchar(128) | FK→lesson_plans, onDelete cascade | |
|
||||
| versionNo | int | notNull | 每 plan 内自增 |
|
||||
| label | varchar(100) | nullable | 手动保存时的标签 |
|
||||
| content | json | notNull | 该版本 content 快照 |
|
||||
| isAuto | boolean | default false | true=自动保存触发 |
|
||||
| creatorId | varchar(128) | FK→users, notNull | |
|
||||
| createdAt | timestamp | defaultNow | |
|
||||
|
||||
索引:`planVersionIdx(planId, versionNo)`(唯一)、`planCreatedIdx(planId, createdAt desc)`
|
||||
|
||||
### 2.3 `lesson_plan_templates`(模板表)
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | id | PK | |
|
||||
| name | varchar(100) | notNull | 模板名 |
|
||||
| type | varchar(50) | notNull | `system`/`personal` |
|
||||
| scope | varchar(50) | notNull | `regular`/`review`/`experiment`/`inquiry`/`blank`/`custom` |
|
||||
| blocks | json | notNull | 预置 block 骨架(blockType + 默认标题 + 提示语,无内容) |
|
||||
| creatorId | varchar(128) | FK→users, nullable | personal 模板拥有者;system 为 null |
|
||||
| createdAt | timestamp | defaultNow | |
|
||||
| updatedAt | timestamp | defaultNow onUpdateNow | |
|
||||
|
||||
索引:`typeCreatorIdx(type, creatorId)`(personal 模板按创建者过滤)
|
||||
|
||||
> 系统预设 4+1 套模板由 seed 脚本写入(type=system)。教师"另存为我的模板"写入 type=personal。
|
||||
|
||||
---
|
||||
|
||||
## 3. Block 文档 JSON 结构
|
||||
|
||||
### 3.1 顶层结构
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"blocks": [
|
||||
{
|
||||
"id": "blk_xxx",
|
||||
"type": "objective",
|
||||
"title": "教学目标",
|
||||
"data": { },
|
||||
"order": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `id`:客户端生成的稳定 ID(CUID2),是 P2 批注锚点、P3 AI 单 block 重写的定位依据
|
||||
- `type`:见 §3.2 枚举
|
||||
- `title`:环节名(可改,模板提供默认值)
|
||||
- `data`:类型相关数据,见 §3.3
|
||||
- `order`:排序索引(整数,编辑器拖拽后重排)
|
||||
|
||||
### 3.2 Block 类型枚举
|
||||
|
||||
| type | 用途 | 出现模板 |
|
||||
|------|------|---------|
|
||||
| `objective` | 教学目标 | 常规/复习/实验 |
|
||||
| `key_point` | 教学重难点 | 常规 |
|
||||
| `import` | 导入 | 常规 |
|
||||
| `new_teaching` | 新授 | 常规 |
|
||||
| `consolidation` | 巩固 | 常规 |
|
||||
| `summary` | 小结 | 常规/复习/实验/探究 |
|
||||
| `homework` | 作业布置(文字描述型) | 常规 |
|
||||
| `blackboard` | 板书设计 | 常规 |
|
||||
| `text_study` | 文本研习(设计稿画布形态) | 语文/英语精读课自定义添加 |
|
||||
| `exercise` | 练习/作业块(P1 核心,关联题目) | 任意模板可添加 |
|
||||
| `rich_text` | 通用富文本(自定义环节) | 复习/实验/探究的自定义环节 |
|
||||
| `reflection` | 教学反思(P3 预留,P0/P1 不渲染特殊 UI) | 任意 |
|
||||
|
||||
### 3.3 各 block.data 结构
|
||||
|
||||
**富文本类**(`objective`/`key_point`/`import`/`new_teaching`/`consolidation`/`summary`/`homework`/`blackboard`/`rich_text`/`reflection`):
|
||||
|
||||
```json
|
||||
{
|
||||
"html": "<p>...</p>",
|
||||
"knowledgePointIds": ["kp_1", "kp_2"]
|
||||
}
|
||||
```
|
||||
|
||||
**`text_study`**(设计稿画布形态的 block 化):
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceText": "天气凉了,树叶黄了...",
|
||||
"annotations": [
|
||||
{
|
||||
"id": "ann_xxx",
|
||||
"anchor": { "start": 12, "end": 20 },
|
||||
"nodeType": "language_feature",
|
||||
"title": "语言特色",
|
||||
"note": "关注'那么'的反复",
|
||||
"color": "yellow"
|
||||
}
|
||||
],
|
||||
"knowledgePointIds": ["kp_1"]
|
||||
}
|
||||
```
|
||||
|
||||
**`exercise`**(P1 核心):
|
||||
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"questionId": "q_xxx",
|
||||
"source": "bank",
|
||||
"score": 5,
|
||||
"order": 0
|
||||
},
|
||||
{
|
||||
"questionId": "inline_draft_xxx",
|
||||
"source": "inline",
|
||||
"inlineContent": {
|
||||
"content": { },
|
||||
"type": "single_choice",
|
||||
"difficulty": 3,
|
||||
"knowledgePointIds": ["kp_1"]
|
||||
},
|
||||
"score": 10,
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"purpose": "class_practice",
|
||||
"knowledgePointIds": ["kp_1"]
|
||||
}
|
||||
```
|
||||
|
||||
- `source`:`bank`=从题库拉取(questionId 已存在于 questions 表);`inline`=课案内新建(编辑期 questionId 为占位 `inline_draft_${cuid}`,发布时入库后回填真实 ID)
|
||||
- `inlineContent`:仅 source=inline 时存在,结构与 `questions` 表字段对齐(content/type/difficulty/knowledgePointIds),发布时作为 `createQuestionWithRelations` 的入参
|
||||
- `purpose`:`class_practice`=课堂练习;`after_class_homework`=课后作业(发布闭环仅处理此类型)
|
||||
|
||||
---
|
||||
|
||||
## 4. 模板系统
|
||||
|
||||
### 4.1 系统预设模板(4+1 套,由 seed 脚本写入)
|
||||
|
||||
| 模板 | scope | block 序列 |
|
||||
|------|-------|-----------|
|
||||
| 常规课 | `regular` | objective → key_point → import → new_teaching → consolidation → summary → homework → blackboard |
|
||||
| 复习课 | `review` | objective → rich_text("知识网络梳理") → rich_text("典型例题精讲") → rich_text("变式训练") → exercise("当堂检测") → summary |
|
||||
| 实验课 | `experiment` | objective → rich_text("器材准备") → rich_text("实验步骤") → rich_text("观察记录表") → rich_text("交流讨论") → summary |
|
||||
| 探究课 | `inquiry` | rich_text("情境导入") → rich_text("问题驱动") → rich_text("小组探究") → rich_text("成果展示") → rich_text("归纳提升") |
|
||||
| 空白 | `blank` | (无预置 block) |
|
||||
|
||||
模板 `blocks` JSON 仅定义骨架(type + 默认 title + 提示语),不含内容。教师选用后生成对应 block 序列,可自由增删、改序、改名、改内容。
|
||||
|
||||
### 4.2 自定义模板
|
||||
|
||||
- 教师在编辑器内"另存为我的模板"→ 写入 `lesson_plan_templates`(type=personal, creatorId=教师)
|
||||
- personal 模板仅创建者可见、可编辑、可删除
|
||||
- 创建课案时模板选择器并列展示 system + 我的 personal 模板
|
||||
|
||||
---
|
||||
|
||||
## 5. Block 编辑器与版本管理
|
||||
|
||||
### 5.1 编辑器交互
|
||||
|
||||
- 主体:可拖拽 block 列表(类 Notion/BlockNote 的块状编辑器)
|
||||
- 每个 block:标题栏(可改名)+ 内容区(按 type 渲染不同编辑组件)+ 拖拽手柄 + 删除/上移/下移/复制
|
||||
- block 增删:顶部"+"按钮选择 block 类型插入;可从模板侧栏拖入预置环节
|
||||
- 自动保存:编辑器 debounce 3s 无操作后触发自动保存(写 `lesson_plans.content` + `lastSavedAt`,不生成版本)
|
||||
- 手动保存:Ctrl+S 或按钮 → 生成新版本(写 `lesson_plan_versions`,isAuto=false)
|
||||
- 版本历史:侧栏抽屉展示版本列表(versionNo + label + 时间 + isAuto 标记),点击预览该版本 content,"回退到此版本"= 用该版本 content 覆盖当前 + 生成新版本
|
||||
|
||||
### 5.2 版本策略
|
||||
|
||||
- 自动保存:只更新 `lesson_plans.content` 与 `lastSavedAt`,**不**写 versions 表(避免版本爆炸)
|
||||
- 手动保存:写一条 versions 记录(isAuto=false)
|
||||
- 定时自动版本:每 30 分钟若有过改动,自动写一条 versions 记录(isAuto=true),防止教师长时间未手动保存丢失历史
|
||||
- 版本上限:每 plan 保留最近 50 条 versions,超出删除最旧的 isAuto=true 记录(手动版本永不被自动清理)
|
||||
|
||||
### 5.3 我的课案库
|
||||
|
||||
- 路由:`/teacher/lesson-plans`
|
||||
- 列表展示:卡片网格,显示 title / 教材章节 / 学科年级 / 模板 / status / 最后保存时间
|
||||
- 筛选:教材(级联章节)、学科、年级、状态、标签(标题关键词搜索)
|
||||
- 操作:编辑、复制(生成副本,title 加" - 副本")、删除(软删除:status=archived)、发布(status=published)
|
||||
|
||||
---
|
||||
|
||||
## 6. P1:知识点标注与关联
|
||||
|
||||
### 6.1 手动标注
|
||||
|
||||
- 在富文本类 block 内选中文本 → 弹出知识点选择器(从 `textbooks` 模块的章节-知识点树勾选)
|
||||
- 选中的 knowledgePointId 写入该 block 的 `data.knowledgePointIds`
|
||||
- block 渲染时在关联的知识点旁显示标签 chip
|
||||
|
||||
### 6.2 AI 推荐(轻量,P1 不做完整 AI 课案生成)
|
||||
|
||||
- 编辑器顶部"AI 推荐知识点"按钮 → 读取当前课案所有 block 的纯文本 → 调用 `shared/lib/ai.createAiChatCompletion` → 返回推荐 knowledgePointId 列表
|
||||
- 教师在弹窗中勾选确认 → 合并到对应 block 的 `knowledgePointIds`
|
||||
- AI 仅做"推荐候选",不自动写入;知识点池来自教材已有知识点(不创建新知识点)
|
||||
|
||||
### 6.3 知识点-课案映射查询(data-access 暴露)
|
||||
|
||||
- `getLessonPlansByKnowledgePoint(knowledgePointId)`:反查哪些课案重点讲解了某知识点(供后续学情分析/教材知识点树反查使用,P1 仅实现 data-access 函数,不做 UI)
|
||||
|
||||
---
|
||||
|
||||
## 7. P1:题目创建 / 拉取 / 同步题库
|
||||
|
||||
### 7.1 从题库拉取(source=bank)
|
||||
|
||||
- exercise block 侧栏:题库搜索器(按知识点 / 题型 / 难度筛选,调用 `questions/data-access.getQuestions`)
|
||||
- 选中题目 → 插入 exercise.items(source=bank, questionId=真实 ID)
|
||||
- 仅引用,不复制题目内容;渲染时按 questionId 查询展示
|
||||
|
||||
### 7.2 课案内新建题目(source=inline)
|
||||
|
||||
- exercise block 内"新建题目"按钮 → 弹出题目编辑器(复用 `questions/components/create-question-dialog` 的表单逻辑)
|
||||
- 编辑期:题目暂存为 inline draft,完整内容存入 `exercise.items[].inlineContent`(结构与 questions 表字段对齐),`questionId` 为占位 `inline_draft_${cuid}`
|
||||
- 保存课案时:inline 题目**不立即入库**,保持 draft 状态(inlineContent 随课案 content 一起持久化)
|
||||
- 发布作业时(见 §8):inline 题目先入库(调用 `questions/data-access.createQuestionWithRelations`,入参取自 inlineContent),用真实 questionId 替换占位 ID,回写到课案 content
|
||||
|
||||
### 7.3 题目-课案关联查询
|
||||
|
||||
- `getLessonPlansByQuestion(questionId)`:反查某题在哪些课案的哪个 exercise block 被使用(data-access 函数,P1 仅实现,不做 UI)
|
||||
|
||||
---
|
||||
|
||||
## 8. P1:作业 / 考试发布打通(复用 exam 中转)
|
||||
|
||||
### 8.1 发布流程
|
||||
|
||||
```
|
||||
教师点击 exercise block(purpose=after_class_homework)的"发布作业"按钮
|
||||
│
|
||||
▼
|
||||
[Action] publishLessonPlanHomeworkAction
|
||||
│
|
||||
├─ requirePermission(LESSON_PLAN_PUBLISH)
|
||||
│
|
||||
├─ 1. inline 题目入库
|
||||
│ └─ 遍历 exercise.items,对 source=inline 的调用
|
||||
│ questions/data-access.createQuestionWithRelations
|
||||
│ (authorId=教师,关联 knowledgePointIds)
|
||||
│ └─ 用真实 questionId 替换课案 content 中的占位 ID
|
||||
│ └─ 更新 lesson_plans.content
|
||||
│
|
||||
├─ 2. 打包成 exam 草稿
|
||||
│ └─ 调用 exams/data-access.persistExamDraft
|
||||
│ (title=课案标题+" - 作业",creatorId=教师,
|
||||
│ sourceLessonPlanId=课案ID,关联 textbookId/chapterId/subjectId/gradeId,
|
||||
│ examQuestions = exercise.items 映射)
|
||||
│ └─ 得到 examId
|
||||
│
|
||||
├─ 3. 下发作业
|
||||
│ └─ 调用 homework/data-access-write.createHomeworkAssignment
|
||||
│ (sourceExamId=examId, title, targets=班级学生列表,
|
||||
│ availableAt, dueAt)
|
||||
│ └─ 得到 assignmentId
|
||||
│
|
||||
├─ 4. 记录溯源
|
||||
│ └─ 在课案 content 的 exercise block.data 写入
|
||||
│ publishedAssignmentId + publishedExamId + publishedAt
|
||||
│
|
||||
└─ revalidatePath("/teacher/lesson-plans") + revalidatePath("/teacher/homework")
|
||||
```
|
||||
|
||||
### 8.2 溯源标记
|
||||
|
||||
- 课案 exercise block 渲染时:若 `data.publishedAssignmentId` 存在,显示"已发布为作业"徽章 + 跳转链接
|
||||
- 作业侧(P1 不改 homework 模块):作业的 sourceExamId → exam → 可查到 sourceLessonPlanId(exam 草稿创建时记录),实现"作业→课案"反查链路
|
||||
- 学情报告(P3):通过 assignmentId → exam → lessonPlanId 回链到课案
|
||||
|
||||
### 8.3 发布前置校验
|
||||
|
||||
- exercise block 至少有 1 道题
|
||||
- inline 题目必须填写完整(content/type/difficulty/knowledgePointIds)
|
||||
- 教师必须对目标班级有 `class_taught` DataScope 权限
|
||||
- 同一 exercise block 不可重复发布(已有 publishedAssignmentId 则禁用发布按钮,提供"重新发布为新作业"选项)
|
||||
|
||||
---
|
||||
|
||||
## 9. 模块文件结构
|
||||
|
||||
```
|
||||
src/modules/lesson-preparation/
|
||||
├─ actions.ts # Server Actions(编排层)
|
||||
├─ data-access.ts # 课案 CRUD + 版本查询
|
||||
├─ data-access-versions.ts # 版本快照写入 + 查询 + 回退
|
||||
├─ data-access-templates.ts # 模板 CRUD(system + personal)
|
||||
├─ data-access-knowledge.ts # 知识点-课案映射查询(P1)
|
||||
├─ publish-service.ts # 发布编排(inline 入库 → exam 草稿 → 作业下发)
|
||||
├─ ai-suggest.ts # AI 知识点推荐(P1 轻量)
|
||||
├─ schema.ts # Zod 验证
|
||||
├─ types.ts # 类型定义(含 Block 类型联合)
|
||||
├─ constants.ts # 模板预设、block 类型枚举、状态常量
|
||||
├─ seed.ts # 系统预设模板 seed 脚本
|
||||
├─ hooks/
|
||||
│ └─ use-lesson-plan-editor.ts # 编辑器状态管理(自动保存/版本/拖拽)
|
||||
└─ components/
|
||||
├─ lesson-plan-list.tsx # 我的课案库列表
|
||||
├─ lesson-plan-card.tsx # 课案卡片
|
||||
├─ lesson-plan-filters.tsx # 筛选器
|
||||
├─ lesson-plan-editor.tsx # 编辑器主壳(block 列表容器)
|
||||
├─ block-renderer.tsx # block 分发渲染
|
||||
├─ blocks/
|
||||
│ ├─ rich-text-block.tsx # 富文本类 block 编辑器
|
||||
│ ├─ text-study-block.tsx # 文本研习画布 block
|
||||
│ ├─ exercise-block.tsx # 练习/作业 block
|
||||
│ └─ reflection-block.tsx # 教学反思(P3 预留,P1 简单渲染)
|
||||
├─ template-picker.tsx # 模板选择器
|
||||
├─ version-history-drawer.tsx # 版本历史抽屉
|
||||
├─ knowledge-point-picker.tsx # 知识点选择器(复用 textbooks 组件)
|
||||
├─ question-bank-picker.tsx # 题库拉取侧栏(复用 questions 组件)
|
||||
├─ inline-question-editor.tsx # 课案内新建题目(复用 questions 表单)
|
||||
└─ publish-homework-dialog.tsx # 发布作业弹窗(选班级/时间)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Server Actions 清单
|
||||
|
||||
所有 Action 遵循项目规范:`requirePermission()` → Zod 校验 → 调用 data-access → `revalidatePath` → 返回 `ActionState<T>`。
|
||||
|
||||
| Action | 权限点 | 用途 |
|
||||
|--------|--------|------|
|
||||
| `getLessonPlansAction` | LESSON_PLAN_READ | 我的课案库列表(含筛选) |
|
||||
| `getLessonPlanByIdAction` | LESSON_PLAN_READ | 获取单个课案(含权限校验:creator 或 published) |
|
||||
| `createLessonPlanAction` | LESSON_PLAN_CREATE | 创建课案(选模板 → 生成初始 content) |
|
||||
| `updateLessonPlanAction` | LESSON_PLAN_UPDATE | 更新课案(自动保存,不生成版本) |
|
||||
| `saveLessonPlanVersionAction` | LESSON_PLAN_UPDATE | 手动保存生成版本 |
|
||||
| `revertLessonPlanVersionAction` | LESSON_PLAN_UPDATE | 回退到指定版本(生成新版本) |
|
||||
| `getLessonPlanVersionsAction` | LESSON_PLAN_READ | 获取版本列表 |
|
||||
| `deleteLessonPlanAction` | LESSON_PLAN_DELETE | 删除课案(软删除:status=archived) |
|
||||
| `duplicateLessonPlanAction` | LESSON_PLAN_CREATE | 复制课案 |
|
||||
| `getLessonPlanTemplatesAction` | LESSON_PLAN_READ | 获取模板列表(system + 我的 personal) |
|
||||
| `saveAsTemplateAction` | LESSON_PLAN_CREATE | 另存为我的模板 |
|
||||
| `deleteTemplateAction` | LESSON_PLAN_DELETE | 删除 personal 模板 |
|
||||
| `suggestKnowledgePointsAction` | LESSON_PLAN_READ + AI_CHAT | AI 推荐知识点(只读返回候选,不写入课案;教师确认后另调 updateLessonPlanAction) |
|
||||
| `publishLessonPlanHomeworkAction` | LESSON_PLAN_PUBLISH + HOMEWORK_CREATE | 发布作业(§8 编排) |
|
||||
|
||||
---
|
||||
|
||||
## 11. data-access 清单
|
||||
|
||||
| 函数 | 用途 |
|
||||
|------|------|
|
||||
| `getLessonPlans(params & scope)` | 课案列表(DataScope 过滤) |
|
||||
| `getLessonPlanById(id, scope)` | 单课案详情 |
|
||||
| `createLessonPlan(input)` | 创建(含模板初始化 content) |
|
||||
| `updateLessonPlanContent(id, content, userId)` | 更新 content + lastSavedAt(自动保存) |
|
||||
| `softDeleteLessonPlan(id, userId)` | status=archived |
|
||||
| `duplicateLessonPlan(id, userId)` | 复制 |
|
||||
| `getLessonPlanVersions(planId)` | 版本列表 |
|
||||
| `createLessonPlanVersion(planId, content, userId, isAuto, label?)` | 写版本快照 |
|
||||
| `revertToVersion(planId, versionNo, userId)` | 用版本 content 覆盖当前 + 生成新版本 |
|
||||
| `pruneAutoVersions(planId, keep=50)` | 清理超出上限的自动版本 |
|
||||
| `getLessonPlanTemplates(userId)` | system + 该用户 personal |
|
||||
| `createPersonalTemplate(input, userId)` | 创建 personal 模板 |
|
||||
| `deletePersonalTemplate(id, userId)` | 删除(仅 owner) |
|
||||
| `getLessonPlansByKnowledgePoint(kpId)` | 知识点反查课案(P1 data-access only) |
|
||||
| `getLessonPlansByQuestion(questionId)` | 题目反查课案(P1 data-access only) |
|
||||
|
||||
---
|
||||
|
||||
## 12. 权限点(新增 5 个)
|
||||
|
||||
在 `src/shared/types/permissions.ts` 新增:
|
||||
|
||||
```typescript
|
||||
// Lesson Plan (备课)
|
||||
LESSON_PLAN_CREATE: "lesson_plan:create",
|
||||
LESSON_PLAN_READ: "lesson_plan:read",
|
||||
LESSON_PLAN_UPDATE: "lesson_plan:update",
|
||||
LESSON_PLAN_DELETE: "lesson_plan:delete",
|
||||
LESSON_PLAN_PUBLISH: "lesson_plan:publish",
|
||||
```
|
||||
|
||||
在 `src/shared/lib/permissions.ts` 的 `ROLE_PERMISSIONS` 映射中:
|
||||
- `teacher`:全部 5 个
|
||||
- `admin`:全部 5 个
|
||||
- `student`/`parent`/其他:无
|
||||
|
||||
---
|
||||
|
||||
## 13. 路由
|
||||
|
||||
| 路由 | 页面 | 权限 |
|
||||
|------|------|------|
|
||||
| `/teacher/lesson-plans` | 我的课案库列表 | LESSON_PLAN_READ |
|
||||
| `/teacher/lesson-plans/new` | 新建课案(选模板) | LESSON_PLAN_CREATE |
|
||||
| `/teacher/lesson-plans/[planId]/edit` | 课案编辑器 | LESSON_PLAN_UPDATE(creator)或 LESSON_PLAN_READ(published 只读) |
|
||||
|
||||
侧边栏导航:在 `layout/config/navigation.ts` 的 teacher 角色菜单新增"备课"项。
|
||||
|
||||
---
|
||||
|
||||
## 14. DataScope 接入
|
||||
|
||||
- `getLessonPlans` 接受 `scope` 参数:
|
||||
- `teacher`/`admin`(type=all 或 class_taught):返回自己创建的 + 公开 published 的
|
||||
- 其他角色:仅 published 的
|
||||
- `getLessonPlanById`:creator 可看自己的 draft;非 creator 仅当 status=published 可看
|
||||
- 写操作(update/delete/publish):仅 creator(DataScope 不适用,直接校验 `creatorId === userId`)
|
||||
|
||||
---
|
||||
|
||||
## 15. 架构图同步计划
|
||||
|
||||
按项目规则"改码必同步图",实现完成后需更新:
|
||||
|
||||
### 15.1 `docs/architecture/004_architecture_impact_map.md`
|
||||
|
||||
- §1.1 分层架构图:modules 行新增 `lesson-preparation`
|
||||
- §1.2 模块依赖关系图:新增 `lesson-preparation` 节点,标注对 textbooks/questions/exams/homework/classes/files 的合理依赖(───▶ data-access)
|
||||
- 第二部分新增 §2.27 lesson-preparation 模块清单(职责/导出函数/依赖/文件清单)
|
||||
- 附录 A 依赖矩阵新增一行一列
|
||||
|
||||
### 15.2 `docs/architecture/005_architecture_data.json`
|
||||
|
||||
- `modules.lesson_preparation`:完整模块节点
|
||||
- `dbTables`:新增 `lesson_plans` / `lesson_plan_versions` / `lesson_plan_templates`
|
||||
- `permissions`:新增 5 个权限点
|
||||
- `routes`:新增 3 个路由
|
||||
- `dependencyMatrix`:新增依赖关系
|
||||
|
||||
### 15.3 `src/shared/db/schema.ts`
|
||||
|
||||
新增 3 张表定义(按现有分节风格,加在合适 section)。schema.ts 当前 1111 行已超 1000 硬上限(P0 已知问题),新增 3 表会加剧;建议本次新增时一并按业务域拆分 schema.ts(但拆分属独立任务,不在本 spec 范围,仅在备注中提示)。
|
||||
|
||||
---
|
||||
|
||||
## 16. 实施分阶段计划
|
||||
|
||||
### P0 地基(先做)
|
||||
|
||||
1. 新增 3 张表 schema + 迁移
|
||||
2. 新增 5 个权限点 + 角色映射
|
||||
3. seed 系统预设模板(4+1 套)
|
||||
4. data-access + data-access-versions + data-access-templates
|
||||
5. 基础 CRUD Actions(create/get/update/delete/duplicate)
|
||||
6. 版本管理 Actions(save version / revert / list)
|
||||
7. 模板 Actions(list / save as / delete)
|
||||
8. 我的课案库列表页 + 筛选
|
||||
9. Block 编辑器主壳 + 富文本类 block + 拖拽排序 + 自动保存
|
||||
10. 版本历史抽屉
|
||||
11. 模板选择器(新建课案入口)
|
||||
12. 路由 + 侧边栏导航
|
||||
13. 同步架构图 004/005
|
||||
|
||||
### P1 联动(P0 完成后)
|
||||
|
||||
14. `text_study` block(设计稿画布形态)
|
||||
15. `exercise` block + 题库拉取侧栏(source=bank)
|
||||
16. `exercise` block + 课案内新建题目(source=inline,draft 暂存)
|
||||
17. 知识点选择器 + block 内 knowledgePointIds 标注
|
||||
18. AI 知识点推荐 Action + 编辑器入口
|
||||
19. publish-service(inline 入库 → exam 草稿 → 作业下发)
|
||||
20. 发布作业弹窗(选班级/时间)
|
||||
21. 溯源标记渲染(已发布徽章 + 跳转)
|
||||
22. data-access-knowledge(反查函数,无 UI)
|
||||
23. 同步架构图(若 P1 新增了导出函数)
|
||||
|
||||
---
|
||||
|
||||
## 17. 验收标准
|
||||
|
||||
### P0 验收
|
||||
|
||||
- [ ] 教师可创建课案(选教材/章节/模板)
|
||||
- [ ] 5 套系统预设模板可选,选用后生成对应 block 骨架
|
||||
- [ ] Block 编辑器:增删改 block、拖拽排序、富文本编辑
|
||||
- [ ] 自动保存(3s debounce)+ 手动保存生成版本
|
||||
- [ ] 版本历史:列表、预览、回退
|
||||
- [ ] 我的课案库:列表、筛选、复制、删除
|
||||
- [ ] 权限校验:非 creator 无法编辑 draft
|
||||
- [ ] `npm run lint` + `npx tsc --noEmit` 零错误
|
||||
- [ ] 架构图 004/005 已同步
|
||||
|
||||
### P1 验收
|
||||
|
||||
- [ ] exercise block 可从题库拉取题目并展示
|
||||
- [ ] exercise block 可课案内新建题目(draft 暂存)
|
||||
- [ ] 富文本 block 可标注知识点(选择器 + chip 展示)
|
||||
- [ ] AI 推荐知识点按钮可用,推荐结果可勾选确认
|
||||
- [ ] exercise block(purpose=after_class_homework)可发布为作业
|
||||
- [ ] 发布后 inline 题目已入库,课案 content 中占位 ID 已替换
|
||||
- [ ] 发布后作业可通过 sourceExamId → exam → sourceLessonPlanId 反查课案
|
||||
- [ ] 已发布 exercise block 显示溯源徽章
|
||||
- [ ] 重复发布被拦截(提供"重新发布为新作业")
|
||||
- [ ] `npm run lint` + `npx tsc --noEmit` 零错误
|
||||
- [ ] 架构图已同步
|
||||
|
||||
---
|
||||
|
||||
## 18. 未覆盖范围(P2/P3 预告,本次不实现)
|
||||
|
||||
以下功能在蓝图中提及,但**不在本 spec 范围**,留作后续独立 spec:
|
||||
|
||||
### P2 协作
|
||||
- 分享链接(密码/有效期)
|
||||
- block 级批注线程(依赖本 spec 的稳定 blockId)
|
||||
- 采纳建议生成新版本
|
||||
|
||||
### P3 智能与回看
|
||||
- AI 课案初稿生成(按模板结构填充)
|
||||
- 环节级 AI 重写(选中 block → 指令 → 替换该 block)
|
||||
- 一致性检查(目标-活动-评价对齐)
|
||||
- 系统内资源推荐(微课/课件/实验视频)
|
||||
- 外部资源对接(国家中小学智慧教育平台 API)
|
||||
- AI 生成资源草稿(课件大纲/微课脚本/学案)
|
||||
- 教学反思 block 完整 UI
|
||||
- 学情回看(班级知识点掌握率/高频错题内嵌)
|
||||
- AI 补救教学建议
|
||||
- 知识点回写教材树(含审核流)
|
||||
|
||||
---
|
||||
|
||||
## 19. 风险与备注
|
||||
|
||||
| 风险 | 影响 | 缓解 |
|
||||
|------|------|------|
|
||||
| `schema.ts` 已 1111 行超 1000 硬上限,新增 3 表加剧 | 违反编码规范 | 本次新增时备注提示;schema.ts 按业务域拆分作为独立任务跟进 |
|
||||
| Block 编辑器复杂度高 | P0 工期风险 | 优先用成熟库(如 BlockNote/Plate)二次封装,不自研底层 |
|
||||
| inline 题目发布时入库失败 | 数据不一致 | publish-service 用事务包裹;失败则回滚 exam 草稿创建,课案 content 不替换占位 ID |
|
||||
| 自动保存频率高导致 versions 表膨胀 | 存储压力 | 自动保存不写 versions;定时自动版本 30min 一次;pruneAutoVersions 保留上限 50 |
|
||||
| AI 推荐知识点依赖 AI Provider 配置 | 功能可用性 | AI 不可用时按钮置灰 + 提示"未配置 AI Provider";不阻塞主流程 |
|
||||
|
||||
---
|
||||
|
||||
> 本 spec 完成后,下一步进入 `writing-plans` skill 生成详细实施计划。
|
||||
2937
docs/superpowers/plans/2026-06-18-lesson-preparation.md
Normal file
2937
docs/superpowers/plans/2026-06-18-lesson-preparation.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user