132 lines
4.2 KiB
TypeScript
132 lines
4.2 KiB
TypeScript
import { Buffer } from "node:buffer";
|
|
import { auth } from "@clerk/nextjs/server";
|
|
import { NextResponse } from "next/server";
|
|
import * as XLSX from "xlsx";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { getNumericParam } from "@/lib/route-params";
|
|
|
|
type AssignmentRouteParams = { assignmentId: string | string[] | undefined };
|
|
|
|
const EXPORTABLE_COLUMNS = {
|
|
studentId: { header: "学号", getter: (submission: any) => submission.studentId },
|
|
studentName: { header: "姓名", getter: (submission: any) => submission.studentName },
|
|
originalFilename: {
|
|
header: "文件名",
|
|
getter: (submission: any) => submission.originalFilename,
|
|
},
|
|
fileUrl: { header: "文件地址", getter: (submission: any) => submission.fileUrl },
|
|
submittedAt: {
|
|
header: "提交时间",
|
|
getter: (submission: any) =>
|
|
submission.submittedAt ? new Date(submission.submittedAt).toISOString() : "",
|
|
},
|
|
evaluationScore: {
|
|
header: "得分",
|
|
getter: (submission: any) =>
|
|
typeof submission.evaluationScore === "number"
|
|
? submission.evaluationScore
|
|
: "",
|
|
},
|
|
evaluationComment: {
|
|
header: "评价评语",
|
|
getter: (submission: any) => submission.evaluationComment ?? "",
|
|
},
|
|
evaluatedAt: {
|
|
header: "评价时间",
|
|
getter: (submission: any) =>
|
|
submission.evaluatedAt ? new Date(submission.evaluatedAt).toISOString() : "",
|
|
},
|
|
} as const;
|
|
|
|
const DEFAULT_COLUMNS = ["studentId", "evaluationScore"] as Array<keyof typeof EXPORTABLE_COLUMNS>;
|
|
|
|
export async function GET(
|
|
request: Request,
|
|
{ params }: { params: Promise<AssignmentRouteParams> },
|
|
) {
|
|
const resolvedParams = await params;
|
|
const assignmentId = getNumericParam(resolvedParams.assignmentId);
|
|
if (Number.isNaN(assignmentId)) {
|
|
return NextResponse.json({ error: "作业不存在" }, { status: 400 });
|
|
}
|
|
|
|
const { userId } = await auth();
|
|
if (!userId) {
|
|
return NextResponse.json({ error: "未授权" }, { status: 401 });
|
|
}
|
|
|
|
const assignment = await prisma.assignment.findFirst({
|
|
where: { id: assignmentId, teacherId: userId },
|
|
select: { id: true, title: true },
|
|
});
|
|
|
|
if (!assignment) {
|
|
return NextResponse.json({ error: "未找到作业" }, { status: 404 });
|
|
}
|
|
|
|
const url = new URL(request.url);
|
|
const columnsParam = url.searchParams.get("columns");
|
|
const requestedColumns = columnsParam
|
|
? columnsParam
|
|
.split(",")
|
|
.map((item) => item.trim())
|
|
.filter((item) => item.length > 0)
|
|
: null;
|
|
|
|
const selectedColumns =
|
|
requestedColumns && requestedColumns.length > 0
|
|
? requestedColumns.filter(
|
|
(key): key is keyof typeof EXPORTABLE_COLUMNS => key in EXPORTABLE_COLUMNS,
|
|
)
|
|
: DEFAULT_COLUMNS;
|
|
|
|
if (!selectedColumns.length) {
|
|
return NextResponse.json({ error: "请选择有效的导出列" }, { status: 400 });
|
|
}
|
|
|
|
const submissions = await prisma.submission.findMany({
|
|
where: { assignmentId },
|
|
orderBy: { submittedAt: "asc" },
|
|
});
|
|
|
|
const worksheetData = submissions.map((submission) => {
|
|
const row: Record<string, unknown> = {};
|
|
for (const column of selectedColumns) {
|
|
const { header, getter } = EXPORTABLE_COLUMNS[column];
|
|
row[header] = getter(submission);
|
|
}
|
|
return row;
|
|
});
|
|
|
|
if (worksheetData.length === 0) {
|
|
worksheetData.push(
|
|
Object.fromEntries(
|
|
selectedColumns.map((column) => [
|
|
EXPORTABLE_COLUMNS[column].header,
|
|
"",
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
|
|
const worksheet = XLSX.utils.json_to_sheet(worksheetData);
|
|
const workbook = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(workbook, worksheet, "Submissions");
|
|
const buffer = XLSX.write(workbook, { bookType: "xlsx", type: "buffer" }) as Buffer;
|
|
|
|
const filenameSafeTitle = assignment.title
|
|
? assignment.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "_")
|
|
: `assignment_${assignmentId}`;
|
|
const filename = `${filenameSafeTitle}_submissions.xlsx`;
|
|
|
|
return new NextResponse(buffer, {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
"Content-Disposition": `attachment; filename="${encodeURIComponent(filename)}"`,
|
|
"Content-Length": buffer.length.toString(),
|
|
"Cache-Control": "no-store",
|
|
},
|
|
});
|
|
}
|