103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
|
|
import { zipSync, strToU8 } from "fflate";
|
|||
|
|
import { adminForbidden, isAdminRequest } from "../../../lib/admin-auth";
|
|||
|
|
import { ensureSchema, getRawDb, getUploadBucket } from "../../../lib/mvp-db";
|
|||
|
|
import { parseResultScreenshotKeys } from "../../../lib/result-screenshots";
|
|||
|
|
|
|||
|
|
function safeName(value: string) {
|
|||
|
|
return value.replace(/[\\/:*?"<>|\r\n]/g, "_").trim().slice(0, 60) || "KOC";
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function csvCell(value: unknown) {
|
|||
|
|
const text = String(value ?? "");
|
|||
|
|
return `"${text.replaceAll('"', '""')}"`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export async function GET(request: Request) {
|
|||
|
|
if (!(await isAdminRequest(request))) return adminForbidden();
|
|||
|
|
try {
|
|||
|
|
await ensureSchema();
|
|||
|
|
const taskId = new URL(request.url).searchParams.get("task")?.trim();
|
|||
|
|
if (!taskId) {
|
|||
|
|
return Response.json({ error: "缺少任务参数" }, { status: 400 });
|
|||
|
|
}
|
|||
|
|
const task = await getRawDb()
|
|||
|
|
.prepare("SELECT name, task_type FROM tasks WHERE id = ?")
|
|||
|
|
.bind(taskId)
|
|||
|
|
.first<{ name: string; task_type: string }>();
|
|||
|
|
if (!task || task.task_type !== "screenshot_collect") {
|
|||
|
|
return Response.json({ error: "没有找到截图回收任务" }, { status: 404 });
|
|||
|
|
}
|
|||
|
|
const rows = await getRawDb()
|
|||
|
|
.prepare(
|
|||
|
|
`SELECT d.id, d.result_screenshot_key, d.result_submitted_at,
|
|||
|
|
d.claimed_at, c.source_row, c.title, p.name AS partner_name,
|
|||
|
|
cl.claimant_name
|
|||
|
|
FROM distributions d
|
|||
|
|
JOIN contents c ON c.id = d.content_id
|
|||
|
|
JOIN partners p ON p.id = d.partner_id
|
|||
|
|
LEFT JOIN claims cl ON cl.id = d.claim_id
|
|||
|
|
WHERE d.task_id = ?
|
|||
|
|
ORDER BY COALESCE(c.source_row, 999999), d.claimed_at, d.id`,
|
|||
|
|
)
|
|||
|
|
.bind(taskId)
|
|||
|
|
.all<{
|
|||
|
|
id: string;
|
|||
|
|
result_screenshot_key: string | null;
|
|||
|
|
result_submitted_at: string | null;
|
|||
|
|
claimed_at: string;
|
|||
|
|
source_row: number | null;
|
|||
|
|
title: string;
|
|||
|
|
partner_name: string;
|
|||
|
|
claimant_name: string | null;
|
|||
|
|
}>();
|
|||
|
|
const files: Record<string, Uint8Array> = {};
|
|||
|
|
const manifest = [
|
|||
|
|
["序号", "搜索关键词", "领取人", "领取时间", "提交时间", "文件名"],
|
|||
|
|
];
|
|||
|
|
let exported = 0;
|
|||
|
|
for (const [rowIndex, row] of rows.results.entries()) {
|
|||
|
|
const fileNames: string[] = [];
|
|||
|
|
const screenshotKeys = row.result_submitted_at
|
|||
|
|
? parseResultScreenshotKeys(row.result_screenshot_key)
|
|||
|
|
: [];
|
|||
|
|
for (const [imageIndex, screenshotKey] of screenshotKeys.entries()) {
|
|||
|
|
const object = await getUploadBucket().get(screenshotKey);
|
|||
|
|
if (!object) continue;
|
|||
|
|
const extension = screenshotKey.split(".").at(-1) || "jpg";
|
|||
|
|
const fileName = `${String(row.source_row ?? rowIndex + 1).padStart(3, "0")}-${safeName(row.claimant_name || row.partner_name)}-${imageIndex + 1}.${extension}`;
|
|||
|
|
files[fileName] = new Uint8Array(await object.arrayBuffer());
|
|||
|
|
fileNames.push(fileName);
|
|||
|
|
exported += 1;
|
|||
|
|
}
|
|||
|
|
manifest.push([
|
|||
|
|
String(row.source_row ?? ""),
|
|||
|
|
row.title,
|
|||
|
|
row.claimant_name || row.partner_name,
|
|||
|
|
row.claimed_at,
|
|||
|
|
row.result_submitted_at || "",
|
|||
|
|
fileNames.join(";"),
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
files["回收清单.csv"] = strToU8(
|
|||
|
|
`\uFEFF${manifest.map((cells) => cells.map(csvCell).join(",")).join("\r\n")}`,
|
|||
|
|
);
|
|||
|
|
const archive = zipSync(files, { level: 0 });
|
|||
|
|
const headers = new Headers({
|
|||
|
|
"Content-Type": "application/zip",
|
|||
|
|
"Content-Disposition": `attachment; filename*=UTF-8''${encodeURIComponent(`${safeName(task.name)}-截图回收.zip`)}`,
|
|||
|
|
"Cache-Control": "private, no-store",
|
|||
|
|
"X-KOC-Exported-Count": String(exported),
|
|||
|
|
});
|
|||
|
|
const body = archive.buffer.slice(
|
|||
|
|
archive.byteOffset,
|
|||
|
|
archive.byteOffset + archive.byteLength,
|
|||
|
|
) as ArrayBuffer;
|
|||
|
|
return new Response(body, { headers });
|
|||
|
|
} catch (error) {
|
|||
|
|
return Response.json(
|
|||
|
|
{ error: error instanceof Error ? error.message : "截图打包失败" },
|
|||
|
|
{ status: 500 },
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|