feat: initialize AutoTeacher Next.js project structure

This commit is contained in:
gameloader
2025-11-10 19:07:47 +08:00
commit 4cb9db1b7c
34 changed files with 8032 additions and 0 deletions

View 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",
},
});
}