188 lines
5.9 KiB
TypeScript

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<ReturnType<typeof getProductPlanningToken>>) {
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, "模板上传失败");
}
}