Your Name 478562c312
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: migrate product planning workspace
2026-03-26 16:38:48 +08:00

144 lines
4.5 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { getDefaultStorageConfig, getSystemSetting, upsertSystemSetting } from "@/lib/db/queries";
import {
PRODUCT_PLANNING_TEMPLATES_KEY,
parseProductPlanningTemplates,
} from "@/lib/product-planning/templates";
import {
isProductPlanningEnabled,
PRODUCT_PLANNING_DISABLED_MESSAGE,
} from "@/lib/product-planning/server-feature";
import { getProductPlanningToken } from "@/lib/product-planning/server-auth";
import type { ProductPlanningTemplateMutationResponse } from "@/lib/product-planning/types";
export const runtime = "nodejs";
function isAdminToken(token: Awaited<ReturnType<typeof getProductPlanningToken>>) {
const type = String(token?.type || "").trim();
return type === "admin" || type === "agent_admin";
}
function jsonError(status: number, error: string) {
const payload: ProductPlanningTemplateMutationResponse = {
success: false,
error,
};
return NextResponse.json(payload, { status });
}
function encodeAttachmentFileName(fileName: string) {
const fallback = fileName.replace(/[^\x20-\x7E]+/g, "_") || "template.xlsx";
return `attachment; filename="${fallback}"; filename*=UTF-8''${encodeURIComponent(fileName)}`;
}
async function readTemplates() {
const setting = await getSystemSetting({ key: PRODUCT_PLANNING_TEMPLATES_KEY });
return parseProductPlanningTemplates(setting?.value);
}
async function tryDeleteTemplateFile(filePath?: string) {
if (!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(filePath, uploadConfig);
} catch (error) {
console.warn("[ProductPlanning][Templates] delete file failed:", error);
}
}
export async function GET(
request: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const token = await getProductPlanningToken(request);
if (!token?.sub) {
return jsonError(401, "未登录");
}
if (!(await isProductPlanningEnabled())) {
return jsonError(403, PRODUCT_PLANNING_DISABLED_MESSAGE);
}
try {
const { id } = await context.params;
const templates = await readTemplates();
const template = templates.find((entry) => entry.id === id);
if (!template) {
return jsonError(404, "未找到模板");
}
const response = await fetch(template.fileUrl, {
headers: {
Accept: template.contentType || "*/*",
},
cache: "no-store",
signal: AbortSignal.timeout(20000),
});
if (!response.ok) {
return jsonError(502, "模板文件下载失败");
}
const buffer = await response.arrayBuffer();
return new NextResponse(buffer, {
status: 200,
headers: {
"Content-Type": template.contentType || response.headers.get("content-type") || "application/octet-stream",
"Content-Length": String(buffer.byteLength),
"Content-Disposition": encodeAttachmentFileName(template.fileName),
"Cache-Control": "no-store",
},
});
} catch (error) {
console.error("[ProductPlanning][Templates] download failed:", error);
return jsonError(502, "模板文件下载失败");
}
}
export async function DELETE(
request: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const token = await getProductPlanningToken(request);
if (!token?.sub) {
return jsonError(401, "未登录");
}
if (!(await isProductPlanningEnabled())) {
return jsonError(403, PRODUCT_PLANNING_DISABLED_MESSAGE);
}
if (!isAdminToken(token)) {
return jsonError(403, "需要管理员权限");
}
try {
const { id } = await context.params;
const templates = await readTemplates();
const template = templates.find((entry) => entry.id === id);
if (!template) {
return jsonError(404, "未找到模板");
}
await upsertSystemSetting({
key: PRODUCT_PLANNING_TEMPLATES_KEY,
value: templates.filter((entry) => entry.id !== id),
updatedBy: String(token.sub),
});
await tryDeleteTemplateFile(template.filePath);
return NextResponse.json({
success: true,
} satisfies ProductPlanningTemplateMutationResponse);
} catch (error) {
console.error("[ProductPlanning][Templates] delete failed:", error);
return jsonError(502, "模板删除失败");
}
}