134 lines
4.7 KiB
TypeScript
134 lines
4.7 KiB
TypeScript
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, "关键词洞察接口调用失败");
|
||
}
|
||
}
|