新增解答题拍照判分:百炼 Qwen-VL 按评分点逐点判分,无 API Key 时演示判分回退;混合卷按真实分值加权计分;判分照片与评分点进错题本;补齐历史迁移缺口(0005 模板生效方式/0006 五张缺失表+grammar_progress point_id)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jacobxu666
2026-08-17 16:24:23 +08:00
co-authored by Claude Opus 4.8
parent 4033abc21d
commit f80a931ef0
21 changed files with 1377 additions and 45 deletions
+49
View File
@@ -0,0 +1,49 @@
// xuchao: 本地开发照片回退——OSS 未配置时,解答题照片直传/查看走本地磁盘
// 生产环境配置了 OSS 或 NODE_ENV=production 时,本路由一律 404
import { NextRequest, NextResponse } from "next/server";
import fs from "node:fs";
import path from "node:path";
import { ossConfigured } from "@/lib/oss";
const BASE = path.join(process.cwd(), ".dev-uploads");
function disabled() {
return ossConfigured() || process.env.NODE_ENV === "production";
}
// xuchao: 键名白名单校验 + 防路径穿越
function safePath(key: string): string | null {
if (!/^exam-solve\/\d+\/[\w-]+\/q\d+-[0-9a-f]{16}\.jpg$/.test(key)) return null;
const p = path.normalize(path.join(BASE, key));
return p.startsWith(BASE + path.sep) ? p : null;
}
// xuchao: 直传入口:multipart file + key
export async function POST(req: NextRequest) {
if (disabled()) return NextResponse.json({ error: "not available" }, { status: 404 });
const form = await req.formData();
const file = form.get("file");
const key = form.get("key");
if (!(file instanceof File) || typeof key !== "string") {
return NextResponse.json({ error: "缺少文件或键名" }, { status: 400 });
}
const target = safePath(key);
if (!target) return NextResponse.json({ error: "键名不合法" }, { status: 400 });
fs.mkdirSync(path.dirname(target), { recursive: true });
const buf = Buffer.from(await file.arrayBuffer());
if (buf.byteLength > 8 * 1024 * 1024) {
return NextResponse.json({ error: "照片过大" }, { status: 400 });
}
fs.writeFileSync(target, buf);
return NextResponse.json({ ok: true, key });
}
// xuchao: 查看入口:?key=xxx
export async function GET(req: NextRequest) {
if (disabled()) return NextResponse.json({ error: "not available" }, { status: 404 });
const key = req.nextUrl.searchParams.get("key") ?? "";
const target = safePath(key);
if (!target || !fs.existsSync(target)) return NextResponse.json({ error: "不存在" }, { status: 404 });
const buf = fs.readFileSync(target);
return new NextResponse(buf, { headers: { "Content-Type": "image/jpeg" } });
}