Просмотр исходного кода

fix: harden JD Parse product import

gangvy 1 месяц назад
Родитель
Сommit
3f15b20240
1 измененных файлов с 60 добавлено и 14 удалено
  1. 60 14
      tools/jd-import-products.mjs

+ 60 - 14
tools/jd-import-products.mjs

@@ -22,6 +22,8 @@ const config = {
   jdAppSecret: process.env.JD_APP_SECRET || '',
   limit: Math.min(20, Math.max(1, Number(valueArg('--limit', '10')) || 10)),
   timeoutMs: Number(process.env.JD_IMPORT_TIMEOUT_MS || 30000),
+  parseRetryAttempts: Math.min(10, Math.max(1, Number(process.env.JD_PARSE_RETRY_ATTEMPTS || 8))),
+  parseRetryDelayMs: Math.min(5000, Math.max(100, Number(process.env.JD_PARSE_RETRY_DELAY_MS || 350))),
 };
 
 const dryRun = args.has('--dry-run');
@@ -43,25 +45,49 @@ function parseHeaders(appId, masterKey) {
 }
 
 async function requestJson(url, init = {}) {
-  const controller = new AbortController();
-  const timer = setTimeout(() => controller.abort(), config.timeoutMs);
-  try {
-    const response = await fetch(url, { ...init, signal: controller.signal });
-    const text = await response.text();
-    let body;
+  const isParseEndpoint = (() => {
     try {
-      body = text ? JSON.parse(text) : null;
+      const parsed = new URL(url);
+      return parsed.hostname === 'server.fmode.cn' &&
+        (parsed.pathname.startsWith('/parse/') || parsed.pathname.startsWith('/backend/'));
     } catch {
-      body = { raw: text.slice(0, 500) };
+      return false;
     }
-    if (!response.ok) {
-      const detail = body?.error || body?.message || body?.errorList?.[0]?.message || response.statusText;
-      throw new Error(`HTTP ${response.status} ${url}: ${detail}`);
+  })();
+  const method = String(init.method || 'GET').toUpperCase();
+  const retryableMethod = ['GET', 'PUT', 'DELETE'].includes(method);
+
+  for (let attempt = 1; attempt <= (isParseEndpoint ? config.parseRetryAttempts : 1); attempt += 1) {
+    const controller = new AbortController();
+    const timer = setTimeout(() => controller.abort(), config.timeoutMs);
+    try {
+      const response = await fetch(url, { ...init, signal: controller.signal });
+      const text = await response.text();
+      let body;
+      try {
+        body = text ? JSON.parse(text) : null;
+      } catch {
+        body = { raw: text.slice(0, 500) };
+      }
+      const transientParseFailure = isParseEndpoint && (
+        (response.status === 403 && body?.error === 'unauthorized') ||
+        (retryableMethod && response.status >= 500)
+      );
+      if (transientParseFailure && attempt < config.parseRetryAttempts) {
+        await new Promise((resolve) => setTimeout(resolve, config.parseRetryDelayMs * attempt));
+        continue;
+      }
+      if (!response.ok) {
+        const detail = body?.error || body?.message || body?.errorList?.[0]?.message || response.statusText;
+        throw new Error(`HTTP ${response.status} ${url}: ${detail}`);
+      }
+      return body;
+    } finally {
+      clearTimeout(timer);
     }
-    return body;
-  } finally {
-    clearTimeout(timer);
   }
+
+  throw new Error(`请求重试耗尽: ${url}`);
 }
 
 function parseQueryUrl(baseUrl, className, where, extra = {}) {
@@ -313,8 +339,28 @@ function buildRecords(batch) {
   for (const item of batch.products) {
     const product = normalizeProduct(item.row, item.detail, batch.shopId, item.collectedAt);
     productDetails.push(product);
+    const stockTotal = item.stocks.reduce((total, stock) => {
+      const value = Number(stock.stockNum ?? 0);
+      return total + (Number.isFinite(value) ? value : 0);
+    }, 0);
     products.push({
       ...product,
+      platform: ['jd'],
+      externalId: product.productId,
+      productCode: product.productId,
+      productName: product.title,
+      description: product.title,
+      primaryCategory: product.category1 || product.category,
+      secondaryCategory: product.category2,
+      productSubcategory: product.category3,
+      tags: ['京东', '本店'],
+      status: stringValue(product.itemStatus || 'active'),
+      stock: stockTotal,
+      pricing: {
+        currency: 'CNY',
+        salePrice: product.price,
+      },
+      lastSyncAt: { __type: 'Date', iso: item.collectedAt },
       priceAmount: product.price,
       currency: 'CNY',
       productSource: 'jd-sp-api',