feat(P2): 实现选课管理、考试监考、学情诊断三大功能模块

## 新增功能模块

### 1. 选课管理(elective)
- 新增表:electiveCourses、courseSelections
- 新增权限:ELECTIVE_MANAGE/ELECTIVE_READ/ELECTIVE_SELECT
- 支持先到先得 + 抽签两种选课模式
- admin/teacher/student 三端页面

### 2. 考试监考(proctoring)
- exams 表扩展:examMode/durationMinutes/antiCheatEnabled 等字段
- 新增表:examProctoringEvents
- 新增权限:EXAM_PROCTOR/EXAM_PROCTOR_READ
- 教师监考面板 + 学生端防作弊监控
- API:/api/proctoring/event 接收事件上报

### 3. 学情诊断报告(diagnostic)
- 新增表:knowledgePointMastery、learningDiagnosticReports
- 新增权限:DIAGNOSTIC_MANAGE/DIAGNOSTIC_READ
- 基于提交答案自动计算知识点掌握度
- 生成个人/班级诊断报告(强项/弱项/建议)
- 雷达图可视化

## 其他改动
- 项目规则:单文件行数限制从 300 行调整为企业级规范(组件≤500/Actions≤800/硬上限1000)
- scripts/seed.ts:消除全部 any 类型,定义内部类型,0 lint 错误
- 架构文档 004/005 同步更新三个新模块
- 迁移文件 0001_heavy_sage.sql 生成

## 验证
- npx tsc --noEmit:0 错误
- npm run lint:0 错误 0 警告
This commit is contained in:
SpecialX
2026-06-17 19:12:51 +08:00
parent baf8f679bf
commit b86255f0ea
46 changed files with 13234 additions and 80 deletions

View File

