refactor: P1-2 actions 层 DB 操作下沉到 data-access (exams/homework/questions/announcements)

This commit is contained in:
SpecialX
2026-06-18 02:31:16 +08:00
parent 2c8e229e00
commit 84d6636bd1
9 changed files with 858 additions and 438 deletions

View File

@@ -0,0 +1,317 @@
import "server-only"
import { createId } from "@paralleldrive/cuid2"
import { and, count, eq } from "drizzle-orm"
import { db } from "@/shared/db"
import {
classes,
classEnrollments,
classSubjectTeachers,
exams,
homeworkAnswers,
homeworkAssignmentQuestions,
homeworkAssignmentTargets,
homeworkAssignments,
homeworkSubmissions,
} from "@/shared/db/schema"
import type { DataScope } from "@/shared/types/permissions"
// ---- Types ----
export type HomeworkExamQuestionData = {
questionId: string
score: number | null
order: number | null
}
export type HomeworkExamData = {
id: string
title: string
subjectId: string | null
structure: unknown
questions: HomeworkExamQuestionData[]
}
export type HomeworkSubmissionPermissionData = {
id: string
studentId: string
status: string | null
assignment: {
dueAt: Date | null
allowLate: boolean
lateDueAt: Date | null
}
}
export type CreateHomeworkAssignmentData = {
assignmentId: string
sourceExamId: string
title: string
description: string | null
structure: unknown
status: string
creatorId: string
availableAt: Date | null
dueAt: Date | null
allowLate: boolean
lateDueAt: Date | null
maxAttempts: number
publish: boolean
questions: HomeworkExamQuestionData[]
targetStudentIds: string[]
}
// ---- Query helpers (for permission/validation in actions) ----
export const getClassTeacherById = async (
classId: string
): Promise<{ id: string; teacherId: string } | null> => {
const [row] = await db
.select({ id: classes.id, teacherId: classes.teacherId })
.from(classes)
.where(eq(classes.id, classId))
.limit(1)
return row ?? null
}
export const getExamWithQuestionsForHomework = async (
examId: string
): Promise<HomeworkExamData | null> => {
const exam = await db.query.exams.findFirst({
where: eq(exams.id, examId),
with: {
questions: {
orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)],
},
},
})
if (!exam) return null
return {
id: exam.id,
title: exam.title,
subjectId: exam.subjectId,
structure: exam.structure,
questions: exam.questions.map((q) => ({
questionId: q.questionId,
score: q.score ?? null,
order: q.order ?? null,
})),
}
}
export const getTeacherAssignedSubjectIds = async (
classId: string,
teacherId: string
): Promise<string[]> => {
const rows = await db
.select({ subjectId: classSubjectTeachers.subjectId })
.from(classSubjectTeachers)
.where(
and(
eq(classSubjectTeachers.classId, classId),
eq(classSubjectTeachers.teacherId, teacherId)
)
)
return rows.map((r) => r.subjectId)
}
export const getActiveClassStudentIdsForHomework = async (
classId: string,
dataScope: DataScope,
userId: string,
classTeacherId: string
): Promise<string[]> => {
const classScope =
dataScope.type === "all"
? eq(classes.id, classId)
: classTeacherId === userId
? eq(classes.teacherId, userId)
: eq(classes.id, classId)
const rows = await db
.select({ studentId: classEnrollments.studentId })
.from(classEnrollments)
.innerJoin(classes, eq(classes.id, classEnrollments.classId))
.where(
and(
eq(classEnrollments.classId, classId),
eq(classEnrollments.status, "active"),
classScope
)
)
return rows.map((r) => r.studentId)
}
export const getHomeworkSubmissionForPermission = async (
submissionId: string
): Promise<HomeworkSubmissionPermissionData | null> => {
const submission = await db.query.homeworkSubmissions.findFirst({
where: eq(homeworkSubmissions.id, submissionId),
with: { assignment: true },
})
if (!submission) return null
return {
id: submission.id,
studentId: submission.studentId,
status: submission.status,
assignment: {
dueAt: submission.assignment.dueAt,
allowLate: submission.assignment.allowLate,
lateDueAt: submission.assignment.lateDueAt,
},
}
}
// ---- Write functions ----
export const createHomeworkAssignment = async (
input: CreateHomeworkAssignmentData
): Promise<string> => {
await db.transaction(async (tx) => {
await tx.insert(homeworkAssignments).values({
id: input.assignmentId,
sourceExamId: input.sourceExamId,
title: input.title,
description: input.description,
structure: input.publish ? input.structure : null,
status: input.status,
creatorId: input.creatorId,
availableAt: input.availableAt,
dueAt: input.dueAt,
allowLate: input.allowLate,
lateDueAt: input.lateDueAt,
maxAttempts: input.maxAttempts,
})
if (input.publish && input.questions.length > 0) {
await tx.insert(homeworkAssignmentQuestions).values(
input.questions.map((q) => ({
assignmentId: input.assignmentId,
questionId: q.questionId,
score: q.score ?? 0,
order: q.order ?? 0,
}))
)
}
if (input.publish && input.targetStudentIds.length > 0) {
await tx.insert(homeworkAssignmentTargets).values(
input.targetStudentIds.map((studentId) => ({
assignmentId: input.assignmentId,
studentId,
}))
)
}
})
return input.assignmentId
}
export const startHomeworkSubmission = async (
assignmentId: string,
studentId: string
): Promise<{ submissionId: string } | { error: string }> => {
const assignment = await db.query.homeworkAssignments.findFirst({
where: eq(homeworkAssignments.id, assignmentId),
})
if (!assignment) return { error: "Assignment not found" }
if (assignment.status !== "published") return { error: "Assignment not available" }
const target = await db.query.homeworkAssignmentTargets.findFirst({
where: and(
eq(homeworkAssignmentTargets.assignmentId, assignmentId),
eq(homeworkAssignmentTargets.studentId, studentId)
),
})
if (!target) return { error: "Not assigned" }
if (assignment.availableAt && assignment.availableAt > new Date()) {
return { error: "Not available yet" }
}
const [attemptRow] = await db
.select({ c: count() })
.from(homeworkSubmissions)
.where(
and(
eq(homeworkSubmissions.assignmentId, assignmentId),
eq(homeworkSubmissions.studentId, studentId)
)
)
const attemptNo = (attemptRow?.c ?? 0) + 1
if (attemptNo > assignment.maxAttempts) return { error: "No attempts left" }
const submissionId = createId()
await db.insert(homeworkSubmissions).values({
id: submissionId,
assignmentId,
studentId,
attemptNo,
status: "started",
startedAt: new Date(),
})
return { submissionId }
}
export const saveHomeworkAnswer = async (
submissionId: string,
questionId: string,
answerContent: unknown
): Promise<void> => {
await db.transaction(async (tx) => {
const existing = await tx.query.homeworkAnswers.findFirst({
where: and(
eq(homeworkAnswers.submissionId, submissionId),
eq(homeworkAnswers.questionId, questionId)
),
})
if (existing) {
await tx
.update(homeworkAnswers)
.set({ answerContent, updatedAt: new Date() })
.where(eq(homeworkAnswers.id, existing.id))
} else {
await tx.insert(homeworkAnswers).values({
id: createId(),
submissionId,
questionId,
answerContent,
})
}
})
}
export const markHomeworkSubmitted = async (
submissionId: string,
isLate: boolean
): Promise<void> => {
const now = new Date()
await db
.update(homeworkSubmissions)
.set({ status: "submitted", submittedAt: now, isLate, updatedAt: now })
.where(eq(homeworkSubmissions.id, submissionId))
}
export const gradeHomeworkAnswers = async (
submissionId: string,
answers: Array<{ id: string; score: number; feedback: string | null }>
): Promise<void> => {
let totalScore = 0
for (const ans of answers) {
await db
.update(homeworkAnswers)
.set({ score: ans.score, feedback: ans.feedback, updatedAt: new Date() })
.where(eq(homeworkAnswers.id, ans.id))
totalScore += ans.score
}
await db
.update(homeworkSubmissions)
.set({ score: totalScore, status: "graded", updatedAt: new Date() })
.where(eq(homeworkSubmissions.id, submissionId))
}