feat: initialize AutoTeacher Next.js project structure
This commit is contained in:
131
src/app/api/assignments/[assignmentId]/export/route.ts
Normal file
131
src/app/api/assignments/[assignmentId]/export/route.ts
Normal file
@ -0,0 +1,131 @@
|
||||
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",
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user