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

@@ -2,6 +2,7 @@ import { db } from "@/shared/db"
import { exams, examQuestions, questions, subjects, grades, classes } from "@/shared/db/schema"
import { count, eq, desc, like, and, or, inArray } from "drizzle-orm"
import { cache } from "react"
import { createId } from "@paralleldrive/cuid2"
import type { Exam, ExamDifficulty, ExamStatus } from "./types"
import type { AiGeneratedQuestion, AiGeneratedStructureNode } from "./ai-pipeline"
@@ -371,3 +372,153 @@ export const getExamsDashboardStats = cache(async (scope?: DataScope): Promise<E
return { examCount: Number(row?.value ?? 0) }
})
/**
* Get exam creator ID for ownership check.
* Returns null if exam not found.
*/
export const getExamCreatorId = async (examId: string): Promise<string | null> => {
const exam = await db.query.exams.findFirst({
where: eq(exams.id, examId),
columns: { creatorId: true },
})
return exam?.creatorId ?? null
}
/**
* Update an exam, optionally replacing its questions.
* Preserves original behavior: questions replacement is not transactional with exam field update.
*/
export const updateExamWithQuestions = async (
examId: string,
data: {
questions?: Array<{ id: string; score: number }>
structure?: unknown
status?: ExamStatus
}
): Promise<void> => {
if (data.questions) {
await db.delete(examQuestions).where(eq(examQuestions.examId, examId))
if (data.questions.length > 0) {
await db.insert(examQuestions).values(
data.questions.map((q, idx) => ({
examId,
questionId: q.id,
score: q.score ?? 0,
order: idx,
}))
)
}
}
const updateData: Partial<typeof exams.$inferInsert> = {}
if (data.status) updateData.status = data.status
if (data.structure !== undefined) updateData.structure = data.structure
if (Object.keys(updateData).length > 0) {
await db.update(exams).set(updateData).where(eq(exams.id, examId))
}
}
/**
* Delete an exam by ID.
*/
export const deleteExamById = async (examId: string): Promise<void> => {
await db.delete(exams).where(eq(exams.id, examId))
}
/**
* Duplicate an exam (including its questions) in a transaction.
* Returns the new exam ID, or null if the source exam is not found.
*/
export const duplicateExam = async (
sourceExamId: string,
newCreatorId: string
): Promise<string | null> => {
const source = await db.query.exams.findFirst({
where: eq(exams.id, sourceExamId),
with: {
questions: {
orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)],
},
},
})
if (!source) return null
const newExamId = createId()
await db.transaction(async (tx) => {
await tx.insert(exams).values({
id: newExamId,
title: `${source.title} (Copy)`,
description: omitScheduledAtFromDescription(source.description),
creatorId: newCreatorId,
startTime: null,
endTime: null,
status: "draft",
structure: source.structure,
})
if (source.questions.length > 0) {
await tx.insert(examQuestions).values(
source.questions.map((q) => ({
examId: newExamId,
questionId: q.questionId,
score: q.score ?? 0,
order: q.order ?? 0,
}))
)
}
})
return newExamId
}
/**
* Get exam preview data (structure + questions).
* Returns null if exam not found.
*/
export const getExamPreview = async (
examId: string
): Promise<{ structure: unknown; questions: Array<{ id: string }> } | null> => {
const exam = await db.query.exams.findFirst({
where: eq(exams.id, examId),
with: {
questions: {
orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)],
with: {
question: true,
},
},
},
})
if (!exam) return null
const questions = exam.questions.map((eqRel) => eqRel.question)
return {
structure: exam.structure,
questions,
}
}
/**
* Get all subjects for exam forms.
*/
export const getExamSubjects = async (): Promise<Array<{ id: string; name: string }>> => {
const allSubjects = await db.query.subjects.findMany({
orderBy: (subjects, { asc }) => [asc(subjects.order), asc(subjects.name)],
})
return allSubjects.map((s) => ({ id: s.id, name: s.name }))
}
/**
* Get all grades for exam forms.
*/
export const getExamGrades = async (): Promise<Array<{ id: string; name: string }>> => {
const allGrades = await db.query.grades.findMany({
orderBy: (grades, { asc }) => [asc(grades.order), asc(grades.name)],
})
return allGrades.map((g) => ({ id: g.id, name: g.name }))
}