import "server-only"; import type { ProductPlanningApiConfig } from "./config"; import { fetchTaobaoDetailPlusItem } from "./detail-plus"; import { syncLegacyTaobaoDetailToPlanningDb } from "./planning-db"; import { extractMonthSale, normalizeJdItem, normalizeTaobaoItem, } from "./taobao"; import type { ProductPlanningMarketplaceItemRef, ProductPlanningMarketplaceItemsResult, ProductPlanningTaobaoItem, } from "./types"; const BATCH_CONCURRENCY = 4; async function fetchJson(url: string, label: string) { const response = await fetch(url, { signal: AbortSignal.timeout(15_000), headers: { Accept: "application/json" }, cache: "no-store", }); if (!response.ok) { throw new Error(`${label}请求失败:${response.status}`); } return response.json(); } async function fetchLegacyTaobaoItem(itemId: string, config: ProductPlanningApiConfig) { const apiKey = config.taobaoApiKey; const apiSecret = config.taobaoApiSecret; if (!apiKey || !apiSecret) { throw new Error("详情增强接口未命中,且缺少旧版淘宝 API Key / Secret,请前往系统设置配置"); } const primaryUrl = `${config.oneboundBaseUrl}/taobao/item_get_pro/?key=${encodeURIComponent(apiKey)}` + `&secret=${encodeURIComponent(apiSecret)}&num_iid=${encodeURIComponent(itemId)}`; const primaryData = await fetchJson(primaryUrl, "淘宝商品详情接口"); if (primaryData?.error_code && primaryData.error_code !== "0000") { throw new Error(primaryData.error || primaryData.reason || "淘宝商品详情接口返回错误"); } if (primaryData?.item) { return { rawItem: primaryData.item as Record, rawResponse: primaryData as Record, }; } const fallbackUrl = `${config.oneboundBaseUrl}/taobao/item_get/?key=${encodeURIComponent(apiKey)}` + `&secret=${encodeURIComponent(apiSecret)}&num_iid=${encodeURIComponent(itemId)}`; const fallbackData = await fetchJson(fallbackUrl, "淘宝商品详情接口"); if (fallbackData?.error_code && fallbackData.error_code !== "0000") { throw new Error(fallbackData.error || fallbackData.reason || "淘宝商品详情接口返回错误"); } if (!fallbackData?.item) { throw new Error("未找到商品信息"); } return { rawItem: fallbackData.item as Record, rawResponse: fallbackData as Record, }; } export async function fetchProductPlanningTaobaoItem( itemId: string, config: ProductPlanningApiConfig ) { const detailPlusResult = await fetchTaobaoDetailPlusItem(itemId, config); if (detailPlusResult.item) { return detailPlusResult.item; } const legacy = await fetchLegacyTaobaoItem(itemId, config); const normalizedItem = normalizeTaobaoItem(legacy.rawItem, itemId); await syncLegacyTaobaoDetailToPlanningDb({ config, requestedItemId: itemId, rawItem: legacy.rawItem, rawResponse: legacy.rawResponse, normalizedItem, }).catch((error) => { console.warn("[ProductPlanning][MarketplaceItems] sync fallback detail failed:", error); }); return normalizedItem; } export async function fetchProductPlanningJdItem( itemId: string, config: ProductPlanningApiConfig ) { const apiKey = config.taobaoApiKey; const apiSecret = config.taobaoApiSecret; if (!apiKey || !apiSecret) { throw new Error("缺少商品策划工具淘宝 API Key / Secret,请前往系统设置配置"); } const apiUrl = `${config.oneboundBaseUrl}/jd/item_get_app/?key=${encodeURIComponent(apiKey)}` + `&secret=${encodeURIComponent(apiSecret)}&num_iid=${encodeURIComponent(itemId)}&domain_type=jd`; const descApiUrl = `${config.oneboundBaseUrl}/jd/item_get_desc/?key=${encodeURIComponent(apiKey)}` + `&secret=${encodeURIComponent(apiSecret)}&num_iid=${encodeURIComponent(itemId)}&domain_type=jd`; const [data, descData] = await Promise.all([ fetchJson(apiUrl, "京东商品详情接口"), fetchJson(descApiUrl, "京东商品详情描述接口").catch(() => null), ]); if (data?.error_code && data.error_code !== "0000") { throw new Error(data.error || data.reason || "京东商品详情接口返回错误"); } if (!data?.item) { throw new Error("未找到商品信息"); } const descHtml = String(descData?.item?.desc || "").trim(); return normalizeJdItem( { ...data.item, desc: descHtml || data.item.desc, }, itemId ); } export async function fetchProductPlanningMonthSale( itemId: string, config: ProductPlanningApiConfig ) { const saleToken = config.saleToken; if (!saleToken) return null; const apiUrl = `${config.saleApiUrl}?itemId=${encodeURIComponent(itemId)}` + `&token=${encodeURIComponent(saleToken)}&v=3.0`; const data = await fetchJson(apiUrl, "月销量接口"); return extractMonthSale(data); } export async function fetchProductPlanningMarketplaceItem( reference: ProductPlanningMarketplaceItemRef, config: ProductPlanningApiConfig, options: { includeMonthSale?: boolean } = {} ): Promise { const item = reference.platform === "jd" ? await fetchProductPlanningJdItem(reference.itemId, config) : await fetchProductPlanningTaobaoItem(reference.itemId, config); if (reference.platform === "taobao" && options.includeMonthSale) { const recentSales = await fetchProductPlanningMonthSale(reference.itemId, config).catch((error) => { console.warn("[ProductPlanning][MarketplaceItems] month sale failed:", error); return null; }); if (typeof recentSales === "number") { item.recentSales = recentSales; } } if (reference.platform === "jd") { item.recentSales = item.recentSales ?? item.totalSales; } return item; } export async function fetchProductPlanningMarketplaceItemsBatch( references: ProductPlanningMarketplaceItemRef[], config: ProductPlanningApiConfig, options: { includeMonthSale?: boolean } = {} ) { const results: ProductPlanningMarketplaceItemsResult[] = new Array(references.length); let cursor = 0; async function worker() { while (cursor < references.length) { const index = cursor; cursor += 1; const reference = references[index]; try { const item = await fetchProductPlanningMarketplaceItem(reference, config, options); results[index] = { ...reference, success: true, item, }; } catch (error) { results[index] = { ...reference, success: false, error: error instanceof Error ? error.message : "获取商品详情失败", }; } } } await Promise.all( Array.from({ length: Math.min(BATCH_CONCURRENCY, references.length) }, () => worker()) ); return results; }