@@ -218,7 +218,7 @@
#### `Permissions` (常量对象)
- 文件:`types/permissions.ts`
- 定义47 个权限常量(`exam:create`, `homework:grade`, `audit_log:read`, `announcement:manage`, `file:upload`, `file:read`, `file:delete`, `grade_record:manage`, `grade_record:read`, `course_plan:manage`, `course_plan:read`, `attendance:manage`, `attendance:read`, `message:send`, `message:read`, `message:delete`, `schedule:auto`, `schedule:adjust` 等)
- 定义:54 个权限常量(`exam:create`, `homework:grade`, `audit_log:read`, `announcement:manage`, `file:upload`, `file:read`, `file:delete`, `grade_record:manage`, `grade_record:read`, `course_plan:manage`, `course_plan:read`, `attendance:manage`, `attendance:read`, `message:send`, `message:read`, `message:delete`, `schedule:auto`, `schedule:adjust`, `elective:manage`, `elective:read`, `elective:select`, `exam:proctor`, `exam:proctor_read`, `diagnostic:manage`, `diagnostic:read` 等)
- 被使用auth-guard.ts, 所有模块的 actions.ts, 前端组件
#### `ROLE_PERMISSIONS` (常量对象)
@@ -228,6 +228,7 @@
- 考勤权限admin/teacher 含 `ATTENDANCE_MANAGE`+`ATTENDANCE_READ`student/parent/grade_head/teaching_head 含 `ATTENDANCE_READ`
- 消息权限admin/teacher/grade_head/teaching_head 含 `MESSAGE_SEND`+`MESSAGE_READ`+`MESSAGE_DELETE`student/parent 含 `MESSAGE_SEND`+`MESSAGE_READ`
- 排课权限admin 含 `SCHEDULE_AUTO`+`SCHEDULE_ADJUST`teacher/student/parent/grade_head/teaching_head 无排课权限
- 学情诊断权限admin/teacher/grade_head 含 `DIAGNOSTIC_MANAGE`+`DIAGNOSTIC_READ`teaching_head/student 含 `DIAGNOSTIC_READ`
- 被使用:`resolvePermissions`, auth.ts (JWT callback)
#### `db` (Drizzle 实例)
@@ -399,6 +400,8 @@
| `schedulingRules` | id, classId, maxDailyHours, maxContinuousHours, lunchBreakStart, lunchBreakEnd, morningStart, afternoonEnd, avoidBackToBack, balancedSubjects, createdAt, updatedAt | scheduling |
| `scheduleChanges` | id, originalScheduleId, classId, originalTeacherId, substituteTeacherId, originalDate, newDate, newStartTime, newEndTime, reason, status, requestedBy, approvedBy, createdAt, updatedAt | scheduling |
| `passwordSecurity` | id, userId, failedLoginAttempts, lockedUntil, passwordChangedAt, mustChangePassword, lastPasswordChange, createdAt, updatedAt | auth, settings |
| `knowledgePointMastery` | id, studentId, knowledgePointId, masteryLevel, totalQuestions, correctQuestions, lastAssessedAt, createdAt, updatedAt | diagnostic |
| `learningDiagnosticReports` | id, studentId, generatedBy, reportType, period, summary, strengths, weaknesses, recommendations, overallScore, status, createdAt, updatedAt | diagnostic |
---
@@ -1420,8 +1423,10 @@
- teacher 角色菜单包含 "Course Plans" 项icon: CalendarRange, href: /teacher/course-plans, permission: Permissions.COURSE_PLAN_READ
- teacher 角色菜单包含 "Attendance" 项icon: CalendarCheck, href: /teacher/attendance, permission: Permissions.ATTENDANCE_MANAGE含子项 Records (/teacher/attendance)、Take Attendance (/teacher/attendance/sheet, permission: ATTENDANCE_MANAGE)、Statistics (/teacher/attendance/stats, permission: ATTENDANCE_READ)
- teacher 角色菜单包含 "Schedule Changes" 项icon: CalendarClock, href: /teacher/schedule-changes, permission: Permissions.SCHEDULE_ADJUST
- teacher 角色菜单包含 "Diagnostic" 项icon: Stethoscope, href: /teacher/diagnostic, permission: Permissions.DIAGNOSTIC_READ
- student 角色菜单包含 "My Grades" 项icon: GraduationCap, href: /student/grades, permission: Permissions.GRADE_RECORD_READ
- student 角色菜单包含 "Attendance" 项icon: CalendarCheck, href: /student/attendance, permission: Permissions.ATTENDANCE_READ
- student 角色菜单包含 "Diagnostic" 项icon: Stethoscope, href: /student/diagnostic, permission: Permissions.DIAGNOSTIC_READ
- parent 角色菜单包含 "Dashboard" 项icon: LayoutDashboard, href: /parent/dashboard无 permission 字段,仅需登录)
- parent 角色菜单包含 "Grades" 项icon: GraduationCap, href: /parent/grades, permission: Permissions.GRADE_RECORD_READ
- parent 角色菜单包含 "Attendance" 项icon: CalendarCheck, href: /parent/attendance, permission: Permissions.ATTENDANCE_READ
@@ -2578,31 +2583,299 @@
---
## 模块proctoring
`src/modules/proctoring`
考试监考模块:监考模式考试实时监控、防作弊事件采集、教师监考面板、学生端防作弊监控、考试模式配置。
> 权限:教师监考面板使用 `requirePermission(EXAM_PROCTOR)`;学生端上报事件使用 `requireAuth()`(学生上报自己的事件,不需要管理权限)。前端组件使用 `usePermission().hasPermission(EXAM_PROCTOR)` 控制权限。
### Server Actions (`actions.ts`)
| Action | 权限 | 用途 |
|--------|------|------|
| `recordProctoringEventAction` | requireAuth() | 学生端上报监考事件(含 submission 归属校验) |
| `getProctoringDashboardAction` | EXAM_PROCTOR | 获取监考面板数据(摘要+学生状态+最近事件) |
### Data Access (`data-access.ts`)
`import "server-only"`
| 函数 | 签名 | 被使用 |
|------|------|--------|
| `recordProctoringEvent` | `(input: RecordProctoringEventInput) => Promise<ProctoringEvent>` | actions.recordProctoringEventAction, API /api/proctoring/event |
| `getProctoringEvents` | `(examId, filters?) => Promise<ProctoringEventWithDetails[]>` | 待扩展 |
| `getProctoringEventsBySubmission` | `(submissionId) => Promise<ProctoringEvent[]>` | 待扩展 |
| `getExamProctoringSummary` | `(examId) => Promise<ExamProctoringSummary>` | actions.getProctoringDashboardAction, teacher/exams/[id]/proctoring/page.tsx |
| `getStudentProctoringStatuses` | `(examId) => Promise<StudentProctoringStatus[]>` | actions.getProctoringDashboardAction, teacher/exams/[id]/proctoring/page.tsx |
| `getExamForProctoring` | `(examId) => Promise<{id,title,examMode,config} | null>` | actions.getProctoringDashboardAction, teacher/exams/[id]/proctoring/page.tsx |
| `getRecentProctoringEvents` | `(examId, limit?) => Promise<ProctoringEventWithDetails[]>` | actions.getProctoringDashboardAction, teacher/exams/[id]/proctoring/page.tsx |
### Types (`types.ts`)
| 类型 | 定义 |
|------|------|
| `ProctoringEventType` | `"tab_switch" \| "window_blur" \| "copy_attempt" \| "paste_attempt" \| "right_click" \| "devtools_open" \| "fullscreen_exit" \| "idle_timeout"` |
| `ExamMode` | `"homework" \| "timed" \| "proctored"` |
| `ProctoringEvent` | `{ id, submissionId, studentId, examId, eventType, eventDetail?, occurredAt, createdAt }` |
| `ProctoringEventWithDetails` | `ProctoringEvent & { studentName, examTitle }` |
| `ExamProctoringSummary` | `{ examId, examTitle, examMode, totalStudents, startedStudents, submittedStudents, totalEvents, abnormalStudents, eventsByType }` |
| `StudentProctoringStatus` | `{ studentId, studentName, submissionId, submissionStatus, eventCount, lastEventAt, isAbnormal, eventsByType }` |
| `ProctoringDashboardData` | `{ summary, students, recentEvents }` |
| `ExamModeConfig` | `{ examMode, durationMinutes, shuffleQuestions, allowLateStart, lateStartGraceMinutes, antiCheatEnabled }` |
| `PROCTORING_EVENT_LABELS` | 事件类型中文标签常量 |
| `EXAM_MODE_LABELS` | 考试模式中文标签常量 |
| `ABNORMAL_EVENT_THRESHOLD` | 异常学生事件数阈值3 |
### Components (`components/`)
| 组件 | 用途 |
|------|------|
| `proctoring-dashboard.tsx` | 教师监考面板实时学生状态、异常事件统计、异常学生高亮、10 秒轮询、usePermission 权限控制) |
| `anti-cheat-monitor.tsx` | 学生端防作弊监控visibilitychange/blur/copy/paste/contextmenu/keydown/fullscreenchange 监听、空闲超时检测、强制全屏、警告提示、事件上报) |
| `exam-mode-config.tsx` | 考试模式配置react-hook-form Controller作业/限时/监考模式选择,限时设置时长,监考设置防作弊选项) |
### API Routes
| 路由 | 方法 | 权限 | 用途 |
|------|------|------|------|
| `/api/proctoring/event` | POST | requireAuth() | 接收学生端上报的监考事件(含 submission 归属校验) |
---
## 模块diagnostic
`src/modules/diagnostic`
学情诊断报告模块:基于知识点掌握度(`knowledgePointMastery` 表)生成个人/班级诊断报告,掌握度雷达图(学生 vs 班级平均),强项/弱项分析,知识点掌握度热力图,需重点关注学生列表,报告发布/删除管理。
> 权限:所有 Server Actions 使用 `requirePermission()` 校验。生成/发布/删除报告使用 `requirePermission(DIAGNOSTIC_MANAGE)`,查询/详情使用 `requirePermission(DIAGNOSTIC_READ)`。admin/teacher/grade_head 角色拥有 DIAGNOSTIC_MANAGE+DIAGNOSTIC_READteaching_head/student 角色仅有 DIAGNOSTIC_READ。前端组件使用 `usePermission().hasPermission(DIAGNOSTIC_MANAGE)` 控制生成/发布/删除按钮可见性(无 `role === "xxx"` 硬编码)。页面路由通过 `getAuthContext()` 进行 DataScope 二次校验:`class_members` 仅查自己,`children` 仅查子女,`class_taught` 必须包含 classId。
### Server Actions (`actions.ts`)
`"use server"`
| Action | 权限 | 用途 |
|--------|------|------|
| `generateStudentReportAction` | DIAGNOSTIC_MANAGE | 生成学生个人诊断报告formData: studentId, period |
| `generateClassReportAction` | DIAGNOSTIC_MANAGE | 生成班级诊断报告formData: classId, period |
| `publishReportAction` | DIAGNOSTIC_MANAGE | 发布诊断报告formData: idstatus → published |
| `deleteReportAction` | DIAGNOSTIC_MANAGE | 删除诊断报告formData: id |
| `getDiagnosticReportsAction` | DIAGNOSTIC_READ | 查询诊断报告列表params: DiagnosticReportQueryParams |
| `getDiagnosticReportByIdAction` | DIAGNOSTIC_READ | 获取诊断报告详情id |
### Data Access (`data-access.ts` + `data-access-reports.ts`)
`import "server-only"`(两个文件均以此开头)
#### `data-access.ts`(掌握度相关)
| 函数 | 签名 | 被使用 |
|------|------|--------|
| `getStudentMastery` | `(studentId: string) => Promise<MasteryWithKnowledgePoint[]>` | getStudentMasterySummary, teacher/diagnostic/student/[studentId] |
| `getStudentMasterySummary` | `(studentId: string) => Promise<StudentMasterySummary \| null>` | generateDiagnosticReport, teacher/diagnostic/student/[studentId], student/diagnostic |
| `updateMasteryFromSubmission` | `(submissionId: string) => Promise<void>` | 待扩展(作业/考试提交后触发onDuplicateKeyUpdate upsert |
| `getClassMasterySummary` | `(classId: string) => Promise<ClassMasterySummary \| null>` | generateClassDiagnosticReport, teacher/diagnostic/class/[classId] |
| `getKnowledgePointStats` | `(classId?: string, gradeId?: string) => Promise<KnowledgePointStat[]>` | teacher/diagnostic/student/[studentId](班级平均对比) |
#### `data-access-reports.ts`(报告相关)
| 函数 | 签名 | 被使用 |
|------|------|--------|
| `generateDiagnosticReport` | `(studentId, period, generatedBy) => Promise<string>` | generateStudentReportAction |
| `generateClassDiagnosticReport` | `(classId, period, generatedBy) => Promise<string>` | generateClassReportAction |
| `getDiagnosticReports` | `(filters: DiagnosticReportQueryParams) => Promise<DiagnosticReportWithDetails[]>` | getDiagnosticReportsAction, teacher/diagnostic, teacher/diagnostic/student/[studentId], student/diagnostic |
| `getDiagnosticReportById` | `(id: string) => Promise<DiagnosticReportWithDetails \| null>` | getDiagnosticReportByIdAction |
| `publishDiagnosticReport` | `(id: string) => Promise<void>` | publishReportAction |
| `deleteDiagnosticReport` | `(id: string) => Promise<void>` | deleteReportAction |
### Types (`types.ts`)
| Type | 定义 |
|------|------|
| `DiagnosticReportType` | `"individual" \| "class" \| "grade"` |
| `DiagnosticReportStatus` | `"draft" \| "published" \| "archived"` |
| `KnowledgePointMastery` | `{ id, studentId, knowledgePointId, masteryLevel(0-100), totalQuestions, correctQuestions, lastAssessedAt, createdAt, updatedAt }` |
| `MasteryWithKnowledgePoint` | `KnowledgePointMastery & { knowledgePointName, knowledgePointDescription }` |
| `StudentMasterySummary` | `{ studentId, studentName, averageMastery, totalKnowledgePoints, strengths(≥80), weaknesses(<60), allMastery }` |
| `DiagnosticReport` | `{ id, studentId, generatedBy, reportType, period, summary, strengths[], weaknesses[], recommendations[], overallScore, status, createdAt, updatedAt }` |
| `DiagnosticReportWithDetails` | `DiagnosticReport & { studentName, generatedByName }` |
| `ClassMasterySummary` | `{ classId, className, studentCount, averageMastery, knowledgePointStats[], studentsNeedingAttention[] }` |
| `KnowledgePointStat` | `{ knowledgePointId, knowledgePointName, averageMastery, masteredCount(≥80), notMasteredCount(<60), totalStudents }` |
| `DiagnosticReportQueryParams` | `{ studentId?, reportType?, status?, period? }` |
| `MasteryRadarPoint` | `{ knowledgePoint, student(0-100), classAverage?(0-100) }` |
### Components (`components/`)
所有组件以 `"use client"` 开头。
| 组件 | 用途 |
|------|------|
| `mastery-radar-chart.tsx` | 知识点掌握度雷达图recharts RadarChart学生 vs 班级平均对比,无数据时显示 EmptyState |
| `student-diagnostic-view.tsx` | 学生诊断视图(概览卡片、雷达图、强项/弱项列表、生成报告表单[DIAGNOSTIC_MANAGE]、最新报告与建议展示) |
| `class-diagnostic-view.tsx` | 班级诊断视图(概览卡片、知识点掌握度热力图[绿≥80/黄60-79/橙40-59/红<40]、知识点排名表、需重点关注学生表[链接到学生视图]、生成班级报告表单[DIAGNOSTIC_MANAGE] |
| `report-list.tsx` | 诊断报告列表reportType/status 过滤器[URL searchParams]、报告表格、发布/删除操作[DIAGNOSTIC_MANAGE]、确认对话框) |
### 数据库表
| 表 | 用途 |
|----|------|
| `knowledgePointMastery` | 知识点掌握度记录(复合主键 studentId+knowledgePointIdonDuplicateKeyUpdate upsert |
| `learningDiagnosticReports` | 学情诊断报告reportType: individual/class/gradestatus: draft/published/archived |
### 页面路由
| 路由 | 组件 | 权限 | DataScope 校验 |
|------|------|------|----------------|
| `/teacher/diagnostic` | ReportList | diagnostic:read | class_members 仅查看自己报告 |
| `/teacher/diagnostic/student/[studentId]` | StudentDiagnosticView | diagnostic:read | class_members 仅自己children 仅子女 |
| `/teacher/diagnostic/class/[classId]` | ClassDiagnosticView | diagnostic:read | class_taught 必须包含 classIdclass_members/children → notFound |
| `/student/diagnostic` | StudentDiagnosticView | diagnostic:read | class_members 仅查自己ctx.userId |
---
## 模块elective
`src/modules/elective`
选课管理模块:选修课程 CRUD、选课开放/关闭、学生选课/退课、抽签模式批量录取runLottery、FCFS 即时录取、DataScope 行级过滤admin 全部、teacher 所教、grade_head 所管年级、student 可选课程)。
> 权限:管理操作使用 `requirePermission(ELECTIVE_MANAGE)`;读取使用 `requirePermission(ELECTIVE_READ)`;学生选课/退课使用 `requirePermission(ELECTIVE_SELECT)`。前端组件使用 `usePermission().hasPermission()` 控制权限。`getStudentSelectionsAction` 对 class_members/children 进行 DataScope 二次校验。
### Server Actions (`actions.ts`)
| Action | 权限 | 用途 |
|--------|------|------|
| `createElectiveCourseAction` | ELECTIVE_MANAGE | 创建选修课程formData: name, subjectId?, teacherId, gradeId?, description?, capacity?, classroom?, schedule?, startDate?, endDate?, selectionStartAt?, selectionEndAt?, selectionMode?, credit? |
| `updateElectiveCourseAction` | ELECTIVE_MANAGE | 更新选修课程id + formData |
| `deleteElectiveCourseAction` | ELECTIVE_MANAGE | 删除选修课程formData: courseId |
| `openSelectionAction` | ELECTIVE_MANAGE | 开放选课formData: courseId |
| `closeSelectionAction` | ELECTIVE_MANAGE | 关闭选课formData: courseId |
| `runLotteryAction` | ELECTIVE_MANAGE | 执行抽签录取formData: courseId返回 {enrolled, waitlist} |
| `selectCourseAction` | ELECTIVE_SELECT | 学生选课formData: courseId, priority? |
| `dropCourseAction` | ELECTIVE_SELECT | 学生退课formData: courseId |
| `getElectiveCoursesAction` | ELECTIVE_READ | 查询选修课程列表(按 DataScope 过滤,传 currentUserId |
| `getStudentSelectionsAction` | ELECTIVE_READ | 查询学生选课记录DataScope 二次校验class_members 仅自己children 仅子女) |
| `getAvailableCoursesAction` | ELECTIVE_SELECT | 获取学生可选课程status=open 且匹配年级) |
### Data Access
#### `data-access.ts` (`import "server-only"`)
| 函数 | 签名 | 被使用 |
|------|------|--------|
| `getElectiveCourses` | `(params?: GetElectiveCoursesParams & { scope?: DataScope; currentUserId?: string }) => Promise<ElectiveCourseWithDetails[]>` | getElectiveCoursesAction, admin/elective, teacher/elective |
| `getElectiveCourseById` | `(id: string) => Promise<ElectiveCourseWithDetails \| null>` | updateElectiveCourseAction, admin/elective/[id]/edit |
| `createElectiveCourse` | `(data: CreateElectiveCourseInput, teacherId: string) => Promise<string>` | createElectiveCourseAction |
| `updateElectiveCourse` | `(id: string, data: Partial<UpdateElectiveCourseInput>) => Promise<void>` | updateElectiveCourseAction |
| `deleteElectiveCourse` | `(id: string) => Promise<void>` | deleteElectiveCourseAction |
| `openSelection` | `(courseId: string) => Promise<void>` | openSelectionAction |
| `closeSelection` | `(courseId: string) => Promise<void>` | closeSelectionAction |
| `getSubjectOptions` | `() => Promise<{id, name}[]>` | admin/elective/create, admin/elective/[id]/edit |
> `buildScopeFilter(scope, userId)` 内部函数owned/class_taught 按 `teacherId` 过滤grade_managed 按 `gradeIds` 过滤class_members/children 返回 null学生通过 `getAvailableCoursesForStudent` 获取可选课程)。
#### `data-access-selections.ts` (`import "server-only"`)
> 从 data-access.ts 拆分以遵守单文件 ≤300 行规则。
| 函数 | 签名 | 被使用 |
|------|------|--------|
| `getCourseSelections` | `(courseId: string) => Promise<CourseSelectionWithDetails[]>` | 待扩展 |
| `getStudentSelections` | `(studentId: string) => Promise<CourseSelectionWithDetails[]>` | getStudentSelectionsAction, student/elective |
| `getStudentGradeId` | `(studentId: string) => Promise<string \| null>` | getAvailableCoursesForStudent |
| `getAvailableCoursesForStudent` | `(studentId: string, gradeId?: string \| null) => Promise<ElectiveCourseWithDetails[]>` | getAvailableCoursesAction, student/elective |
#### `data-access-operations.ts` (`import "server-only"`)
> 从 data-access.ts 拆分以遵守单文件 ≤300 行规则。包含选课/退课/抽签的写操作。
| 函数 | 签名 | 被使用 |
|------|------|--------|
| `runLottery` | `(courseId: string) => Promise<{enrolled: number, waitlist: number}>` | runLotteryAction |
| `selectCourse` | `(courseId: string, studentId: string, priority?: number) => Promise<{status: CourseSelectionStatus, message: string}>` | selectCourseAction |
| `dropCourse` | `(courseId: string, studentId: string) => Promise<void>` | dropCourseAction |
### Schema (`schema.ts`)
| Schema | 用途 |
|--------|------|
| `ElectiveCourseStatusEnum` | 课程状态枚举draft/open/closed/cancelled |
| `ElectiveSelectionModeEnum` | 选课模式枚举fcfs/lottery |
| `CourseSelectionStatusEnum` | 选课状态枚举selected/enrolled/waitlist/dropped/rejected |
| `CreateElectiveCourseSchema` | 创建课程校验name 必填teacherId 必填capacity 1-500 默认 30selectionMode 默认 fcfscredit 默认 1.0 |
| `UpdateElectiveCourseSchema` | 更新课程校验(所有字段可选,含 status |
| `SelectCourseSchema` | 选课校验courseId 必填priority 1-10 可选) |
| `DropCourseSchema` | 退课校验courseId 必填) |
| `RunLotterySchema` | 抽签校验courseId 必填) |
### 类型/接口 (`types.ts`)
| 类型 | 定义 |
|------|------|
| `ElectiveCourseStatus` | `"draft" \| "open" \| "closed" \| "cancelled"` |
| `ElectiveSelectionMode` | `"fcfs" \| "lottery"` |
| `CourseSelectionStatus` | `"selected" \| "enrolled" \| "waitlist" \| "dropped" \| "rejected"` |
| `ElectiveCourse` | 课程完整类型id, name, subjectId?, teacherId, gradeId?, description?, capacity, enrolledCount, classroom?, schedule?, startDate?, endDate?, selectionStartAt?, selectionEndAt?, status, selectionMode, credit, createdAt, updatedAt |
| `ElectiveCourseWithDetails` | `ElectiveCourse & { teacherName?, subjectName?, gradeName? }` |
| `CourseSelection` | 选课记录类型id, courseId, studentId, status, priority?, selectedAt, enrolledAt?, droppedAt?, lotteryRank?, createdAt, updatedAt |
| `CourseSelectionWithDetails` | `CourseSelection & { courseName?, studentName?, courseCapacity?, courseEnrolledCount?, courseStatus? }` |
| `GetElectiveCoursesParams` | 查询参数status?, gradeId?, subjectId?, teacherId? |
| `ELECTIVE_STATUS_LABELS` | 课程状态标签常量 |
| `ELECTIVE_STATUS_COLORS` | 课程状态颜色常量Badge variant |
| `SELECTION_MODE_LABELS` | 选课模式标签常量 |
| `COURSE_SELECTION_STATUS_LABELS` | 选课状态标签常量 |
| `COURSE_SELECTION_STATUS_COLORS` | 选课状态颜色常量Badge variant |
### 导出组件 (`components/`)
| 组件文件 | 功能 |
|---------|------|
| `elective-course-list.tsx` | 课程卡片列表(管理员/教师视图,含编辑/开放/关闭/抽签/删除操作按钮usePermission 权限控制) |
| `elective-course-form.tsx` | 课程创建/编辑表单name, subjectId, teacherId, gradeId, description, capacity, classroom, schedule, dates, selectionMode, credit |
| `student-selection-view.tsx` | 学生选课视图(可选课程列表 + 我的选课记录,含选课/退课按钮) |
### 路由页面
| 路由 | 组件 | 权限 | 说明 |
|------|------|------|------|
| `/admin/elective` | ElectiveCourseList | elective:manage | 管理员选修课程列表scope=all |
| `/admin/elective/create` | ElectiveCourseForm | elective:manage | 创建选修课程 |
| `/admin/elective/[id]/edit` | ElectiveCourseForm (edit) | elective:manage | 编辑选修课程 |
| `/teacher/elective` | ElectiveCourseList (teacher) | elective:manage | 教师选修课程列表scope=class_taught/owned按 teacherId 过滤) |
| `/student/elective` | StudentSelectionView | elective:select | 学生选课页面(可选课程 + 我的选课) |
---
## 模块间依赖矩阵
| ↓ 使用 → | shared | auth | exams | homework | questions | textbooks | classes | school | dashboard | layout | settings | users | audit | announcements | files | grades | course-plans | parent | messaging | attendance | scheduling |
|----------|--------|------|-------|----------|-----------|-----------|---------|--------|-----------|--------|----------|-------|-------|---------------|-------|-------|-------------|--------|-----------|------------|-----------|
| **shared** | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **auth** | db,schema,permissions,login-logger | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **exams** | db,auth-guard,types,ai | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **homework** | db,auth-guard,types | auth | data-access.getExams | - | - | - | schema | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **questions** | db,auth-guard,types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **textbooks** | db,auth-guard,types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **classes** | db,auth-guard,types | auth | - | homework-insights | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **school** | db,auth-guard,types,audit-logger | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **dashboard** | db,types | auth | - | data-access.getTeacherGradeTrends,getStudentDashboardGrades | - | - | data-access.getTeacherClasses,getStudentClasses,getStudentSchedule | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **layout** | hooks.usePermission | auth(useSession) | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | notification-dropdown | - | - |
| **settings** | db,auth-guard,ai,types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **users** | db,auth-guard(requireAuth,requirePermission),types,lib.excel | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **audit** | db,auth-guard.requirePermission,types.permissions | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **announcements** | db,auth-guard,types | auth | - | - | - | - | - | - | data-access.getGrades | - | - | - | - | - | - | - | - | - | - | - | - |
| **files** | db,auth-guard(requireAuth,requirePermission),types,lib/file-storage | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **grades** | db,auth-guard,types,lib.excel | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **course-plans** | db,auth-guard.requirePermission,types | auth | - | - | - | - | - | data-access.getAdminClasses,getStaffOptions | data-access.getAcademicYears | - | - | - | - | - | - | - | - | - | - | - | - |
| **parent** | db,auth-guard(requireAuth),types | auth | - | data-access.getStudentHomeworkAssignments,getStudentDashboardGrades | - | - | data-access.getStudentClasses,getStudentSchedule | - | - | - | - | - | - | - | - | data-access.getStudentGradeSummary | - | - | - | - | - |
| **messaging** | db,auth-guard(requirePermission,requireAuth),types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **attendance** | db,auth-guard.requirePermission,types | auth | - | - | - | - | - | data-access.getTeacherClasses,getAdminClasses | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **scheduling** | db,auth-guard(requirePermission,getAuthContext),types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| ↓ 使用 → | shared | auth | exams | homework | questions | textbooks | classes | school | dashboard | layout | settings | users | audit | announcements | files | grades | course-plans | parent | messaging | attendance | scheduling | proctoring | diagnostic | elective |
|----------|--------|------|-------|----------|-----------|-----------|---------|--------|-----------|--------|----------|-------|-------|---------------|-------|-------|-------------|--------|-----------|------------|-----------|------------|------------|----------|
| **shared** | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **auth** | db,schema,permissions,login-logger | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **exams** | db,auth-guard,types,ai | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **homework** | db,auth-guard,types | auth | data-access.getExams | - | - | - | schema | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **questions** | db,auth-guard,types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **textbooks** | db,auth-guard,types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **classes** | db,auth-guard,types | auth | - | homework-insights | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **school** | db,auth-guard,types,audit-logger | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **dashboard** | db,types | auth | - | data-access.getTeacherGradeTrends,getStudentDashboardGrades | - | - | data-access.getTeacherClasses,getStudentClasses,getStudentSchedule | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **layout** | hooks.usePermission | auth(useSession) | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | notification-dropdown | - | - | - | - |
| **settings** | db,auth-guard,ai,types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **users** | db,auth-guard(requireAuth,requirePermission),types,lib.excel | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **audit** | db,auth-guard.requirePermission,types.permissions | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **announcements** | db,auth-guard,types | auth | - | - | - | - | - | - | data-access.getGrades | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **files** | db,auth-guard(requireAuth,requirePermission),types,lib/file-storage | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **grades** | db,auth-guard,types,lib.excel | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **course-plans** | db,auth-guard.requirePermission,types | auth | - | - | - | - | - | data-access.getAdminClasses,getStaffOptions | data-access.getAcademicYears | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **parent** | db,auth-guard(requireAuth),types | auth | - | data-access.getStudentHomeworkAssignments,getStudentDashboardGrades | - | - | data-access.getStudentClasses,getStudentSchedule | - | - | - | - | - | - | - | - | data-access.getStudentGradeSummary | - | - | - | - | - | - |
| **messaging** | db,auth-guard(requirePermission,requireAuth),types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **attendance** | db,auth-guard.requirePermission,types | auth | - | - | - | - | - | data-access.getTeacherClasses,getAdminClasses | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **scheduling** | db,auth-guard(requirePermission,getAuthContext),types | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **proctoring** | db,auth-guard(requirePermission,requireAuth),types,components.ui,hooks.usePermission | auth | schema.exams,examSubmissions,examProctoringEvents | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **diagnostic** | db,auth-guard(requirePermission,getAuthContext),types,hooks.usePermission,components.ui | auth | schema.examSubmissions,submissionAnswers,questionsToKnowledgePoints | - | - | - | schema.classes,classEnrollments | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
| **elective** | db,auth-guard.requirePermission,types,types.DataScope,hooks.usePermission,components.ui | auth | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - | - |
---
@@ -2616,6 +2889,8 @@
5. 在 exams/actions.ts 中作为 `creatorId` 写入 `exams`
6. 在 homework/actions.ts 中作为 `creatorId` 写入 `homeworkAssignments`
7. 在 classes/data-access.ts 中查询 `getTeacherClasses(teacherId)``getGradeManagedClasses(userId)`
8. 在 elective/actions.ts 中作为 `teacherId` 默认值写入 `electiveCourses`createElectiveCourseAction作为 `studentId` 查询学生选课selectCourseAction/dropCourseAction/getStudentSelectionsAction
9. 在 elective/data-access.ts 中 `getElectiveCourses({ scope, currentUserId })``teacherId` 过滤class_taught/owned scope
### `examId`
1.`exams/actions.ts``createExamAction` 产生,通过 CUID2 生成,写入 `exams`
@@ -2634,13 +2909,13 @@
6.`auth-guard.ts` 中通过 `classSubjectTeachers` 查询教师关联的 classIds构建 `DataScope.class_taught`
### `permission`
1.`shared/types/permissions.ts``Permissions` 常量定义(47 个权限点,含 `AUDIT_LOG_READ``ANNOUNCEMENT_MANAGE``FILE_UPLOAD``FILE_READ``FILE_DELETE``GRADE_RECORD_MANAGE``GRADE_RECORD_READ``COURSE_PLAN_MANAGE``COURSE_PLAN_READ``ATTENDANCE_MANAGE``ATTENDANCE_READ``MESSAGE_SEND``MESSAGE_READ``MESSAGE_DELETE``SCHEDULE_AUTO``SCHEDULE_ADJUST` 等)
2.`shared/lib/permissions.ts` 中通过 `ROLE_PERMISSIONS` 映射角色到权限列表admin 角色包含 `AUDIT_LOG_READ``COURSE_PLAN_MANAGE`+`COURSE_PLAN_READ`admin/teacher 含 `FILE_UPLOAD/READ/DELETE``GRADE_RECORD_MANAGE/READ`teacher/student/grade_head/teaching_head 含 `COURSE_PLAN_READ`student/parent 含 `FILE_READ``GRADE_RECORD_READ`admin/teacher 含 `ATTENDANCE_MANAGE`+`ATTENDANCE_READ`student/parent/grade_head/teaching_head 含 `ATTENDANCE_READ`admin/teacher/parent/grade_head/teaching_head 含 `MESSAGE_SEND/READ/DELETE`student 含 `MESSAGE_READ/DELETE` 但无 `MESSAGE_SEND`admin 含 `SCHEDULE_AUTO`+`SCHEDULE_ADJUST`teacher/student/parent/grade_head/teaching_head 无排课权限)
1.`shared/types/permissions.ts``Permissions` 常量定义(57 个权限点,含 `AUDIT_LOG_READ``ANNOUNCEMENT_MANAGE``FILE_UPLOAD``FILE_READ``FILE_DELETE``GRADE_RECORD_MANAGE``GRADE_RECORD_READ``COURSE_PLAN_MANAGE``COURSE_PLAN_READ``ATTENDANCE_MANAGE``ATTENDANCE_READ``MESSAGE_SEND``MESSAGE_READ``MESSAGE_DELETE``SCHEDULE_AUTO``SCHEDULE_ADJUST``DIAGNOSTIC_MANAGE``DIAGNOSTIC_READ``ELECTIVE_MANAGE``ELECTIVE_READ``ELECTIVE_SELECT` 等)
2.`shared/lib/permissions.ts` 中通过 `ROLE_PERMISSIONS` 映射角色到权限列表admin 角色包含 `AUDIT_LOG_READ``COURSE_PLAN_MANAGE`+`COURSE_PLAN_READ`admin/teacher 含 `FILE_UPLOAD/READ/DELETE``GRADE_RECORD_MANAGE/READ`teacher/student/grade_head/teaching_head 含 `COURSE_PLAN_READ`student/parent 含 `FILE_READ``GRADE_RECORD_READ`admin/teacher 含 `ATTENDANCE_MANAGE`+`ATTENDANCE_READ`student/parent/grade_head/teaching_head 含 `ATTENDANCE_READ`admin/teacher/parent/grade_head/teaching_head 含 `MESSAGE_SEND/READ/DELETE`student 含 `MESSAGE_READ/DELETE` 但无 `MESSAGE_SEND`admin 含 `SCHEDULE_AUTO`+`SCHEDULE_ADJUST`teacher/student/parent/grade_head/teaching_head 无排课权限admin/teacher/grade_head 含 `DIAGNOSTIC_MANAGE`+`DIAGNOSTIC_READ`teaching_head/student 含 `DIAGNOSTIC_READ`admin/teacher 含 `ELECTIVE_MANAGE`+`ELECTIVE_READ`student 含 `ELECTIVE_SELECT`+`ELECTIVE_READ`grade_head/teaching_head 含 `ELECTIVE_READ`
3.`auth.ts` JWT callback 中通过 `resolvePermissions(roleNames)` 合并多角色权限,存入 JWT
4.`proxy.ts` middleware 中通过 `token.permissions` 检查路由访问权限
5.`shared/lib/auth-guard.ts` 中通过 `requirePermission(permission)` 在 Server Action 层断言权限(如 audit-logs 页面使用 `requirePermission(AUDIT_LOG_READ)``DELETE /api/files/[id]` 使用 `requirePermission(FILE_DELETE)`messaging/actions.ts 使用 `requirePermission(MESSAGE_SEND/READ/DELETE)`attendance/actions.ts 使用 `requirePermission(ATTENDANCE_MANAGE/READ)`users/actions.ts 的 `importUsersAction`/`exportUsersAction`/`downloadUserTemplateAction` 使用 `requirePermission(USER_MANAGE)`grades/actions.ts 的 `exportGradesAction` 使用 `requirePermission(GRADE_RECORD_READ)``POST /api/import` 使用 `requirePermission(USER_MANAGE)`scheduling/actions.ts 使用 `requirePermission(SCHEDULE_AUTO/SCHEDULE_ADJUST)`
5.`shared/lib/auth-guard.ts` 中通过 `requirePermission(permission)` 在 Server Action 层断言权限(如 audit-logs 页面使用 `requirePermission(AUDIT_LOG_READ)``DELETE /api/files/[id]` 使用 `requirePermission(FILE_DELETE)`messaging/actions.ts 使用 `requirePermission(MESSAGE_SEND/READ/DELETE)`attendance/actions.ts 使用 `requirePermission(ATTENDANCE_MANAGE/READ)`users/actions.ts 的 `importUsersAction`/`exportUsersAction`/`downloadUserTemplateAction` 使用 `requirePermission(USER_MANAGE)`grades/actions.ts 的 `exportGradesAction` 使用 `requirePermission(GRADE_RECORD_READ)``POST /api/import` 使用 `requirePermission(USER_MANAGE)`scheduling/actions.ts 使用 `requirePermission(SCHEDULE_AUTO/SCHEDULE_ADJUST)`diagnostic/actions.ts 使用 `requirePermission(DIAGNOSTIC_MANAGE/READ)`elective/actions.ts 使用 `requirePermission(ELECTIVE_MANAGE/READ/SELECT)`
6.`shared/hooks/use-permission.ts` 中通过 `hasPermission(permission)` 在客户端组件中条件渲染
7.`layout/config/navigation.ts` 中作为 `NavItem.permission` 字段过滤侧边栏菜单Audit Logs 菜单项使用 `Permissions.AUDIT_LOG_READ`Messages 菜单项使用 `Permissions.MESSAGE_READ`Attendance 菜单项 teacher 使用 `Permissions.ATTENDANCE_MANAGE`student/parent 使用 `Permissions.ATTENDANCE_READ`Import Users 菜单项使用 `Permissions.USER_MANAGE`Scheduling 菜单项 admin 使用 `Permissions.SCHEDULE_ADJUST`/`SCHEDULE_AUTO`teacher Schedule Changes 菜单项使用 `Permissions.SCHEDULE_ADJUST`
7.`layout/config/navigation.ts` 中作为 `NavItem.permission` 字段过滤侧边栏菜单Audit Logs 菜单项使用 `Permissions.AUDIT_LOG_READ`Messages 菜单项使用 `Permissions.MESSAGE_READ`Attendance 菜单项 teacher 使用 `Permissions.ATTENDANCE_MANAGE`student/parent 使用 `Permissions.ATTENDANCE_READ`Import Users 菜单项使用 `Permissions.USER_MANAGE`Scheduling 菜单项 admin 使用 `Permissions.SCHEDULE_ADJUST`/`SCHEDULE_AUTO`teacher Schedule Changes 菜单项使用 `Permissions.SCHEDULE_ADJUST`teacher/student Diagnostic 菜单项使用 `Permissions.DIAGNOSTIC_READ`Electives 菜单项 admin/teacher 使用 `Permissions.ELECTIVE_MANAGE`student 使用 `Permissions.ELECTIVE_SELECT`
### `DataScope`
1.`auth-guard.ts``resolveDataScope(userId, roles)` 根据用户角色和 DB 关系动态计算
@@ -2652,6 +2927,8 @@
7.`parent/children/[studentId]/page.tsx` 中通过 `ctx.dataScope.type === "children" && !ctx.dataScope.childrenIds.includes(studentId)` 二次校验家长拥有该子女
8. 传递到 `attendance/data-access.getAttendanceRecords({ scope })` 进行行级过滤class_taught 按教师班级过滤children 按子女过滤class_members 仅查自己all 查全部)
9.`attendance/actions.ts``getStudentAttendanceAction` 中对 class_members/children 进行 DataScope 二次校验
10. 传递到 `elective/data-access.getElectiveCourses({ scope, currentUserId })` 进行行级过滤owned/class_taught 按 `teacherId` 过滤grade_managed 按 `gradeIds` 过滤class_members/children 返回 null 由 `getAvailableCoursesForStudent` 处理)
11.`elective/actions.ts``getStudentSelectionsAction` 中对 class_members/children 进行 DataScope 二次校验
---
@@ -2761,6 +3038,7 @@
|------|------|------|------|------|
| `/teacher/exams` | ExamDataTable | server | exam:read | 考试列表dataAccess: exams/data-access.getExams |
| `/teacher/exams/[id]/build` | ExamAssemblyPanel | client | exam:update | 组卷页面components: assembly/*, actions: updateExamAction |
| `/teacher/exams/[id]/proctoring` | ProctoringDashboard | server+client | exam:proctor | 教师监考面板dataAccess: proctoring/data-access.getExamForProctoring,getExamProctoringSummary,getStudentProctoringStatuses,getRecentProctoringEvents权限requirePermission(EXAM_PROCTOR)组件proctoring/components/proctoring-dashboard.tsx10 秒轮询刷新) |
| `/teacher/exams/grading` | ExamGradingList | server | exam:read | 考试批改列表 |
| `/teacher/exams/grading/[submissionId]` | ExamGradingView | client | exam:read | 考试批改页面 |
| `/teacher/classes/my/[id]` | ClassDetail | server | class:read | 班级详情dataAccess: classes/data-access.getClassDetails |
@@ -2822,6 +3100,25 @@
| `/settings` | 角色分发设置页 | server | auth_required | 根据权限渲染 Admin/Teacher/Student 设置视图(含 General/Appearance/Security/Notifications tabSecurity tab 含 PasswordChangeFormNotifications tab 含 NotificationPreferencesFormdataAccess: messaging/notification-preferences.getNotificationPreferences |
| `/settings/security` | SecuritySettingsPage | server | auth_required | 安全设置独立页面PasswordChangeForm + 安全提示权限requireAuth() |
### diagnostic/* 路由
| 路由 | 组件 | 类型 | 权限 | 说明 |
|------|------|------|------|------|
| `/teacher/diagnostic` | ReportList | client | diagnostic:read | 学情诊断报告列表reportType/status 过滤器[URL searchParams]dataAccess: diagnostic/data-access-reports.getDiagnosticReportsactions: publishReportAction, deleteReportAction[DIAGNOSTIC_MANAGE]权限requirePermission(DIAGNOSTIC_READ)DataScope.class_members 仅查看自己报告) |
| `/teacher/diagnostic/student/[studentId]` | StudentDiagnosticView | client | diagnostic:read | 学生学情诊断视图(概览卡片+雷达图+强项/弱项+生成报告[DIAGNOSTIC_MANAGE]+最新报告dataAccess: getStudentMasterySummary, getKnowledgePointStats[班级平均对比], getDiagnosticReportsactions: generateStudentReportAction权限getAuthContext + DataScope 二次校验class_members 仅自己children 仅子女) |
| `/teacher/diagnostic/class/[classId]` | ClassDiagnosticView | client | diagnostic:read | 班级学情诊断视图(概览+知识点热力图+排名表+需重点关注学生+生成班级报告[DIAGNOSTIC_MANAGE]dataAccess: getClassMasterySummaryactions: generateClassReportAction权限getAuthContext + DataScope 校验class_taught 必须包含 classIdclass_members/children → notFound |
| `/student/diagnostic` | StudentDiagnosticView | client | diagnostic:read | 学生本人学情诊断视图(概览+雷达图+强项/弱项+最新报告dataAccess: getStudentMasterySummary(ctx.userId), getDiagnosticReports(studentId=ctx.userId)权限requirePermission(DIAGNOSTIC_READ)DataScope.class_members 仅查自己) |
### elective/* 路由
| 路由 | 组件 | 类型 | 权限 | 说明 |
|------|------|------|------|------|
| `/admin/elective` | ElectiveCourseList | server | elective:manage | 管理员选修课程列表dataAccess: getElectiveCourses(scope=all)actions: deleteElectiveCourseAction, openSelectionAction, closeSelectionAction, runLotteryAction权限requirePermission(ELECTIVE_MANAGE) |
| `/admin/elective/create` | ElectiveCourseForm | client | elective:manage | 创建选修课程actions: createElectiveCourseActiondataAccess: getSubjectOptions权限requirePermission(ELECTIVE_MANAGE) |
| `/admin/elective/[id]/edit` | ElectiveCourseForm (edit) | client | elective:manage | 编辑选修课程actions: updateElectiveCourseActiondataAccess: getElectiveCourseById, getSubjectOptions权限requirePermission(ELECTIVE_MANAGE) |
| `/teacher/elective` | ElectiveCourseList (teacher) | server | elective:manage | 教师选修课程列表dataAccess: getElectiveCourses(scope=class_taught/owned, currentUserId)actions: deleteElectiveCourseAction, openSelectionAction, closeSelectionAction, runLotteryAction权限requirePermission(ELECTIVE_MANAGE);按 teacherId 过滤) |
| `/student/elective` | StudentSelectionView | server | elective:select | 学生选课页面dataAccess: getAvailableCoursesForStudent, getStudentSelectionsactions: selectCourseAction, dropCourseAction权限requirePermission(ELECTIVE_SELECT) |
### API 路由(含速率限制)
| 路由 | 方法 | 限流规则 | 说明 |