| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626 |
- import { Injectable } from '@angular/core';
- import { Observable, from, of, firstValueFrom } from 'rxjs';
- import { catchError, map } from 'rxjs/operators';
- import { DataStoreService } from '../../../app/core/services/data-store.service';
- import { PermissionService, StoreProductFilters } from './permission.service';
- export interface ProductDimension {
- asin: string;
- parentAsin?: string;
- sellerSku?: string;
- productSku?: string;
- designerId?: string;
- designerName?: string;
- developerId?: string;
- developerName?: string;
- operatorId?: string;
- operatorName?: string;
- userAccount?: string;
- site?: string;
- departCode?: string;
- accessUserId?: string;
- itemStatus?: number;
- fulfillmentType?: string;
- fnsku?: string;
- sellerId?: string;
- listingColor?: string;
- listingSize?: string;
- listingVariation?: string;
- imageUrl?: string;
- smallImageUrl?: string;
- listingImageUrl?: string;
- listingSmallImageUrl?: string;
- photo?: any;
- listingOpenDateSite?: string | Date;
- firstOrderDateSite?: string | Date;
- eccangPrice?: number;
- eccangSalePrice?: number;
- eccangListingPrice?: number;
- eccangRegularPrice?: number;
- ratings?: number;
- ratingsCount?: number;
- rank?: number;
- category?: string[];
- listingSalesVolumeOfMonth?: number;
- }
- export interface SiteDimensionOption {
- value: string;
- label: string;
- }
- export type ProductDimensionFields = Omit<ProductDimension, 'asin'>;
- interface AccessibleOptionSnapshot {
- designerName: string[];
- operatorName: string[];
- developerName: string[];
- fulfillmentType: string[];
- site: string[];
- }
- @Injectable({
- providedIn: 'root'
- })
- export class ProductDimensionService {
- private readonly batchSize = 80;
- private readonly ownProductClasses = new Set(['ProductDetail', 'AsinSkuMapping', 'Product']);
- private dimensionCache = new Map<string, ProductDimension>();
- private optionCache = new Map<string, string[]>();
- private siteOptionCache: SiteDimensionOption[] | null = null;
- private accessibleOptionCache: { ts: number; snapshot: AccessibleOptionSnapshot } | null = null;
- private accessibleOptionPromise: Promise<AccessibleOptionSnapshot> | null = null;
- private readonly accessibleOptionTtlMs = 60_000;
- constructor(private permissionService: PermissionService, private dataStore: DataStoreService) {}
- private static readonly SITE_LABELS: Record<string, string> = {
- us: '美国站',
- uk: '英国站',
- de: '德国站',
- fr: '法国站',
- es: '西班牙站',
- it: '意大利站',
- jp: '日本站',
- ca: '加拿大站',
- mx: '墨西哥站',
- br: '巴西站',
- in: '印度站',
- au: '澳大利亚站',
- ae: '阿联酋站',
- sa: '沙特站'
- };
- private static readonly SITE_ALIASES: Record<string, string[]> = {
- us: ['us', 'usa', '美国', '美国站', 'united states', 'amazon.com', 'atvpdkikx0der', '1'],
- uk: ['uk', 'gb', '英国', '英国站', 'united kingdom', 'amazon.co.uk', 'a1f83g8c2aro7p', '2'],
- de: ['de', '德国', '德国站', 'germany', 'amazon.de', 'a1pa6795ukmfr9', '3'],
- fr: ['fr', '法国', '法国站', 'france', 'amazon.fr', 'a13v1ib3viyzzh', '4'],
- in: ['in', 'india', '印度', '印度站', 'amazon.in', 'a21tjruun4kgv', '5'],
- ca: ['ca', 'canada', '加拿大', '加拿大站', 'amazon.ca', 'a2euq1wtgctbg2', '6'],
- jp: ['jp', '日本', '日本站', 'japan', 'amazon.co.jp', 'a1vc38t7yxb528', '7'],
- es: ['es', '西班牙', '西班牙站', 'spain', 'amazon.es', 'a1rkkupihcs9hs', '8'],
- it: ['it', '意大利', '意大利站', 'italy', 'amazon.it', 'apj6jra9ng5v4', '9'],
- mx: ['mx', 'mexico', '墨西哥', '墨西哥站', 'amazon.com.mx', 'a1am78c64um0y8', '10'],
- ae: ['ae', 'uae', '阿联酋', '阿联酋站', 'amazon.ae', 'a2vigq35rcs4ug', '11'],
- au: ['au', 'australia', '澳大利亚', '澳大利亚站', 'amazon.com.au', 'a39ibj37trp1c6', '12'],
- br: ['br', 'brazil', '巴西', '巴西站', 'amazon.com.br', 'a2q3y263d00kwc', '13'],
- sa: ['sa', 'ksa', '沙特', '沙特站', 'saudi arabia', 'amazon.sa', 'a17e79c6d8dwnp', '14']
- };
- getByAsins(asins: string[]): Observable<Map<string, ProductDimension>> {
- const normalized = this.normalizeAsins(asins);
- if (!normalized.length) return of(new Map<string, ProductDimension>());
- const missing = normalized.filter(asin => !this.dimensionCache.has(asin));
- if (!missing.length) {
- return of(this.pickCached(normalized));
- }
- return from(this.fetchDimensions(missing)).pipe(
- map(() => this.pickCached(normalized)),
- catchError(err => {
- console.warn('[ProductDimension] 获取产品维度失败:', err?.message || err);
- return of(this.pickCached(normalized));
- })
- );
- }
- enrichRows<T extends Record<string, any>>(rows: T[], asinGetter?: (row: T) => string): Observable<Array<T & ProductDimensionFields>> {
- if (!rows.length) return of([]);
- const getAsin = asinGetter || ((row: T) => row['asin'] || row['Asin'] || '');
- const asins = rows.map(row => getAsin(row)).filter(Boolean);
- return this.getByAsins(asins).pipe(
- map(dimensionMap => rows.map(row => {
- const asin = this.normalizeAsin(getAsin(row));
- const dimension = dimensionMap.get(asin);
- return dimension ? { ...row, ...this.toFields(dimension) } : row;
- }))
- );
- }
- getDesignerOptions(): Observable<string[]> {
- return this.getStringOptions('designerName');
- }
- getOperatorOptions(): Observable<string[]> {
- return this.getStringOptions('operatorName');
- }
- getDeveloperOptions(): Observable<string[]> {
- return this.getStringOptions('developerName');
- }
- getFulfillmentTypeOptions(): Observable<string[]> {
- return this.getStringOptions('fulfillmentType');
- }
- getSiteOptions(): Observable<SiteDimensionOption[]> {
- if (this.siteOptionCache) return of(this.siteOptionCache);
- return from(this.fetchSiteOptions()).pipe(
- map(options => {
- this.siteOptionCache = options;
- return options;
- }),
- catchError(err => {
- console.warn('[ProductDimension] 获取站点选项失败:', err?.message || err);
- return of([]);
- })
- );
- }
- clearCache(): void {
- this.dimensionCache.clear();
- this.optionCache.clear();
- this.siteOptionCache = null;
- this.accessibleOptionCache = null;
- this.accessibleOptionPromise = null;
- }
- private async fetchDimensions(asins: string[]): Promise<void> {
- try {
- const dimensions = await this.permissionService.getProductDimensions(asins);
- const keys = Object.keys(dimensions || {});
- if (keys.length) {
- keys.forEach(rawAsin => {
- const asin = this.normalizeAsin(rawAsin);
- const dimension = dimensions[rawAsin] as ProductDimension;
- if (asin && dimension) {
- this.dimensionCache.set(asin, { ...dimension, asin });
- }
- });
- return;
- }
- } catch (err: any) {
- console.warn('[ProductDimension] 获取产品维度失败:', err?.message || err);
- }
- await this.fetchDimensionsLegacy(asins);
- }
- private async fetchDimensionsLegacy(asins: string[]): Promise<void> {
- const detailMap = await this.fetchProductDetails(asins);
- const fallbackMap = await this.fetchAsinSkuMappings(asins);
- for (const asin of asins) {
- const dimension = this.mergeDimensions(detailMap.get(asin), fallbackMap.get(asin));
- if (dimension) this.dimensionCache.set(asin, dimension);
- }
- }
- private async fetchProductDetails(asins: string[]): Promise<Map<string, ProductDimension>> {
- const result = new Map<string, ProductDimension>();
- for (let i = 0; i < asins.length; i += this.batchSize) {
- const batch = asins.slice(i, i + this.batchSize);
- const rows = await firstValueFrom(this.dataStore.query('ProductDetail', { asin: batch }, { limit: batch.length }));
- const filteredRows = await this.permissionService.filterOwnProductRows(
- rows.map((row: any) => row.toJSON ? row.toJSON() : row)
- );
- filteredRows.forEach((data: any) => {
- const dimension = this.fromProductDetail(data);
- if (dimension.asin) result.set(dimension.asin, dimension);
- });
- }
- return result;
- }
- private async fetchAsinSkuMappings(asins: string[]): Promise<Map<string, ProductDimension>> {
- const grouped = new Map<string, any[]>();
- for (let i = 0; i < asins.length; i += this.batchSize) {
- const batch = asins.slice(i, i + this.batchSize);
- const rows = await firstValueFrom(this.dataStore.query('AsinSkuMapping', { asin: batch }, { limit: Math.min(300, Math.max(batch.length * 5, batch.length)) }));
- const filteredRows = await this.permissionService.filterOwnProductRows(
- rows.map((row: any) => row.toJSON ? row.toJSON() : row)
- );
- filteredRows.forEach((data: any) => {
- const asin = this.normalizeAsin(data.asin);
- if (!asin) return;
- const list = grouped.get(asin) || [];
- list.push(data);
- grouped.set(asin, list);
- });
- }
- const result = new Map<string, ProductDimension>();
- grouped.forEach((rows, asin) => {
- const best = this.pickBestMapping(rows);
- if (best) result.set(asin, this.fromAsinSkuMapping(best));
- });
- return result;
- }
- private getStringOptions(field: string): Observable<string[]> {
- const cached = this.optionCache.get(field);
- if (cached) return of(cached);
- return from(this.fetchStringOptions(field)).pipe(
- map(options => {
- this.optionCache.set(field, options);
- return options;
- }),
- catchError(err => {
- console.warn(`[ProductDimension] 获取 ${field} 选项失败:`, err?.message || err);
- return of([]);
- })
- );
- }
- private async fetchStringOptions(field: string): Promise<string[]> {
- let fromSnapshot: string[] = [];
- try {
- const snapshot = await this.getAccessibleOptionSnapshot();
- fromSnapshot = snapshot[field as keyof AccessibleOptionSnapshot] || [];
- } catch (err: any) {
- console.warn(`[ProductDimension] 可访问产品选项快照失败,回退表查询 ${field}:`, err?.message || err);
- }
- if (field !== 'designerName' && fromSnapshot.length) return fromSnapshot;
- const [fromDetail, fromMapping] = await Promise.all([
- this.fetchDistinctStringValues('ProductDetail', field).catch(err => {
- console.warn(`[ProductDimension] ProductDetail ${field} options failed:`, err?.message || err);
- return [] as string[];
- }),
- this.fetchDistinctStringValues('AsinSkuMapping', field).catch(err => {
- console.warn(`[ProductDimension] AsinSkuMapping ${field} options failed:`, err?.message || err);
- return [] as string[];
- })
- ]);
- return Array.from(new Set([...fromSnapshot, ...fromDetail, ...fromMapping]))
- .sort((a, b) => a.localeCompare(b, 'zh-Hans-CN'));
- }
- private async fetchSiteOptions(): Promise<SiteDimensionOption[]> {
- try {
- const snapshot = await this.getAccessibleOptionSnapshot();
- const fromSnapshot = this.buildSiteOptions(snapshot.site);
- if (fromSnapshot.length) return fromSnapshot;
- } catch (err: any) {
- console.warn('[ProductDimension] 可访问产品站点快照失败,回退表查询:', err?.message || err);
- }
- const primarySources = [
- {
- className: 'ProductDetail',
- fields: [
- 'site', 'Site',
- 'marketplace', 'Marketplace',
- 'marketplaceId', 'MarketplaceId',
- 'country', 'Country',
- 'countryCode', 'CountryCode'
- ]
- },
- {
- className: 'AmazonProduct',
- fields: [
- 'site', 'Site',
- 'marketplace', 'Marketplace',
- 'marketplaceId', 'MarketplaceId',
- 'domain', 'Domain',
- 'country', 'Country',
- 'countryCode', 'CountryCode'
- ]
- }
- ];
- const fallbackSource = {
- className: 'AsinSkuMapping',
- fields: [
- 'site', 'Site',
- 'marketplace', 'Marketplace',
- 'marketplaceId', 'MarketplaceId',
- 'country', 'Country',
- 'countryCode', 'CountryCode'
- ]
- };
- let values = await this.fetchSiteValuesFromSources(primarySources);
- if (!values.length) {
- values = await this.fetchSiteValuesFromSources([fallbackSource]);
- }
- return this.buildSiteOptions(values);
- }
- private async getAccessibleOptionSnapshot(): Promise<AccessibleOptionSnapshot> {
- if (this.accessibleOptionCache && Date.now() - this.accessibleOptionCache.ts < this.accessibleOptionTtlMs) {
- return this.accessibleOptionCache.snapshot;
- }
- if (this.accessibleOptionPromise) return this.accessibleOptionPromise;
- this.accessibleOptionPromise = this.permissionService
- .listAccessibleStoreProducts(this.emptyStoreProductFilters(), 1, 1000)
- .then(result => {
- const products = result.products || [];
- const optionValues = (options: any[] | undefined) => (options || [])
- .map(option => this.cleanText(option?.value ?? option?.label ?? option))
- .filter(Boolean);
- const productValues = (field: string) => products
- .map(product => this.cleanText((product as Record<string, any>)[field]))
- .filter(Boolean);
- const snapshot: AccessibleOptionSnapshot = {
- designerName: this.uniqueSorted([...optionValues(result.options?.designers), ...productValues('designerName')]),
- operatorName: this.uniqueSorted([...optionValues(result.options?.operators), ...productValues('operatorName')]),
- developerName: this.uniqueSorted(productValues('developerName')),
- fulfillmentType: this.uniqueSorted([...optionValues(result.options?.fulfillments), ...productValues('fulfillmentType')]),
- site: this.uniqueSorted([...optionValues(result.options?.sites), ...productValues('site')])
- };
- this.accessibleOptionCache = { ts: Date.now(), snapshot };
- return snapshot;
- })
- .finally(() => {
- this.accessibleOptionPromise = null;
- });
- return this.accessibleOptionPromise;
- }
- private buildSiteOptions(values: string[]): SiteDimensionOption[] {
- const optionMap = new Map<string, SiteDimensionOption>();
- values.forEach(raw => {
- const value = this.normalizeSiteCode(raw);
- if (!value) return;
- optionMap.set(value, {
- value,
- label: ProductDimensionService.SITE_LABELS[value] || String(raw || value).trim()
- });
- });
- const order = ['us', 'uk', 'de', 'fr', 'es', 'it', 'jp', 'ca', 'mx', 'br', 'in', 'au', 'ae', 'sa'];
- return Array.from(optionMap.values()).sort((a, b) => {
- const ai = order.indexOf(a.value);
- const bi = order.indexOf(b.value);
- if (ai !== -1 || bi !== -1) return (ai === -1 ? Number.MAX_SAFE_INTEGER : ai) - (bi === -1 ? Number.MAX_SAFE_INTEGER : bi);
- return a.label.localeCompare(b.label, 'zh-Hans-CN');
- });
- }
- private uniqueSorted(values: string[]): string[] {
- return Array.from(new Set(values.map(value => this.cleanText(value)).filter(Boolean)))
- .sort((a, b) => a.localeCompare(b, 'zh-Hans-CN'));
- }
- private emptyStoreProductFilters(): StoreProductFilters {
- return {
- asin: '',
- sellerSku: '',
- site: '',
- userAccount: '',
- itemName: '',
- itemStatus: '',
- fulfillmentType: '',
- designerName: '',
- operatorName: ''
- };
- }
- private async fetchSiteValuesFromSources(sources: Array<{ className: string; fields: string[] }>): Promise<string[]> {
- const tasks = sources.map(source => this.fetchSiteValuesFromSource(source).catch(err => {
- console.warn(`[ProductDimension] 获取 ${source.className} 站点字段失败:`, err?.message || err);
- return [];
- }));
- return (await Promise.all(tasks)).flat();
- }
- private async fetchSiteValuesFromSource(source: { className: string; fields: string[] }): Promise<string[]> {
- const rows = await firstValueFrom(this.dataStore.query(source.className, {}, { select: source.fields, limit: 5000 }));
- const readableRows = this.ownProductClasses.has(source.className)
- ? await this.permissionService.filterOwnProductRows(rows.map((row: any) => row.toJSON ? row.toJSON() : row))
- : rows.map((row: any) => row.toJSON ? row.toJSON() : row);
- const values = new Set<string>();
- readableRows.forEach((row: any) => {
- source.fields.forEach(field => {
- const raw = row[field];
- const value = String(raw || '').trim();
- if (value && value !== '-') values.add(value);
- });
- });
- return Array.from(values).sort((a, b) => a.localeCompare(b, 'zh-Hans-CN'));
- }
- private normalizeSiteCode(value: any): string {
- const normalized = String(value || '').trim().toLowerCase();
- if (!normalized || normalized === '-') return '';
- for (const [code, aliases] of Object.entries(ProductDimensionService.SITE_ALIASES)) {
- if (aliases.includes(normalized)) return code;
- }
- return normalized;
- }
- private async fetchDistinctStringValues(className: string, field: string): Promise<string[]> {
- const rows = await firstValueFrom(this.dataStore.query(className, { [field]: { $exists: true } }, { select: [field], limit: 5000 }));
- const readableRows = this.ownProductClasses.has(className)
- ? await this.permissionService.filterOwnProductRows(rows.map((row: any) => row.toJSON ? row.toJSON() : row))
- : rows.map((row: any) => row.toJSON ? row.toJSON() : row);
- const values = new Set<string>();
- readableRows.forEach((row: any) => {
- const value = this.cleanText(row[field]);
- if (value) values.add(value);
- });
- return Array.from(values).sort((a, b) => a.localeCompare(b, 'zh-Hans-CN'));
- }
- private fromProductDetail(data: any): ProductDimension {
- return {
- asin: this.normalizeAsin(data.asin),
- parentAsin: this.cleanText(data.parentAsin),
- sellerSku: this.cleanText(data.sellerSku),
- productSku: this.cleanText(data.productSku),
- designerId: this.cleanText(data.designerId),
- designerName: this.cleanText(data.designerName),
- developerId: this.cleanText(data.developerId),
- developerName: this.cleanText(data.developerName),
- operatorId: this.cleanText(data.operatorId),
- operatorName: this.cleanText(data.operatorName),
- userAccount: this.cleanText(data.userAccount),
- site: this.cleanText(data.site),
- departCode: this.cleanText(data.departCode),
- accessUserId: this.cleanText(data.accessUserId),
- itemStatus: Number(data.itemStatus) || 0,
- fulfillmentType: this.cleanText(data.fulfillmentType),
- fnsku: this.cleanText(data.fnsku),
- sellerId: this.cleanText(data.sellerId),
- listingColor: this.cleanText(data.listingColor),
- listingSize: this.cleanText(data.listingSize),
- listingVariation: this.cleanText(data.listingVariation),
- imageUrl: this.cleanText(data.imageUrl),
- smallImageUrl: this.cleanText(data.smallImageUrl),
- listingImageUrl: this.cleanText(data.listingImageUrl),
- listingSmallImageUrl: this.cleanText(data.listingSmallImageUrl),
- photo: data.photo || '',
- listingOpenDateSite: data.listingOpenDateSite || '',
- firstOrderDateSite: data.firstOrderDateSite || '',
- eccangPrice: Number(data.eccangPrice) || 0,
- eccangSalePrice: Number(data.eccangSalePrice) || 0,
- eccangListingPrice: Number(data.eccangListingPrice) || 0,
- eccangRegularPrice: Number(data.eccangRegularPrice) || 0,
- ratings: Number(data.ratings) || 0,
- ratingsCount: Number(data.ratingsCount) || 0,
- rank: Number(data.rank) || 0,
- category: Array.isArray(data.category) ? data.category : [],
- listingSalesVolumeOfMonth: Number(data.listingSalesVolumeOfMonth) || 0
- };
- }
- private fromAsinSkuMapping(data: any): ProductDimension {
- return {
- asin: this.normalizeAsin(data.asin),
- parentAsin: this.cleanText(data.parentAsin),
- sellerSku: this.cleanText(data.sellerSku),
- productSku: this.cleanText(data.productSku),
- designerId: this.cleanText(data.designerId),
- designerName: this.cleanText(data.designerName),
- developerId: this.cleanText(data.developerId),
- developerName: this.cleanText(data.developerName),
- operatorId: this.cleanText(data.operatorId),
- operatorName: this.cleanText(data.operatorName),
- userAccount: this.cleanText(data.userAccount),
- site: this.cleanText(data.site),
- departCode: this.cleanText(data.departCode),
- accessUserId: this.cleanText(data.accessUserId),
- itemStatus: Number(data.itemStatus) || 0,
- fulfillmentType: this.cleanText(data.fulfillmentType),
- fnsku: this.cleanText(data.fnsku),
- sellerId: this.cleanText(data.sellerId),
- listingColor: this.cleanText(data.color),
- listingSize: this.cleanText(data.size),
- listingVariation: this.cleanText(data.variation),
- imageUrl: this.cleanText(data.imageUrl),
- smallImageUrl: this.cleanText(data.smallImageUrl),
- listingImageUrl: this.cleanText(data.imageUrl),
- listingSmallImageUrl: this.cleanText(data.smallImageUrl),
- photo: data.photo || '',
- listingOpenDateSite: data.openDateSite || '',
- firstOrderDateSite: data.firstOrderDateSite || '',
- eccangPrice: Number(data.price) || 0,
- eccangSalePrice: Number(data.salePrice) || 0,
- eccangListingPrice: Number(data.listingPrice) || 0,
- eccangRegularPrice: Number(data.regularPrice) || 0
- };
- }
- private toFields(dimension: ProductDimension): ProductDimensionFields {
- const { asin, ...fields } = dimension;
- return Object.entries(fields).reduce((acc, [key, value]) => {
- if (['category', 'ratings', 'ratingsCount', 'rank', 'listingSalesVolumeOfMonth'].includes(key)) return acc;
- if (value === undefined || value === null) return acc;
- if (typeof value === 'string' && !this.isValidText(value)) return acc;
- if (Array.isArray(value) && !value.length) return acc;
- if (typeof value === 'number' && value === 0) return acc;
- (acc as Record<string, any>)[key] = value;
- return acc;
- }, {} as ProductDimensionFields);
- }
- private pickCached(asins: string[]): Map<string, ProductDimension> {
- const result = new Map<string, ProductDimension>();
- asins.forEach(asin => {
- const dimension = this.dimensionCache.get(asin);
- if (dimension) result.set(asin, dimension);
- });
- return result;
- }
- private pickBestMapping(rows: any[]): any | null {
- return rows.slice().sort((a, b) => {
- const activeA = Number(a.itemStatus) === 1 ? 0 : 1;
- const activeB = Number(b.itemStatus) === 1 ? 0 : 1;
- if (activeA !== activeB) return activeA - activeB;
- const completeA = this.completenessScore(a);
- const completeB = this.completenessScore(b);
- if (completeA !== completeB) return completeB - completeA;
- const timeA = new Date(a.syncedAt || a.updatedAt || 0).getTime();
- const timeB = new Date(b.syncedAt || b.updatedAt || 0).getTime();
- return timeB - timeA;
- })[0] || null;
- }
- private completenessScore(row: any): number {
- return ['sellerSku', 'productSku', 'designerName', 'developerName', 'operatorName', 'fulfillmentType', 'color', 'size']
- .reduce((score, key) => {
- return this.isValidText(row?.[key]) ? score + 1 : score;
- }, 0);
- }
- private mergeDimensions(detail?: ProductDimension, fallback?: ProductDimension): ProductDimension | undefined {
- if (!detail) return fallback;
- if (!fallback) return detail;
- return {
- ...detail,
- sellerSku: this.pickText(detail.sellerSku, fallback.sellerSku),
- productSku: this.pickText(detail.productSku, fallback.productSku),
- designerId: this.pickText(detail.designerId, fallback.designerId),
- designerName: this.pickText(detail.designerName, fallback.designerName),
- developerId: this.pickText(detail.developerId, fallback.developerId),
- developerName: this.pickText(detail.developerName, fallback.developerName),
- operatorId: this.pickText(detail.operatorId, fallback.operatorId),
- operatorName: this.pickText(detail.operatorName, fallback.operatorName),
- fulfillmentType: this.pickText(detail.fulfillmentType, fallback.fulfillmentType),
- listingColor: this.pickText(detail.listingColor, fallback.listingColor),
- listingSize: this.pickText(detail.listingSize, fallback.listingSize)
- };
- }
- private pickText(primary: any, fallback: any): string {
- const p = this.cleanText(primary);
- return p || this.cleanText(fallback);
- }
- private cleanText(value: any): string {
- const text = String(value || '').trim();
- return this.isValidText(text) ? text : '';
- }
- private isValidText(value: any): boolean {
- const text = String(value || '').trim();
- if (!text) return false;
- const normalized = text.toLowerCase();
- return !['-', '—', '–', '未分配', '未分配设计师', 'unknown', 'null', 'undefined', 'n/a', 'na'].includes(normalized);
- }
- private normalizeAsins(asins: string[]): string[] {
- return Array.from(new Set(asins.map(asin => this.normalizeAsin(asin)).filter(Boolean)));
- }
- private normalizeAsin(asin: string): string {
- return String(asin || '').trim().toUpperCase();
- }
- }
|