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,55 +2,21 @@
import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guard";
import { Permissions } from "@/shared/types/permissions";
import { db } from "@/shared/db";
import { chapters, knowledgePoints, questions, questionsToKnowledgePoints, textbooks } from "@/shared/db/schema";
import { CreateQuestionSchema } from "./schema";
import type { CreateQuestionInput } from "./schema";
import { ActionState } from "@/shared/types/action-state";
import { revalidatePath } from "next/cache";
import { createId } from "@paralleldrive/cuid2";
import { and, asc, eq } from "drizzle-orm";
import { z } from "zod";
import { getQuestions, type GetQuestionsParams } from "./data-access";
import {
createQuestionWithRelations,
deleteQuestionByIdRecursive,
getKnowledgePointOptions,
getQuestions,
updateQuestionById,
type GetQuestionsParams,
} from "./data-access";
import type { KnowledgePointOption } from "./types";
type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0]
async function insertQuestionWithRelations(
tx: Tx,
input: z.infer<typeof CreateQuestionSchema>,
authorId: string,
parentId: string | null = null
) {
const newQuestionId = createId();
await tx.insert(questions).values({
id: newQuestionId,
content: input.content,
type: input.type,
difficulty: input.difficulty,
authorId: authorId,
parentId: parentId,
});
if (input.knowledgePointIds && input.knowledgePointIds.length > 0) {
await tx.insert(questionsToKnowledgePoints).values(
input.knowledgePointIds.map((kpId) => ({
questionId: newQuestionId,
knowledgePointId: kpId,
}))
);
}
if (input.subQuestions && input.subQuestions.length > 0) {
for (const subQ of input.subQuestions) {
await insertQuestionWithRelations(tx, subQ, authorId, newQuestionId);
}
}
return newQuestionId;
}
export async function createNestedQuestion(
prevState: ActionState<string> | undefined,
formData: FormData | CreateQuestionInput
@@ -81,9 +47,7 @@ export async function createNestedQuestion(
const input = validatedFields.data;
await db.transaction(async (tx) => {
await insertQuestionWithRelations(tx, input, ctx.userId, null);
});
await createQuestionWithRelations(input, ctx.userId);
revalidatePath("/teacher/questions");
@@ -114,7 +78,7 @@ const UpdateQuestionSchema = z.object({
id: z.string().min(1),
type: z.enum(["single_choice", "multiple_choice", "text", "judgment", "composite"]),
difficulty: z.number().min(1).max(5),
content: z.any(),
content: z.unknown(),
knowledgePointIds: z.array(z.string()).optional(),
});
@@ -140,32 +104,9 @@ export async function updateQuestionAction(
};
}
const input = parsed.data;
const { id, ...updateData } = parsed.data;
await db.transaction(async (tx) => {
await tx
.update(questions)
.set({
type: input.type,
difficulty: input.difficulty,
content: input.content,
updatedAt: new Date(),
})
.where(canEditAll ? eq(questions.id, input.id) : and(eq(questions.id, input.id), eq(questions.authorId, ctx.userId)));
await tx
.delete(questionsToKnowledgePoints)
.where(eq(questionsToKnowledgePoints.questionId, input.id));
if (input.knowledgePointIds && input.knowledgePointIds.length > 0) {
await tx.insert(questionsToKnowledgePoints).values(
input.knowledgePointIds.map((kpId) => ({
questionId: input.id,
knowledgePointId: kpId,
}))
);
}
});
await updateQuestionById(id, updateData, canEditAll, ctx.userId);
revalidatePath("/teacher/questions");
@@ -181,19 +122,6 @@ export async function updateQuestionAction(
}
}
async function deleteQuestionRecursive(tx: Tx, questionId: string) {
const children = await tx
.select({ id: questions.id })
.from(questions)
.where(eq(questions.parentId, questionId));
for (const child of children) {
await deleteQuestionRecursive(tx, child.id);
}
await tx.delete(questions).where(eq(questions.id, questionId));
}
export async function deleteQuestionAction(
prevState: ActionState<string> | undefined,
formData: FormData
@@ -207,21 +135,7 @@ export async function deleteQuestionAction(
return { success: false, message: "Invalid question ID" };
}
await db.transaction(async (tx) => {
const q = await tx.query.questions.findFirst({
where: eq(questions.id, questionId),
});
if (!q) {
throw new Error("Question not found");
}
if (!canDeleteAll && q.authorId !== ctx.userId) {
throw new Error("Unauthorized");
}
await deleteQuestionRecursive(tx, questionId);
});
await deleteQuestionByIdRecursive(questionId, canDeleteAll, ctx.userId);
revalidatePath("/teacher/questions");
@@ -252,39 +166,7 @@ export async function getQuestionsAction(params: GetQuestionsParams) {
export async function getKnowledgePointOptionsAction(): Promise<KnowledgePointOption[]> {
try {
await requirePermission(Permissions.QUESTION_READ);
const rows = await db
.select({
id: knowledgePoints.id,
name: knowledgePoints.name,
chapterId: chapters.id,
chapterTitle: chapters.title,
textbookId: textbooks.id,
textbookTitle: textbooks.title,
subject: textbooks.subject,
grade: textbooks.grade,
})
.from(knowledgePoints)
.leftJoin(chapters, eq(chapters.id, knowledgePoints.chapterId))
.leftJoin(textbooks, eq(textbooks.id, chapters.textbookId))
.orderBy(
asc(textbooks.title),
asc(chapters.order),
asc(chapters.title),
asc(knowledgePoints.order),
asc(knowledgePoints.name)
);
return rows.map((row) => ({
id: row.id,
name: row.name,
chapterId: row.chapterId ?? null,
chapterTitle: row.chapterTitle ?? null,
textbookId: row.textbookId ?? null,
textbookTitle: row.textbookTitle ?? null,
subject: row.subject ?? null,
grade: row.grade ?? null,
}));
return await getKnowledgePointOptions();
} catch (e) {
if (e instanceof PermissionDeniedError) {
throw e;