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

134 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextRequest, NextResponse } from "next/server";
import { getResolvedProductPlanningApiConfig } from "@/lib/product-planning/server-config";
import {
isProductPlanningEnabled,
PRODUCT_PLANNING_DISABLED_MESSAGE,
} from "@/lib/product-planning/server-feature";
import { getProductPlanningToken } from "@/lib/product-planning/server-auth";
import type {
ProductPlanningKeywordInsightTag,
ProductPlanningKeywordInsightGroup,
ProductPlanningKeywordInsightResponse,
ProductPlanningKeywordInsightSuggestion,
} from "@/lib/product-planning/types";
export const runtime = "nodejs";
function jsonError(status: number, keyword: string, error: string) {
const payload: ProductPlanningKeywordInsightResponse = {
success: false,
keyword,
suggestions: [],
groups: [],
error,
};
return NextResponse.json(payload, { status });
}
function normalizeSuggestions(items: unknown) {
if (!Array.isArray(items)) return [] as ProductPlanningKeywordInsightSuggestion[];
return items
.map((entry, index) => {
if (!Array.isArray(entry) || entry.length < 2) return null;
const keyword = String(entry[0] || "").trim();
const weight = Number.parseFloat(String(entry[1] || "0"));
if (!keyword) return null;
return {
keyword,
weight: Number.isFinite(weight) ? weight : 0,
rank: index + 1,
} satisfies ProductPlanningKeywordInsightSuggestion;
})
.filter((item): item is ProductPlanningKeywordInsightSuggestion => Boolean(item));
}
function normalizeGroups(items: unknown) {
if (!Array.isArray(items)) return [] as ProductPlanningKeywordInsightGroup[];
return items
.map((group) => {
if (!group || typeof group !== "object") return null;
const record = group as Record<string, unknown>;
const tagBuckets = Array.isArray(record.data) ? record.data : [];
const tags = tagBuckets.flatMap((bucket) =>
Array.isArray(bucket)
? bucket
.map((tag) => {
if (!tag || typeof tag !== "object") return null;
const tagRecord = tag as Record<string, unknown>;
const title = String(tagRecord.title || "").trim();
if (!title) return null;
return {
title,
type: String(tagRecord.type || "").toLowerCase() === "hot" ? "hot" : "normal",
} as const;
})
.filter(
(tag): tag is ProductPlanningKeywordInsightTag => Boolean(tag)
)
: []
);
return {
index: Number.parseInt(String(record.index || "0"), 10) || 0,
type: String(record.type || "tag_group").trim() || "tag_group",
tags,
} satisfies ProductPlanningKeywordInsightGroup;
})
.filter((item): item is ProductPlanningKeywordInsightGroup => Boolean(item));
}
export async function GET(request: NextRequest) {
const token = await getProductPlanningToken(request);
if (!token?.sub) {
return jsonError(401, "", "未登录");
}
if (!(await isProductPlanningEnabled())) {
return jsonError(403, "", PRODUCT_PLANNING_DISABLED_MESSAGE);
}
const keyword = String(request.nextUrl.searchParams.get("q") || "").trim();
if (!keyword) {
return jsonError(400, "", "缺少关键词参数");
}
const config = await getResolvedProductPlanningApiConfig();
const apiKey = config.taobaoApiKey;
const apiSecret = config.taobaoApiSecret;
if (!apiKey || !apiSecret) {
return jsonError(500, keyword, "缺少商品策划工具淘宝 API Key / Secret,请前往系统设置配置");
}
const apiUrl =
`${config.oneboundBaseUrl}/taobao/item_search_suggest/?key=${encodeURIComponent(apiKey)}` +
`&secret=${encodeURIComponent(apiSecret)}&q=${encodeURIComponent(keyword)}&cache=yes`;
try {
const response = await fetch(apiUrl, {
signal: AbortSignal.timeout(15000),
headers: { Accept: "application/json" },
cache: "no-store",
});
if (!response.ok) {
return jsonError(502, keyword, `关键词洞察接口请求失败:${response.status}`);
}
const data = await response.json();
if (data?.error_code && data.error_code !== "0000") {
return jsonError(502, keyword, data.error || data.reason || "关键词洞察接口返回错误");
}
return NextResponse.json({
success: true,
keyword,
suggestions: normalizeSuggestions(data?.items?.result),
groups: normalizeGroups(data?.items?.magic),
} satisfies ProductPlanningKeywordInsightResponse);
} catch (error) {
console.error("[ProductPlanning][KeywordInsight] request failed:", error);
return jsonError(502, keyword, "关键词洞察接口调用失败");
}
}