import "server-only" import { createId } from "@paralleldrive/cuid2" import { and, count, eq } from "drizzle-orm" import { db } from "@/shared/db" import { homeworkAnswers, homeworkAssignmentQuestions, homeworkAssignmentTargets, homeworkAssignments, homeworkSubmissions, } from "@/shared/db/schema" import { getActiveStudentIdsByClassId, getClassTeacherById as getClassTeacherIdFromClass, getTeacherSubjectIdsByClass, } from "@/modules/classes/data-access" import { getExamWithQuestionsForHomework as getExamWithQuestionsFromExams, type ExamWithQuestionsForHomework, } from "@/modules/exams/data-access" import type { DataScope } from "@/shared/types/permissions" // ---- Types ---- export type HomeworkExamQuestionData = { questionId: string score: number | null order: number | null } export type HomeworkExamData = ExamWithQuestionsForHomework 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 | null 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) ---- // These delegate to cross-module data-access interfaces to avoid direct DB queries. export const getClassTeacherById = async ( classId: string ): Promise<{ id: string; teacherId: string | null } | null> => { const teacherId = await getClassTeacherIdFromClass(classId) if (teacherId === null) return null return { id: classId, teacherId } } export const getExamWithQuestionsForHomework = async ( examId: string ): Promise => { return await getExamWithQuestionsFromExams(examId) } export const getTeacherAssignedSubjectIds = async ( classId: string, teacherId: string ): Promise => { return await getTeacherSubjectIdsByClass(classId, teacherId) } export const getActiveClassStudentIdsForHomework = async ( classId: string, _dataScope: DataScope, _userId: string, _classTeacherId: string | null ): Promise => { // Permission/scope filtering is handled by requirePermission in actions.ts. // This function returns active students for the class via the classes data-access interface. return await getActiveStudentIdsByClassId(classId) } export const getHomeworkSubmissionForPermission = async ( submissionId: string ): Promise => { 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, }, } } /** * 批改权限校验:获取提交记录及其作业的创建者信息 * 用于 gradeHomeworkSubmissionAction 校验教师是否有权批改该提交 * 返回 null 表示提交记录不存在 */ export const getHomeworkSubmissionForGrading = async ( submissionId: string ): Promise<{ id: string assignmentId: string creatorId: string sourceExamId: string | null } | null> => { const submission = await db.query.homeworkSubmissions.findFirst({ where: eq(homeworkSubmissions.id, submissionId), with: { assignment: true }, }) if (!submission) return null return { id: submission.id, assignmentId: submission.assignmentId, creatorId: submission.assignment.creatorId, sourceExamId: submission.assignment.sourceExamId, } } // ---- Write functions ---- export const createHomeworkAssignment = async ( input: CreateHomeworkAssignmentData ): Promise => { 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 => { 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 => { 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 => { await db.transaction(async (tx) => { let totalScore = 0 for (const ans of answers) { await tx .update(homeworkAnswers) .set({ score: ans.score, feedback: ans.feedback, updatedAt: new Date() }) .where(eq(homeworkAnswers.id, ans.id)) totalScore += ans.score } await tx .update(homeworkSubmissions) .set({ score: totalScore, status: "graded", updatedAt: new Date() }) .where(eq(homeworkSubmissions.id, submissionId)) }) }