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; export async function GET( request: Request, { params }: { params: Promise }, ) { 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 = {}; 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", }, }); }