| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167 |
- import fs from 'node:fs';
- const configPath = process.env.VOC_CONFIG || '/etc/jianhen-voc/config.json';
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
- const parseUrl = process.env.PARSE_URL || 'http://127.0.0.1:3000/parse';
- const appId = config.parse.appId;
- const masterKey = config.parse.masterKey;
- const relayBase = String(config.relay.baseUrl || '').replace(/\/$/, '');
- const relayAuth = config.relay.auth;
- const parseHeaders = {
- 'Content-Type': 'application/json',
- 'X-Parse-Application-Id': appId,
- 'X-Parse-Master-Key': masterKey,
- };
- async function parseRequest(path, method = 'GET', body) {
- const response = await fetch(`${parseUrl}${path}`, {
- method,
- headers: parseHeaders,
- body: body === undefined ? undefined : JSON.stringify(body),
- });
- const result = await response.json();
- if (!response.ok || result.error) throw new Error(`${method} ${path}: ${JSON.stringify(result.error || response.status)}`);
- return result;
- }
- async function relay(path, domain, body) {
- const response = await fetch(`${relayBase}${config.relay.sorftimePath}`, {
- method: 'POST',
- headers: {
- Authorization: relayAuth,
- 'Content-Type': 'application/json',
- 'X-Request-Id': `enrich_${Date.now()}_${Math.random().toString(16).slice(2)}`,
- },
- body: JSON.stringify({ path, method: 'POST', query: { domain }, body }),
- });
- const result = await response.json();
- if (!response.ok || (result.code !== undefined && ![0, 200].includes(Number(result.code)))) {
- throw new Error(result.message || `Relay HTTP ${response.status}`);
- }
- let data = result.data ?? result.Data ?? result;
- if (data && typeof data === 'object' && !Array.isArray(data)) data = data.data ?? data.Data ?? data;
- return data;
- }
- async function upsert(className, where, payload) {
- const encoded = encodeURIComponent(JSON.stringify(where));
- const existing = await parseRequest(`/classes/${className}?where=${encoded}&limit=1`);
- const objectId = existing.results?.[0]?.objectId;
- return objectId
- ? parseRequest(`/classes/${className}/${objectId}`, 'PUT', payload)
- : parseRequest(`/classes/${className}`, 'POST', payload);
- }
- const siteByMarketplace = {
- ATVPDKIKX0DER: 'us', A2EUQ1WTGCTBG2: 'ca', A1AM78C64UM0Y8: 'mx', A2Q3Y263D00KWC: 'br',
- A1F83G8C2ARO7P: 'uk', A1PA6795UKMFR9: 'de', A13V1IB3VIYZZH: 'fr', A1RKKUPIHCS9HS: 'es',
- APJ6JRA9NG5V4: 'it', A1VC38T7YXB528: 'jp', A39IBJ37TRP1C6: 'au', A2VIGQ35RCS4UG: 'ae',
- A17E79C6D8DWNP: 'sa', A21TJRUUN4KGV: 'in',
- };
- const imageOf = value => Array.isArray(value) ? String(value[0] || '') : String(value || '');
- const categoryOf = value => {
- if (typeof value === 'string') return value;
- if (Array.isArray(value)) return value.map(item => categoryOf(item)).filter(Boolean).join(' > ');
- if (value && typeof value === 'object') return String(value.Name || value.name || value.CategoryName || '');
- return '';
- };
- const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
- const shopsResult = await parseRequest('/classes/Shop?limit=1000');
- const shops = new Map(shopsResult.results.map(shop => [shop.objectId, shop]));
- const listingsResult = await parseRequest('/classes/Listing?limit=1000');
- let enriched = 0;
- let reviewsSaved = 0;
- let failed = 0;
- for (const listing of listingsResult.results) {
- const asin = String(listing.asin || '').trim().toUpperCase();
- const shopId = listing.shop?.objectId || '';
- if (!asin || !shopId) continue;
- const shop = shops.get(shopId) || {};
- const domain = Number(shop.domain || 1);
- const marketplaceId = listing.marketplaceId || shop.marketplaceId || '';
- const pointer = { __type: 'Pointer', className: 'Shop', objectId: shopId };
- try {
- const raw = await relay('/api/ProductRequest', domain, {
- ASIN: asin,
- Trend: 1,
- QueryTrendStartDt: '',
- QueryTrendEndDt: '',
- });
- if (raw && (raw.Asin || raw.ASIN || raw.Title)) {
- const payload = {
- asin,
- parentAsin: raw.ParentAsin || listing.parentAsin || '',
- sku: listing.sku || '',
- sellerSku: listing.sku || '',
- title: raw.Title || listing.title || '',
- itemName: raw.Title || listing.title || '',
- imageUrl: imageOf(raw.Photo) || listing.mainImage || '',
- photo: imageOf(raw.Photo) || listing.mainImage || '',
- category: [categoryOf(raw.Category) || listing.productType || ''].filter(Boolean),
- productType: listing.productType || '',
- brand: raw.Brand || '',
- sellerId: raw.BuyboxSellerId || '',
- ratings: Number(raw.Ratings || 0),
- rating: Number(raw.Ratings || 0),
- ratingsCount: Number(raw.RatingsCount || 0),
- price: Number(raw.Price || listing.price || 0),
- salesPrice: Number(raw.SalesPrice || raw.Price || listing.price || 0),
- rank: Number(raw.Rank || 0),
- bsrCategory: Array.isArray(raw.BsrCategory) ? raw.BsrCategory : [],
- listingSalesVolumeOfMonth: Number(raw.ListingSalesVolumeOfMonth || 0),
- ListingSalesVolumeOfMonth: Number(raw.ListingSalesVolumeOfMonth || 0),
- listingSalesOfMonth: Number(raw.ListingSalesOfMonth || 0),
- marketplaceId,
- domain: String(domain),
- site: siteByMarketplace[marketplaceId] || String(domain),
- storeName: shop.name || '',
- shopName: shop.name || '',
- shopId,
- source: 'sorftime-product-request',
- onlineDate: String(raw.OnlineDate || listing.createdDate?.iso || listing.createdDate || '').slice(0, 10),
- shop: pointer,
- rawData: raw,
- };
- const where = { asin, shop: pointer };
- await upsert('Product', where, payload);
- await upsert('ProductDetail', where, payload);
- enriched += 1;
- }
- const reviewData = await relay('/api/ProductReviewsQuery', domain, {
- ASIN: asin,
- PageIndex: 1,
- OnlyPurchase: 0,
- Star: '1,2,3,4,5',
- });
- const reviews = reviewData?.Reviews || reviewData?.Items || reviewData?.List || (Array.isArray(reviewData) ? reviewData : []);
- for (const review of reviews) {
- const reviewId = String(review.ReviewId || review.Id || review.ID || review.id || `${asin}:${review.ReviewsDate || review.ReviewDate || ''}:${review.ProfileName || review.Author || ''}`);
- await upsert('SorftimeReviews', { reviewId }, {
- reviewId,
- asin,
- star: Number(review.Star || review.Rating || 0),
- rating: Number(review.Star || review.Rating || 0),
- title: review.Title || review.ReviewTitle || '',
- content: review.Content || review.ReviewContent || review.Body || '',
- reviewDate: review.ReviewsDate || review.ReviewDate || review.Date || '',
- author: review.ProfileName || review.Author || '',
- verified: Boolean(review.OnlyPurchase || review.VerifiedPurchase),
- shop: pointer,
- rawData: review,
- });
- reviewsSaved += 1;
- }
- } catch (error) {
- failed += 1;
- console.warn(`Failed ${asin}: ${error.message}`);
- }
- await delay(150);
- }
- console.log(JSON.stringify({ listings: listingsResult.results.length, enriched, reviewsSaved, failed }));
|