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:
217
src/modules/elective/data-access-operations.ts
Normal file
217
src/modules/elective/data-access-operations.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import "server-only"
|
||||
|
||||
import { createId } from "@paralleldrive/cuid2"
|
||||
import { and, asc, eq, inArray } from "drizzle-orm"
|
||||
|
||||
import { db } from "@/shared/db"
|
||||
import {
|
||||
courseSelections,
|
||||
electiveCourses,
|
||||
} from "@/shared/db/schema"
|
||||
|
||||
import type { CourseSelectionStatus } from "./types"
|
||||
|
||||
export async function runLottery(courseId: string): Promise<{
|
||||
enrolled: number
|
||||
waitlist: number
|
||||
}> {
|
||||
const [course] = await db
|
||||
.select()
|
||||
.from(electiveCourses)
|
||||
.where(eq(electiveCourses.id, courseId))
|
||||
.limit(1)
|
||||
if (!course) throw new Error("Course not found")
|
||||
|
||||
const selections = await db
|
||||
.select()
|
||||
.from(courseSelections)
|
||||
.where(
|
||||
and(
|
||||
eq(courseSelections.courseId, courseId),
|
||||
eq(courseSelections.status, "selected")
|
||||
)
|
||||
)
|
||||
.orderBy(asc(courseSelections.priority), asc(courseSelections.selectedAt))
|
||||
|
||||
if (selections.length === 0) {
|
||||
return { enrolled: 0, waitlist: 0 }
|
||||
}
|
||||
|
||||
const shuffled = [...selections].sort(() => Math.random() - 0.5)
|
||||
const capacity = course.capacity
|
||||
const now = new Date()
|
||||
|
||||
let enrolledCount = 0
|
||||
let waitlistCount = 0
|
||||
for (let i = 0; i < shuffled.length; i++) {
|
||||
const sel = shuffled[i]
|
||||
const rank = i + 1
|
||||
if (i < capacity) {
|
||||
await db
|
||||
.update(courseSelections)
|
||||
.set({
|
||||
status: "enrolled",
|
||||
lotteryRank: rank,
|
||||
enrolledAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(courseSelections.id, sel.id))
|
||||
enrolledCount++
|
||||
} else {
|
||||
await db
|
||||
.update(courseSelections)
|
||||
.set({
|
||||
status: "waitlist",
|
||||
lotteryRank: rank,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(courseSelections.id, sel.id))
|
||||
waitlistCount++
|
||||
}
|
||||
}
|
||||
|
||||
await db
|
||||
.update(electiveCourses)
|
||||
.set({ enrolledCount, status: "closed", updatedAt: now })
|
||||
.where(eq(electiveCourses.id, courseId))
|
||||
|
||||
return { enrolled: enrolledCount, waitlist: waitlistCount }
|
||||
}
|
||||
|
||||
export async function selectCourse(
|
||||
courseId: string,
|
||||
studentId: string,
|
||||
priority?: number
|
||||
): Promise<{ status: CourseSelectionStatus; message: string }> {
|
||||
const [course] = await db
|
||||
.select()
|
||||
.from(electiveCourses)
|
||||
.where(eq(electiveCourses.id, courseId))
|
||||
.limit(1)
|
||||
if (!course) throw new Error("Course not found")
|
||||
if (course.status !== "open") throw new Error("Course selection is not open")
|
||||
|
||||
const now = new Date()
|
||||
if (course.selectionStartAt && now < course.selectionStartAt) {
|
||||
throw new Error("Selection has not started yet")
|
||||
}
|
||||
if (course.selectionEndAt && now > course.selectionEndAt) {
|
||||
throw new Error("Selection has ended")
|
||||
}
|
||||
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(courseSelections)
|
||||
.where(
|
||||
and(
|
||||
eq(courseSelections.courseId, courseId),
|
||||
eq(courseSelections.studentId, studentId),
|
||||
inArray(courseSelections.status, ["selected", "enrolled", "waitlist"])
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
if (existing) throw new Error("Already selected this course")
|
||||
|
||||
const id = createId()
|
||||
let status: CourseSelectionStatus = "selected"
|
||||
let enrolledAt: Date | null = null
|
||||
|
||||
if (course.selectionMode === "fcfs" && course.enrolledCount < course.capacity) {
|
||||
status = "enrolled"
|
||||
enrolledAt = now
|
||||
await db
|
||||
.update(electiveCourses)
|
||||
.set({
|
||||
enrolledCount: course.enrolledCount + 1,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(electiveCourses.id, courseId))
|
||||
} else if (course.selectionMode === "fcfs") {
|
||||
status = "waitlist"
|
||||
}
|
||||
|
||||
await db.insert(courseSelections).values({
|
||||
id,
|
||||
courseId,
|
||||
studentId,
|
||||
status,
|
||||
priority: priority ?? 1,
|
||||
selectedAt: now,
|
||||
enrolledAt,
|
||||
})
|
||||
|
||||
return {
|
||||
status,
|
||||
message:
|
||||
status === "enrolled"
|
||||
? "Enrolled successfully"
|
||||
: status === "waitlist"
|
||||
? "Added to waitlist"
|
||||
: "Selection submitted",
|
||||
}
|
||||
}
|
||||
|
||||
export async function dropCourse(
|
||||
courseId: string,
|
||||
studentId: string
|
||||
): Promise<void> {
|
||||
const [existing] = await db
|
||||
.select()
|
||||
.from(courseSelections)
|
||||
.where(
|
||||
and(
|
||||
eq(courseSelections.courseId, courseId),
|
||||
eq(courseSelections.studentId, studentId),
|
||||
inArray(courseSelections.status, ["selected", "enrolled", "waitlist"])
|
||||
)
|
||||
)
|
||||
.limit(1)
|
||||
if (!existing) throw new Error("No active selection found")
|
||||
|
||||
const now = new Date()
|
||||
await db
|
||||
.update(courseSelections)
|
||||
.set({ status: "dropped", droppedAt: now, updatedAt: now })
|
||||
.where(eq(courseSelections.id, existing.id))
|
||||
|
||||
if (existing.status === "enrolled") {
|
||||
const [course] = await db
|
||||
.select()
|
||||
.from(electiveCourses)
|
||||
.where(eq(electiveCourses.id, courseId))
|
||||
.limit(1)
|
||||
if (course && course.selectionMode === "fcfs") {
|
||||
const newEnrolledCount = Math.max(0, course.enrolledCount - 1)
|
||||
await db
|
||||
.update(electiveCourses)
|
||||
.set({ enrolledCount: newEnrolledCount, updatedAt: now })
|
||||
.where(eq(electiveCourses.id, courseId))
|
||||
|
||||
const [nextWait] = await db
|
||||
.select()
|
||||
.from(courseSelections)
|
||||
.where(
|
||||
and(
|
||||
eq(courseSelections.courseId, courseId),
|
||||
eq(courseSelections.status, "waitlist")
|
||||
)
|
||||
)
|
||||
.orderBy(asc(courseSelections.priority), asc(courseSelections.selectedAt))
|
||||
.limit(1)
|
||||
if (nextWait) {
|
||||
await db
|
||||
.update(courseSelections)
|
||||
.set({
|
||||
status: "enrolled",
|
||||
enrolledAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(courseSelections.id, nextWait.id))
|
||||
await db
|
||||
.update(electiveCourses)
|
||||
.set({ enrolledCount: newEnrolledCount + 1, updatedAt: now })
|
||||
.where(eq(electiveCourses.id, courseId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user