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

@@ -0,0 +1,55 @@
import { notFound } from "next/navigation"
import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guard"
import { Permissions } from "@/shared/types/permissions"
import { ProctoringDashboard } from "@/modules/proctoring/components/proctoring-dashboard"
import {
getExamForProctoring,
getExamProctoringSummary,
getStudentProctoringStatuses,
getRecentProctoringEvents,
} from "@/modules/proctoring/data-access"
import type { ProctoringDashboardData } from "@/modules/proctoring/types"
export const dynamic = "force-dynamic"
export default async function ExamProctoringPage({
params,
}: {
params: Promise<{ id: string }>
}) {
try {
await requirePermission(Permissions.EXAM_PROCTOR)
} catch (error) {
if (error instanceof PermissionDeniedError) {
return (
<div className="p-10 text-center text-muted-foreground">
exam:proctor
</div>
)
}
throw error
}
const { id } = await params
const exam = await getExamForProctoring(id)
if (!exam) return notFound()
// 并行拉取面板初始数据
const [summary, students, recentEvents] = await Promise.all([
getExamProctoringSummary(id),
getStudentProctoringStatuses(id),
getRecentProctoringEvents(id, 20),
])
const initialData: ProctoringDashboardData = {
summary,
students,
recentEvents,
}
return (
<div className="flex h-full flex-col space-y-4 p-4">
<ProctoringDashboard examId={id} initialData={initialData} />
</div>
)
}