试卷题目配图(几何体):编辑器上传区+孩子端渲染+文件名校验+public挂卷持久化
test / test (push) Successful in 37s

uploadPaperFigure 存 public/exam/<卷>/<uuid>.ext;编辑器预览/移除;
quiz-block 渲染 q.figure;compose 挂 exam-figures 卷防重建丢失。

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
jacobxu666
2026-08-20 16:08:27 +08:00
co-authored by Claude Haiku 4.5
parent 890e7ef3ac
commit c706f485cc
8 changed files with 123 additions and 7 deletions
+24
View File
@@ -1,6 +1,8 @@
"use server";
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { and, eq, sql } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { z } from "zod";
@@ -392,3 +394,25 @@ export async function deleteExamPaper(paperId: string): Promise<void> {
revalidatePath("/parent/exams");
revalidatePath("/exam");
}
// xuchao: 上传试卷题目配图(几何体等)——存 public/exam/<paperId>/<uuid>.<ext>,返回文件名写回 q.figure
// 生产容器 public/exam 挂卷持久化(compose volumes: exam-figures),重建不丢
export async function uploadPaperFigure(
paperId: string,
file: File,
): Promise<{ filename?: string; error?: string }> {
const parent = await getSessionUser();
if (!parent || parent.role !== "parent") return { error: "请先以家长身份登录" };
if (!/^[a-z0-9-]+$/.test(paperId)) return { error: "试卷编号不合法" };
if (!file.type.startsWith("image/")) return { error: "只能上传图片文件" };
const ext = file.name.toLowerCase().split(".").pop();
if (!["png", "jpg", "jpeg", "webp"].includes(ext ?? "")) return { error: "只支持 png/jpg/webp 图片" };
if (file.size > 8 * 1024 * 1024) return { error: "图片不能超过 8MB" };
const buf = Buffer.from(await file.arrayBuffer());
const filename = `${crypto.randomUUID()}.${ext}`;
const dir = path.join(process.cwd(), "public", "exam", paperId);
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, filename), buf);
return { filename };
}