feat(submission): allow resubmission and add file size limit

This commit is contained in:
gameloader
2025-11-18 14:00:31 +08:00
parent 35b4d7470f
commit 8ec8a70c3b

View File

@ -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<string, unknown> & { formPayload?: unknown } = {};
try {
if (metadataRaw) {
const parsedMetadata = JSON.parse(String(metadataRaw));
if (parsedMetadata && typeof parsedMetadata === "object" && !Array.isArray(parsedMetadata)) {
metadata = parsedMetadata as Record<string, unknown>;
}
}
} 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 });