"use server" import { requirePermission } from "@/shared/lib/auth-guard" import { Permissions } from "@/shared/types/permissions" import { CreateQuestionSchema } from "./schema" import type { CreateQuestionInput } from "./schema" import type { ActionState } from "@/shared/types/action-state" import { invalidateFor } from "@/shared/lib/cache" import { z } from "zod" import { createQuestionWithRelations, deleteQuestionByIdRecursive, deleteQuestionsBatch, exportQuestions, getChapterOptions, getKnowledgePointOptions, getKnowledgePointOptionsByChapter, getQuestions, getTextbookOptions, importQuestions, updateQuestionById, type GetQuestionsParams, type QuestionExportItem, type QuestionImportItem, } from "./data-access" import type { ChapterOption, KnowledgePointOption, TextbookOption } from "./types" import { QuestionTypeEnum } from "./schema" import { handleActionError, safeJsonParse } from "@/shared/lib/action-utils" import { trackQuestionCreated, trackQuestionUpdated, trackQuestionDeleted } from "./utils/track-event" /** Result type of getQuestions (data + meta) */ type QuestionsListResult = Awaited> /** Result type of getKnowledgePointOptions */ type KnowledgePointOptionsResult = KnowledgePointOption[] export async function createQuestionAction( prevState: ActionState | undefined, formData: FormData | CreateQuestionInput, ): Promise> { try { const ctx = await requirePermission(Permissions.QUESTION_CREATE) let rawInput: unknown = formData if (formData instanceof FormData) { const jsonString = formData.get("json") if (typeof jsonString === "string") { rawInput = safeJsonParse(jsonString, "题目内容格式无效") } else { return { success: false, message: "Invalid submission format. Expected JSON." } } } const validatedFields = CreateQuestionSchema.safeParse(rawInput) if (!validatedFields.success) { return { success: false, message: "Validation failed", errors: validatedFields.error.flatten().fieldErrors, } } const input = validatedFields.data const questionId = await createQuestionWithRelations(input, ctx.userId) trackQuestionCreated(questionId, input.type, input.difficulty) await invalidateFor("questions.create") return { success: true, message: "Question created successfully", data: questionId, } } catch (e) { return handleActionError(e) } } 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.record(z.string(), z.unknown()), knowledgePointIds: z.array(z.string()).optional(), }) export async function updateQuestionAction( prevState: ActionState | undefined, formData: FormData, ): Promise> { try { const ctx = await requirePermission(Permissions.QUESTION_UPDATE) const canEditAll = ctx.dataScope.type === "all" const jsonString = formData.get("json") if (typeof jsonString !== "string") { return { success: false, message: "Invalid submission format. Expected JSON." } } const parsed = UpdateQuestionSchema.safeParse(safeJsonParse(jsonString, "题目内容格式无效")) if (!parsed.success) { return { success: false, message: "Validation failed", errors: parsed.error.flatten().fieldErrors, } } const { id, ...updateData } = parsed.data await updateQuestionById(id, updateData, canEditAll, ctx.userId) trackQuestionUpdated(id, updateData.type, updateData.difficulty) await invalidateFor("questions.update") return { success: true, message: "Question updated successfully", data: id } } catch (e) { return handleActionError(e) } } export async function deleteQuestionAction( prevState: ActionState | undefined, formData: FormData, ): Promise> { try { const ctx = await requirePermission(Permissions.QUESTION_DELETE) const canDeleteAll = ctx.dataScope.type === "all" const questionId = formData.get("questionId") if (typeof questionId !== "string") { return { success: false, message: "Invalid question ID" } } await deleteQuestionByIdRecursive(questionId, canDeleteAll, ctx.userId) trackQuestionDeleted(questionId) await invalidateFor("questions.delete") return { success: true, message: "Question deleted successfully", data: questionId } } catch (e) { return handleActionError(e) } } const BatchDeleteSchema = z.object({ ids: z.array(z.string().min(1)).min(1, "At least one question ID is required"), }) /** * 批量删除题目。 * * 输入 JSON:{ ids: string[] } * 权限:QUESTION_DELETE,遵循数据范围过滤。 */ export async function deleteQuestionsBatchAction( prevState: ActionState<{ deleted: number }> | undefined, formData: FormData, ): Promise> { try { const ctx = await requirePermission(Permissions.QUESTION_DELETE) const canDeleteAll = ctx.dataScope.type === "all" const jsonString = formData.get("json") if (typeof jsonString !== "string") { return { success: false, message: "Invalid submission format. Expected JSON." } } const parsed = BatchDeleteSchema.safeParse(safeJsonParse(jsonString, "批量删除参数无效")) if (!parsed.success) { return { success: false, message: "Validation failed", errors: parsed.error.flatten().fieldErrors, } } const deletedCount = await deleteQuestionsBatch(parsed.data.ids, canDeleteAll, ctx.userId) for (const id of parsed.data.ids) { trackQuestionDeleted(id) } await invalidateFor("questions.deleteBatch") return { success: true, message: "Batch delete completed", data: { deleted: deletedCount } } } catch (e) { return handleActionError(e) } } export async function getQuestionsAction( params: GetQuestionsParams, ): Promise> { try { await requirePermission(Permissions.QUESTION_READ) const data = await getQuestions(params) return { success: true, data } } catch (e) { return handleActionError(e) } } export async function getKnowledgePointOptionsAction(): Promise< ActionState > { try { await requirePermission(Permissions.QUESTION_READ) const data = await getKnowledgePointOptions() return { success: true, data } } catch (e) { return handleActionError(e) } } /** Result type of getTextbookOptions */ type TextbookOptionsResult = TextbookOption[] /** Result type of getChapterOptions */ type ChapterOptionsResult = ChapterOption[] /** Result type of getKnowledgePointOptionsByChapter */ type KnowledgePointOptionsByChapterResult = { id: string; name: string }[] /** * 获取教材选项列表(级联筛选第一级)。 */ export async function getTextbookOptionsAction(): Promise< ActionState > { try { await requirePermission(Permissions.QUESTION_READ) const data = await getTextbookOptions() return { success: true, data } } catch (e) { return handleActionError(e) } } /** * 获取指定教材下的章节选项列表(级联筛选第二级)。 */ export async function getChapterOptionsAction( textbookId: string, ): Promise> { try { await requirePermission(Permissions.QUESTION_READ) const data = await getChapterOptions(textbookId) return { success: true, data } } catch (e) { return handleActionError(e) } } /** * 获取指定章节下的知识点选项列表(级联筛选第三级)。 */ export async function getKnowledgePointOptionsByChapterAction( chapterId: string, ): Promise> { try { await requirePermission(Permissions.QUESTION_READ) const data = await getKnowledgePointOptionsByChapter(chapterId) return { success: true, data } } catch (e) { return handleActionError(e) } } // --------------------------------------------------------------------------- // 导入/导出 Server Actions // --------------------------------------------------------------------------- const ExportSchema = z.object({ ids: z.array(z.string().min(1)).optional(), }) /** * 导出题目为 JSON 格式。 * * 输入 JSON:{ ids?: string[] }(不传 ids 则导出全部,受权限范围限制) * 权限:QUESTION_READ,遵循数据范围过滤。 */ export async function exportQuestionsAction( formData: FormData, ): Promise> { try { const ctx = await requirePermission(Permissions.QUESTION_READ) const canExportAll = ctx.dataScope.type === "all" const jsonString = formData.get("json") const inputJson = typeof jsonString === "string" ? jsonString : "{}" const parsed = ExportSchema.safeParse(safeJsonParse(inputJson, "导出参数无效")) if (!parsed.success) { return { success: false, message: "Validation failed", errors: parsed.error.flatten().fieldErrors, } } const data = await exportQuestions(parsed.data.ids, canExportAll, ctx.userId) return { success: true, data } } catch (e) { return handleActionError(e) } } const ImportItemSchema = z.object({ type: QuestionTypeEnum, difficulty: z.number().min(1).max(5), content: z.unknown(), knowledgePointIds: z.array(z.string()).optional(), }) const ImportSchema = z.object({ items: z.array(ImportItemSchema).min(1, "至少需要一条题目"), }) /** * 批量导入题目。 * * 输入 JSON:{ items: QuestionImportItem[] } * 权限:QUESTION_CREATE。 */ export async function importQuestionsAction( prevState: ActionState<{ imported: number }> | undefined, formData: FormData, ): Promise> { try { const ctx = await requirePermission(Permissions.QUESTION_CREATE) const jsonString = formData.get("json") if (typeof jsonString !== "string") { return { success: false, message: "Invalid submission format. Expected JSON." } } const parsed = ImportSchema.safeParse(safeJsonParse(jsonString, "导入参数无效")) if (!parsed.success) { return { success: false, message: "Validation failed", errors: parsed.error.flatten().fieldErrors, } } const items: QuestionImportItem[] = parsed.data.items.map((item) => ({ type: item.type, difficulty: item.difficulty, content: item.content, knowledgePointIds: item.knowledgePointIds, })) const createdIds = await importQuestions(items, ctx.userId) for (let i = 0; i < createdIds.length; i++) { const item = items[i] trackQuestionCreated(createdIds[i], item.type, item.difficulty) } await invalidateFor("questions.import") return { success: true, message: "Import completed", data: { imported: createdIds.length } } } catch (e) { return handleActionError(e) } }