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 = {}; 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 }, ); } }