Files
cet6-90-day-checkin/app/api/backup/route.ts
T

58 lines
4.6 KiB
TypeScript

import { eq } from "drizzle-orm";
import { getDb } from "../../../db";
import { mockExams, progress, studyDays } from "../../../db/schema";
const user = (r: Request) => r.headers.get("oai-authenticated-user-id");
const validDate = (value: unknown) => typeof value === "string" && /^2026-(09|10|11|12)-\d{2}$/.test(value);
const safeText = (value: unknown, max: number) => typeof value === "string" ? value.slice(0, max) : "";
const taskKeys = new Set(["vocab", "listening", "reading", "output", "review"]);
export async function GET(r: Request) {
const id = user(r);
if (!id) return Response.json({ error: "请登录后再导出数据。" }, { status: 401 });
try {
const db = getDb();
const [allProgress, allStudyDays, allMockExams] = await Promise.all([
db.select().from(progress).where(eq(progress.userId, id)),
db.select().from(studyDays).where(eq(studyDays.userId, id)),
db.select().from(mockExams).where(eq(mockExams.userId, id)),
]);
return Response.json({ format: "cet6-checkin-backup", version: 1, exportedAt: new Date().toISOString(), data: {
progress: allProgress.map(({ userId, ...row }) => row),
studyDays: allStudyDays.map(({ userId, ...row }) => row),
mockExams: allMockExams.map(({ userId, ...row }) => row),
} });
} catch { return Response.json({ error: "暂时无法导出云端数据。" }, { status: 500 }); }
}
export async function POST(r: Request) {
const id = user(r);
if (!id) return Response.json({ error: "请登录后再导入数据。" }, { status: 401 });
let backup: any;
try { backup = await r.json(); } catch { return Response.json({ error: "文件不是有效的 JSON。" }, { status: 400 }); }
const data = backup?.data;
if (backup?.format !== "cet6-checkin-backup" || backup?.version !== 1 || !data || !Array.isArray(data.progress) || !Array.isArray(data.studyDays) || !Array.isArray(data.mockExams) || data.progress.length > 90 || data.studyDays.length > 90 || data.mockExams.length > 100) return Response.json({ error: "这不是可识别的六级打卡备份文件。" }, { status: 400 });
try {
const db = getDb(), now = new Date().toISOString();
for (const item of data.progress) {
if (!validDate(item?.planDate) || !Array.isArray(item?.doneTasks) || item.doneTasks.some((key: unknown) => typeof key !== "string" || !taskKeys.has(key))) return Response.json({ error: "打卡记录中含有无效数据。" }, { status: 400 });
const values = { userId: id, planDate: item.planDate, doneTasks: JSON.stringify([...new Set(item.doneTasks)]), mood: safeText(item.mood, 20), note: safeText(item.note, 1000), checkinType: item.checkinType === "makeup" ? "makeup" : "normal", checkedAt: safeText(item.checkedAt, 40) || now, updatedAt: now };
await db.insert(progress).values(values).onConflictDoUpdate({ target: [progress.userId, progress.planDate], set: values });
}
for (const item of data.studyDays) {
if (!validDate(item?.planDate)) return Response.json({ error: "学习记录中含有无效日期。" }, { status: 400 });
let durations: Record<string, number> = {}; try { durations = typeof item.durations === "string" ? JSON.parse(item.durations) : item.durations || {}; } catch { return Response.json({ error: "学习用时格式不正确。" }, { status: 400 }); }
const cleanDurations = Object.fromEntries(Object.entries(durations).map(([key, value]) => [key, Math.max(0, Math.min(600, Number(value) || 0))]));
const values = { userId: id, planDate: item.planDate, durations: JSON.stringify(cleanDurations), weeklyReview: safeText(item.weeklyReview, 1500), nextFocus: safeText(item.nextFocus, 400), updatedAt: now };
await db.insert(studyDays).values(values).onConflictDoUpdate({ target: [studyDays.userId, studyDays.planDate], set: values });
}
for (const item of data.mockExams) {
if (!validDate(item?.examDate)) return Response.json({ error: "模考记录中含有无效日期。" }, { status: 400 });
const score = (value: unknown) => safeText(value, 16).replace(/[^\d.]/g, "").slice(0, 6);
const values = { userId: id, examDate: item.examDate, listening: score(item.listening), reading: score(item.reading), writing: score(item.writing), translation: score(item.translation), note: safeText(item.note, 1000), updatedAt: now };
await db.insert(mockExams).values(values).onConflictDoUpdate({ target: [mockExams.userId, mockExams.examDate], set: values });
}
return Response.json({ imported: { progress: data.progress.length, studyDays: data.studyDays.length, mockExams: data.mockExams.length } });
} catch { return Response.json({ error: "导入失败,云端数据未能完整更新。" }, { status: 500 }); }
}