import { randomUUID } from "node:crypto"; import { NextRequest, NextResponse } from "next/server"; import { getDefaultStorageConfig, getSystemSetting, upsertSystemSetting } from "@/lib/db/queries"; import { PRODUCT_PLANNING_TEMPLATES_KEY, normalizeTemplateCategoryName, parseProductPlanningTemplates, upsertProductPlanningTemplate, } from "@/lib/product-planning/templates"; import { isProductPlanningEnabled, PRODUCT_PLANNING_DISABLED_MESSAGE, } from "@/lib/product-planning/server-feature"; import type { ProductPlanningTemplate, ProductPlanningTemplateListResponse, ProductPlanningTemplateMutationResponse, } from "@/lib/product-planning/types"; import { getProductPlanningToken } from "@/lib/product-planning/server-auth"; import { uploadFile } from "@/lib/storage/service"; export const runtime = "nodejs"; const MAX_TEMPLATE_FILE_SIZE = 10 * 1024 * 1024; const ALLOWED_TEMPLATE_EXTENSIONS = new Set(["txt", "csv", "xlsx"]); function isAdminToken(token: Awaited>) { const type = String(token?.type || "").trim(); return type === "admin" || type === "agent_admin"; } function jsonListError(status: number, error: string) { const payload: ProductPlanningTemplateListResponse = { success: false, templates: [], error, }; return NextResponse.json(payload, { status }); } function jsonMutationError(status: number, error: string) { const payload: ProductPlanningTemplateMutationResponse = { success: false, error, }; return NextResponse.json(payload, { status }); } function getTemplateExtension(fileName: string) { return fileName.toLowerCase().split(".").pop() || ""; } async function readTemplates() { const setting = await getSystemSetting({ key: PRODUCT_PLANNING_TEMPLATES_KEY }); return parseProductPlanningTemplates(setting?.value); } async function tryDeleteLegacyTemplateFile(template: ProductPlanningTemplate) { if (!template.filePath) return; try { const storageConfig = await getDefaultStorageConfig().catch(() => undefined); if (!storageConfig || storageConfig.type !== "s3") return; const { deleteS3ObjectByPath, toDbS3UploadConfig } = await import("@/lib/storage/service"); const uploadConfig = toDbS3UploadConfig(storageConfig); if (!uploadConfig) return; await deleteS3ObjectByPath(template.filePath, uploadConfig); } catch (error) { console.warn("[ProductPlanning][Templates] failed to delete legacy template file:", error); } } export async function GET(request: NextRequest) { const token = await getProductPlanningToken(request); if (!token?.sub) { return jsonListError(401, "未登录"); } if (!(await isProductPlanningEnabled())) { return jsonListError(403, PRODUCT_PLANNING_DISABLED_MESSAGE); } try { const templates = await readTemplates(); return NextResponse.json({ success: true, templates, } satisfies ProductPlanningTemplateListResponse); } catch (error) { console.error("[ProductPlanning][Templates] list failed:", error); return jsonListError(503, "模板库读取失败"); } } export async function POST(request: NextRequest) { const token = await getProductPlanningToken(request); if (!token?.sub) { return jsonMutationError(401, "未登录"); } if (!(await isProductPlanningEnabled())) { return jsonMutationError(403, PRODUCT_PLANNING_DISABLED_MESSAGE); } if (!isAdminToken(token)) { return jsonMutationError(403, "需要管理员权限"); } let formData: FormData; try { formData = await request.formData(); } catch { return jsonMutationError(400, "上传表单解析失败"); } const categoryName = normalizeTemplateCategoryName(formData.get("categoryName")); const description = String(formData.get("description") || "").trim(); const file = formData.get("file"); if (!categoryName) { return jsonMutationError(400, "缺少模板名称"); } if (categoryName.length > 50) { return jsonMutationError(400, "模板名称不能超过 50 个字符"); } if (!(file instanceof File)) { return jsonMutationError(400, "缺少模板文件"); } const extension = getTemplateExtension(file.name); if (!ALLOWED_TEMPLATE_EXTENSIONS.has(extension)) { return jsonMutationError(400, "模板文件仅支持 txt、csv、xlsx"); } if (file.size <= 0) { return jsonMutationError(400, "模板文件不能为空"); } if (file.size > MAX_TEMPLATE_FILE_SIZE) { return jsonMutationError(400, "模板文件不能超过 10MB"); } try { const buffer = await file.arrayBuffer(); const uploaded = await uploadFile(file.name, buffer); const now = new Date().toISOString(); const nextTemplate: ProductPlanningTemplate = { id: randomUUID(), categoryName, description: description || undefined, fileName: file.name, fileUrl: uploaded.url, filePath: uploaded.pathname, fileSize: file.size, contentType: uploaded.contentType || file.type || undefined, createdAt: now, updatedAt: now, uploadedById: String(token.sub), }; const currentTemplates = await readTemplates(); const replacedTemplates = currentTemplates.filter( (template) => normalizeTemplateCategoryName(template.categoryName) === normalizeTemplateCategoryName(categoryName) ); const templates = upsertProductPlanningTemplate(currentTemplates, nextTemplate); await upsertSystemSetting({ key: PRODUCT_PLANNING_TEMPLATES_KEY, value: templates, updatedBy: String(token.sub), }); await Promise.all(replacedTemplates.map((template) => tryDeleteLegacyTemplateFile(template))); return NextResponse.json({ success: true, template: nextTemplate, } satisfies ProductPlanningTemplateMutationResponse); } catch (error) { console.error("[ProductPlanning][Templates] upload failed:", error); return jsonMutationError(502, "模板上传失败"); } }