From 8ec8a70c3b0e01bfadf8b1e3aa1d7b1d03503d11 Mon Sep 17 00:00:00 2001 From: gameloader Date: Tue, 18 Nov 2025 14:00:31 +0800 Subject: [PATCH] feat(submission): allow resubmission and add file size limit --- .../[assignmentId]/submissions/route.ts | 81 ++++++++++++------- 1 file changed, 54 insertions(+), 27 deletions(-) diff --git a/src/app/api/assignments/[assignmentId]/submissions/route.ts b/src/app/api/assignments/[assignmentId]/submissions/route.ts index 18f4379..c76263f 100644 --- a/src/app/api/assignments/[assignmentId]/submissions/route.ts +++ b/src/app/api/assignments/[assignmentId]/submissions/route.ts @@ -1,11 +1,12 @@ import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; -import { uploadAssignmentFile } from "@/lib/s3"; +import { deleteAssignmentFile, uploadAssignmentFile } from "@/lib/s3"; import { evaluateAssignment } from "@/lib/grading"; import { getNumericParam } from "@/lib/route-params"; import { submissionSchema } from "@/lib/validators"; const ACCEPT_EXTENSIONS = new Set([".pdf"]); +const MAX_FILE_SIZE_BYTES = 20 * 1024 * 1024; // 20MB type AssignmentRouteParams = { assignmentId: string | string[] | undefined }; @@ -45,17 +46,22 @@ export async function POST( return NextResponse.json({ error: "请上传作业文件" }, { status: 400 }); } - let metadata: unknown = {}; - if (metadataRaw) { - try { - metadata = JSON.parse(String(metadataRaw)); - } catch (error) { - console.error("Failed to parse submission metadata", error); - return NextResponse.json({ error: "表单信息格式错误" }, { status: 400 }); + let metadata: Record & { formPayload?: unknown } = {}; + try { + if (metadataRaw) { + const parsedMetadata = JSON.parse(String(metadataRaw)); + if (parsedMetadata && typeof parsedMetadata === "object" && !Array.isArray(parsedMetadata)) { + metadata = parsedMetadata as Record; + } } + } catch (error) { + console.error("Failed to parse submission metadata", error); + return NextResponse.json({ error: "表单信息格式错误" }, { status: 400 }); } - const parsed = submissionSchema.safeParse(metadata ?? {}); + const parsed = submissionSchema.safeParse({ + ...metadata, + }); if (!parsed.success) { return NextResponse.json( { error: parsed.error.flatten() }, @@ -73,6 +79,10 @@ export async function POST( return NextResponse.json({ error: "作业不存在" }, { status: 404 }); } + if (file.size > MAX_FILE_SIZE_BYTES) { + return NextResponse.json({ error: "PDF 文件大小不能超过 20MB" }, { status: 400 }); + } + const studentIdRaw = parsed.data.studentId.trim(); const studentNameRaw = parsed.data.studentName.trim(); @@ -93,13 +103,6 @@ export async function POST( }, }); - if (existingSubmission) { - return NextResponse.json( - { error: "该学号已提交,请勿重复提交" }, - { status: 409 }, - ); - } - const arrayBuffer = await file.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); const studentId = studentIdRaw; @@ -121,18 +124,35 @@ export async function POST( let autoEvaluated = false; try { const uploadResult = await uploadAssignmentFile(buffer, file.name, assignmentId); + const previousFileKey = existingSubmission?.fileKey ?? null; - const submission = await prisma.submission.create({ - data: { - assignmentId, - studentId, - studentName, - originalFilename: file.name, - fileUrl: uploadResult.url, - fileKey: uploadResult.key, - formPayload: parsed.data.formPayload ?? undefined, - }, - }); + const submission = existingSubmission + ? await prisma.submission.update({ + where: { id: existingSubmission.id }, + data: { + studentId, + studentName, + originalFilename: file.name, + fileUrl: uploadResult.url, + fileKey: uploadResult.key, + formPayload: parsed.data.formPayload ?? undefined, + submittedAt: new Date(), + evaluationScore: null, + evaluationComment: null, + evaluatedAt: null, + }, + }) + : await prisma.submission.create({ + data: { + assignmentId, + studentId, + studentName, + originalFilename: file.name, + fileUrl: uploadResult.url, + fileKey: uploadResult.key, + formPayload: parsed.data.formPayload ?? undefined, + }, + }); const shouldAutoEvaluate = Boolean( assignment.autoEvaluate && assignment.gradingCriteria?.trim(), @@ -160,6 +180,13 @@ export async function POST( console.error("Auto evaluation failed", error); } } + if (previousFileKey) { + try { + await deleteAssignmentFile(previousFileKey); + } catch (error) { + console.error("Failed to delete previous submission file", error); + } + } } catch (error) { console.error("Failed to submit assignment", error); return NextResponse.json({ error: "上传失败" }, { status: 500 });