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

@@ -6,10 +6,19 @@ import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guar
import { Permissions } from "@/shared/types/permissions"
import { z } from "zod"
import { createId } from "@paralleldrive/cuid2"
import { db } from "@/shared/db"
import { exams, examQuestions } from "@/shared/db/schema"
import { eq } from "drizzle-orm"
import { buildExamDescription, omitScheduledAtFromDescription, persistAiGeneratedExamDraft, persistExamDraft, resolveSubjectGradeNames } from "./data-access"
import {
buildExamDescription,
deleteExamById,
duplicateExam,
getExamCreatorId,
getExamGrades,
getExamPreview,
getExamSubjects,
persistAiGeneratedExamDraft,
persistExamDraft,
resolveSubjectGradeNames,
updateExamWithQuestions,
} from "./data-access"
import {
AiGeneratedStructureSchema,
AiInsertQuestionSchema,
@@ -568,39 +577,18 @@ export async function updateExamAction(
// Ownership check: non-admin users can only update their own exams
if (ctx.dataScope.type !== "all") {
const exam = await db.query.exams.findFirst({
where: eq(exams.id, examId),
columns: { creatorId: true },
})
if (!exam || exam.creatorId !== ctx.userId) {
const creatorId = await getExamCreatorId(examId)
if (!creatorId || creatorId !== ctx.userId) {
return failState<string>("You can only update exams you created")
}
}
try {
if (questions) {
await db.delete(examQuestions).where(eq(examQuestions.examId, examId))
if (questions.length > 0) {
await db.insert(examQuestions).values(
questions.map((q, idx) => ({
examId,
questionId: q.id,
score: q.score ?? 0,
order: idx,
}))
)
}
}
// Prepare update object
const updateData: Partial<typeof exams.$inferInsert> = {}
if (status) updateData.status = status
if (structure !== undefined) updateData.structure = structure
if (Object.keys(updateData).length > 0) {
await db.update(exams).set(updateData).where(eq(exams.id, examId))
}
await updateExamWithQuestions(examId, {
questions: questions ?? undefined,
structure,
status,
})
} catch {
return failState<string>("Database error: Failed to update exam")
}
@@ -642,17 +630,14 @@ export async function deleteExamAction(
// Ownership check: non-admin users can only delete their own exams
if (ctx.dataScope.type !== "all") {
const exam = await db.query.exams.findFirst({
where: eq(exams.id, examId),
columns: { creatorId: true },
})
if (!exam || exam.creatorId !== ctx.userId) {
const creatorId = await getExamCreatorId(examId)
if (!creatorId || creatorId !== ctx.userId) {
return failState<string>("You can only delete exams you created")
}
}
try {
await db.delete(exams).where(eq(exams.id, examId))
await deleteExamById(examId)
} catch {
return failState<string>("Database error: Failed to delete exam")
}
@@ -692,45 +677,13 @@ export async function duplicateExamAction(
const { examId } = parsed.data
const source = await db.query.exams.findFirst({
where: eq(exams.id, examId),
with: {
questions: {
orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)],
},
},
})
if (!source) {
return failState<string>("Exam not found")
}
const newExamId = createId()
let newExamId: string
try {
await db.transaction(async (tx) => {
await tx.insert(exams).values({
id: newExamId,
title: `${source.title} (Copy)`,
description: omitScheduledAtFromDescription(source.description),
creatorId: ctx.userId,
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,
}))
)
}
})
const duplicatedId = await duplicateExam(examId, ctx.userId)
if (!duplicatedId) {
return failState<string>("Exam not found")
}
newExamId = duplicatedId
} catch {
return failState<string>("Database error: Failed to duplicate exam")
}
@@ -753,25 +706,14 @@ export async function getExamPreviewAction(
await requirePermission(Permissions.EXAM_READ)
try {
const exam = await db.query.exams.findFirst({
where: eq(exams.id, examId),
with: {
questions: {
orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)],
with: {
question: true
}
}
}
})
const exam = await getExamPreview(examId)
if (!exam) {
return failState<{ structure: unknown; questions: Array<{ id: string }> }>("Exam not found")
}
const questions = exam.questions.map((eq) => eq.question)
return successState({
structure: exam.structure,
questions,
questions: exam.questions,
})
} catch (error) {
console.error(error)
@@ -790,11 +732,8 @@ export async function getSubjectsAction(): Promise<ActionState<{ id: string; nam
await requirePermission(Permissions.EXAM_READ)
try {
const allSubjects = await db.query.subjects.findMany({
orderBy: (subjects, { asc }) => [asc(subjects.order), asc(subjects.name)],
})
return successState(allSubjects.map((s) => ({ id: s.id, name: s.name })))
const allSubjects = await getExamSubjects()
return successState(allSubjects)
} catch (error) {
console.error("Failed to fetch subjects:", error)
return failState<{ id: string; name: string }[]>("Failed to load subjects")
@@ -812,11 +751,8 @@ export async function getGradesAction(): Promise<ActionState<{ id: string; name:
await requirePermission(Permissions.EXAM_READ)
try {
const allGrades = await db.query.grades.findMany({
orderBy: (grades, { asc }) => [asc(grades.order), asc(grades.name)],
})
return successState(allGrades.map((g) => ({ id: g.id, name: g.name })))
const allGrades = await getExamGrades()
return successState(allGrades)
} catch (error) {
console.error("Failed to fetch grades:", error)
return failState<{ id: string; name: string }[]>("Failed to load grades")