import type { ProductPlanningPropertyRow, ProductPlanningSkuRow, ProductPlanningTaobaoItem, } from "./types"; import { ensureHttpsUrl, normalizeText, parseInteger, parseRangeValue } from "./utils"; function normalizePrice(value: unknown) { const parsed = parseRangeValue(value); return typeof parsed === "number" ? parsed : null; } function normalizeImageList(item: any) { if (!Array.isArray(item?.item_imgs)) return [] as string[]; return item.item_imgs .map((entry: unknown) => { if (typeof entry === "string") return ensureHttpsUrl(entry); if (entry && typeof entry === "object") { const url = (entry as Record).url; return typeof url === "string" ? ensureHttpsUrl(url) : ""; } return ""; }) .filter(Boolean); } function normalizeMixedImageList(value: unknown) { if (!Array.isArray(value)) return [] as string[]; return value .map((entry: unknown) => { if (typeof entry === "string") return ensureHttpsUrl(entry); if (entry && typeof entry === "object") { const record = entry as Record; return ensureHttpsUrl(record.url || record.img || record.image || ""); } return ""; }) .filter(Boolean); } function extractDetailImagesFromHtml(value: unknown) { const html = String(value || ""); if (!html) return [] as string[]; const matched = html.match(/(?:data-lazyload|src)=["']([^"']+\.(?:jpg|jpeg|png|gif|webp)(?:[^"']*)?)["']/gi); if (!matched) return [] as string[]; const urls = matched .map((entry) => { const urlMatch = entry.match(/["']([^"']+)["']/); return ensureHttpsUrl(urlMatch?.[1] || ""); }) .filter(Boolean); return Array.from(new Set(urls)); } function normalizeDetailImages(item: any) { const descImages = normalizeMixedImageList(item?.desc_img); if (descImages.length > 0) return Array.from(new Set(descImages)); return extractDetailImagesFromHtml(item?.desc); } function normalizeSkuList(item: any) { const skuRows = Array.isArray(item?.skus?.sku) ? item.skus.sku : []; const result: ProductPlanningSkuRow[] = []; for (const sku of skuRows) { const name = normalizeText(sku?.properties_name) || normalizeText(sku?.properties) || normalizeText(sku?.sku_id) || "默认规格"; result.push({ name, price: normalizePrice(sku?.price), }); } return result; } function normalizeJdSkuList(item: any) { const skuRows = Array.isArray(item?.skus) ? item.skus : []; const result: ProductPlanningSkuRow[] = []; for (const sku of skuRows) { const specs = Array.isArray(sku?.specs) ? sku.specs : []; const specNames = specs.flatMap((spec: any) => Array.isArray(spec?.values) ? spec.values .map((entry: any) => normalizeText(entry?.vname)) .filter(Boolean) : [] ); const name = specNames.join(" / ") || normalizeText(sku?.name) || normalizeText(sku?.sku_id) || "默认规格"; result.push({ name, price: normalizePrice(sku?.price ?? item?.price), }); } return result; } function normalizePropertyList(item: any) { const result: ProductPlanningPropertyRow[] = []; if (Array.isArray(item?.props)) { for (const prop of item.props) { const name = normalizeText(prop?.name); const value = normalizeText(prop?.value); if (!name || !value) continue; result.push({ name, value }); } } if (item?.props_list && typeof item.props_list === "object") { for (const [name, value] of Object.entries(item.props_list)) { const normalizedName = normalizeText(name); const normalizedValue = normalizeText(value); if (!normalizedName || !normalizedValue) continue; if (result.some((entry) => entry.name === normalizedName && entry.value === normalizedValue)) { continue; } result.push({ name: normalizedName, value: normalizedValue }); } } return result; } function normalizeJdPropertyList(item: any) { const result = normalizePropertyList(item); const attrs = Array.isArray(item?.attrs) ? item.attrs : []; for (const attr of attrs) { const name = normalizeText(attr?.name || attr?.attr_name); const value = normalizeText(attr?.value || attr?.attr_value) || (Array.isArray(attr?.values) ? attr.values.map((entry: any) => normalizeText(entry?.vname || entry)).filter(Boolean).join(" / ") : ""); if (!name || !value) continue; if (result.some((entry) => entry.name === name && entry.value === value)) { continue; } result.push({ name, value }); } return result; } export function extractMaterialSummary(properties: ProductPlanningPropertyRow[]) { const directMaterial = properties.find((property) => property.name === "材质"); const woodMaterial = properties.find((property) => property.name === "木质材质"); if (directMaterial && woodMaterial) { return `${directMaterial.value}(${woodMaterial.value})`; } if (woodMaterial) return woodMaterial.value; if (directMaterial) return directMaterial.value; const partialMaterial = properties.find( (property) => property.name.includes("材质") || property.name.includes("面料") || property.name.toLowerCase().includes("material") ); return partialMaterial?.value || undefined; } export function extractDetailedMaterialSummary(properties: ProductPlanningPropertyRow[]) { const materialProperties = properties.filter((property) => [ "材质", "木质材质", "饰面材质", "实木占比", "面料", "成分", "工艺", "表面处理", "风格", "流行元素", ].some((keyword) => property.name.includes(keyword)) ); if (materialProperties.length === 0) { return undefined; } return materialProperties.map((property) => `${property.name}:${property.value}`).join(" | "); } export function normalizeTaobaoItem(item: any, itemId: string): ProductPlanningTaobaoItem { const itemImages = normalizeImageList(item); const detailImages = normalizeDetailImages(item); const skus = normalizeSkuList(item); const properties = normalizePropertyList(item); const material = extractMaterialSummary(properties); const detailedMaterial = extractDetailedMaterialSummary(properties); return { platform: "taobao", dataSource: "legacy", itemId, requestedItemId: itemId, title: normalizeText(item?.title) || "无标题", sourceUrl: ensureHttpsUrl(item?.source_url || ""), detailUrl: ensureHttpsUrl(item?.detail_url || ""), shopId: normalizeText(item?.seller_info?.shop_id) || normalizeText(item?.shop_id) || undefined, shopName: normalizeText(item?.seller_info?.shop_name) || normalizeText(item?.shop_name) || normalizeText(item?.nick) || undefined, location: normalizeText(item?.location) || undefined, categoryId: normalizeText(item?.cid || item?.category_id) || undefined, price: normalizePrice(item?.price), originalPrice: normalizePrice(item?.orginal_price || item?.original_price || item?.price), salesText: normalizeText(item?.sales) || undefined, totalSales: parseInteger(item?.sales || item?.volume || item?.total_sold, 0), recentSales: null, brand: normalizeText(item?.brand) || undefined, isTmall: Boolean(item?.tmall) || String(item?.detail_url || "").includes("tmall.com"), mainImage: ensureHttpsUrl(item?.pic_url || itemImages[0] || ""), itemImages, detailImages, descriptionHtml: normalizeText(item?.desc) ? String(item.desc) : undefined, videoUrl: ensureHttpsUrl(item?.video?.url || ""), videoThumb: ensureHttpsUrl(item?.video?.thumb || ""), skus, properties, material, detailedMaterial, skuCount: skus.length, }; } export function normalizeJdItem(item: any, itemId: string): ProductPlanningTaobaoItem { const itemImages = normalizeMixedImageList(item?.item_imgs); const detailImages = normalizeDetailImages(item); const skus = normalizeJdSkuList(item); const properties = normalizeJdPropertyList(item); const material = extractMaterialSummary(properties); const detailedMaterial = extractDetailedMaterialSummary(properties); const totalSales = parseInteger(item?.sales || item?.volume || item?.total_sold, 0); return { platform: "jd", dataSource: "jd", itemId, requestedItemId: itemId, title: normalizeText(item?.title) || "无标题", detailUrl: ensureHttpsUrl(item?.detail_url || `https://item.jd.com/${itemId}.html`), shopName: normalizeText(item?.shop_name) || normalizeText(item?.seller_info?.shop_name) || normalizeText(item?.nick) || undefined, shopId: normalizeText(item?.shop_id) || normalizeText(item?.seller_info?.shop_id) || undefined, location: normalizeText(item?.location) || undefined, categoryId: normalizeText(item?.cid || item?.category_id) || undefined, price: normalizePrice(item?.price), originalPrice: normalizePrice(item?.orginal_price || item?.original_price || item?.price), salesText: normalizeText(item?.sales || item?.volume) || undefined, totalSales, recentSales: totalSales, brand: normalizeText(item?.brand) || undefined, isTmall: false, mainImage: ensureHttpsUrl(item?.pic_url || itemImages[0] || ""), itemImages, detailImages, descriptionHtml: normalizeText(item?.desc) ? String(item.desc) : undefined, videoUrl: ensureHttpsUrl(item?.video?.url || ""), videoThumb: ensureHttpsUrl(item?.video?.thumb || ""), skus, properties, material, detailedMaterial, skuCount: skus.length, }; } export function extractMonthSale(raw: Record | null | undefined) { if (!raw || typeof raw !== "object") return null; const candidates = [ raw.biz30day, raw.sales, raw.month_sales, (raw.data as Record | undefined)?.sales, (raw.data as Record | undefined)?.month_sales, (raw.data as Record | undefined)?.recent_sales, raw.recent_sales, ]; for (const candidate of candidates) { const parsed = parseRangeValue(candidate); if (typeof parsed === "number") { return parsed; } } return null; }