feat: 开发采集、采集箱和商品编辑功能

This commit is contained in:
Joey
2026-08-15 22:17:26 +08:00
parent c61d1a3154
commit 36357843d0
130 changed files with 18005 additions and 12 deletions
+107
View File
@@ -0,0 +1,107 @@
/**
* JSON-LD 提取器(schema.org/Product)
*
* Ozon 是 SSR 站点,商品页 HTML 里带 application/ld+json,
* 是 DOM 之外最稳定的结构化来源(比哈希类名稳定一个数量级)。
*
* 参考实现(毛子ERP)也解析 application/ld+json 取 description / offers.url。
*/
export interface JsonLdProduct {
title?: string;
description?: string;
brand?: string;
sku?: string;
price?: string;
currency?: string;
images: string[];
rating?: string;
reviewCount?: string;
}
function asString(v: unknown): string | undefined {
if (typeof v === 'string') return v;
if (typeof v === 'number') return String(v);
return undefined;
}
function findProduct(node: unknown): any | null {
if (Array.isArray(node)) {
for (const item of node) {
const r = findProduct(item);
if (r) return r;
}
return null;
}
if (!node || typeof node !== 'object') return null;
const obj = node as Record<string, unknown>;
const type = obj['@type'];
const types = Array.isArray(type) ? type : [type];
if (types.some((t) => t === 'Product')) return obj;
// @graph 包裹
if (Array.isArray(obj['@graph'])) {
for (const g of obj['@graph']) {
const r = findProduct(g);
if (r) return r;
}
}
return null;
}
function collectImages(node: unknown, out: string[]): void {
if (!node) return;
if (typeof node === 'string') {
if (/^(https?:)?\/\/.+/i.test(node) && !out.includes(node)) out.push(node);
return;
}
if (Array.isArray(node)) {
node.forEach((n) => collectImages(n, out));
return;
}
if (typeof node === 'object') {
for (const v of Object.values(node as Record<string, unknown>)) {
collectImages(v, out);
}
}
}
export function extractJsonLd(): JsonLdProduct | null {
try {
const scripts = document.querySelectorAll('script[type="application/ld+json"]');
for (const script of Array.from(scripts)) {
const text = script.textContent?.trim();
if (!text) continue;
let data: unknown;
try {
data = JSON.parse(text);
} catch {
continue;
}
const product = findProduct(data);
if (!product) continue;
const offers = Array.isArray(product.offers) ? product.offers[0] : product.offers;
const brandName = product.brand?.name ?? (typeof product.brand === 'string' ? product.brand : undefined);
const images: string[] = [];
if (product.image) collectImages(product.image, images);
return {
title: asString(product.name),
description: asString(product.description),
brand: asString(brandName),
sku: asString(product.sku),
price: asString(offers?.price),
currency: asString(offers?.priceCurrency),
images,
rating: asString(product.aggregateRating?.ratingValue),
reviewCount: asString(product.aggregateRating?.reviewCount),
};
}
} catch (err) {
console.warn('[JSON-LD] 提取失败:', err);
}
return null;
}