|
|
@@ -1,6 +1,7 @@
|
|
|
import { ParseRestClient } from '../../../db/parse-rest.client.js';
|
|
|
import { VOC_PARSE_CLASSES } from '../../../db/parse-rest.schema.js';
|
|
|
import { adaptJdProductResponse, JD_PRODUCT_DETAIL_PATH } from '../adapters/jd-product.adapter.js';
|
|
|
+import { adaptJdReviewResponse, JD_PRODUCT_COMMENTS_PATH } from '../adapters/jd-review.adapter.js';
|
|
|
import type { GatewayRequestClient } from './jd-sync.service.js';
|
|
|
import { classifySyncFailure } from './jd-sync.service.js';
|
|
|
import { ParseRestVocRepository } from '../../saas-platform/parse-rest-voc.repository.js';
|
|
|
@@ -24,6 +25,12 @@ export interface CompetitorSyncResult {
|
|
|
skipped: number;
|
|
|
failed: number;
|
|
|
failedProductIds: string[];
|
|
|
+ reviewProductsRequested: number;
|
|
|
+ reviewProductsSucceeded: number;
|
|
|
+ reviewProductsSkipped: number;
|
|
|
+ reviewProductsFailed: number;
|
|
|
+ reviewsUpserted: number;
|
|
|
+ failedReviewProductIds: string[];
|
|
|
}
|
|
|
|
|
|
export class ParseRestCompetitorSyncService {
|
|
|
@@ -38,7 +45,17 @@ export class ParseRestCompetitorSyncService {
|
|
|
platform: 'jd';
|
|
|
concurrency?: number;
|
|
|
refresh?: boolean;
|
|
|
- onProgress?: (progress: { completed: number; total: number; productId: string; status: 'succeeded' | 'skipped' | 'failed' }) => void;
|
|
|
+ includeReviews?: boolean;
|
|
|
+ refreshReviews?: boolean;
|
|
|
+ reviewPages?: number;
|
|
|
+ onProgress?: (progress: {
|
|
|
+ completed: number;
|
|
|
+ total: number;
|
|
|
+ productId: string;
|
|
|
+ status: 'succeeded' | 'skipped' | 'failed';
|
|
|
+ reviewStatus: 'not_requested' | 'succeeded' | 'skipped' | 'failed';
|
|
|
+ reviewCount: number;
|
|
|
+ }) => void;
|
|
|
}): Promise<CompetitorSyncResult> {
|
|
|
const relations = await this.client.findAll<RelationObject>(VOC_PARSE_CLASSES.productRelation, {
|
|
|
workspaceId: input.workspaceId,
|
|
|
@@ -66,6 +83,12 @@ export class ParseRestCompetitorSyncService {
|
|
|
skipped: 0,
|
|
|
failed: 0,
|
|
|
failedProductIds: [],
|
|
|
+ reviewProductsRequested: input.includeReviews ? targets.length : 0,
|
|
|
+ reviewProductsSucceeded: 0,
|
|
|
+ reviewProductsSkipped: 0,
|
|
|
+ reviewProductsFailed: 0,
|
|
|
+ reviewsUpserted: 0,
|
|
|
+ failedReviewProductIds: [],
|
|
|
};
|
|
|
let cursor = 0;
|
|
|
let completed = 0;
|
|
|
@@ -76,6 +99,8 @@ export class ParseRestCompetitorSyncService {
|
|
|
if (!target) return;
|
|
|
const productId = target.competitorProductId;
|
|
|
let status: 'succeeded' | 'skipped' | 'failed';
|
|
|
+ let reviewStatus: 'not_requested' | 'succeeded' | 'skipped' | 'failed' = 'not_requested';
|
|
|
+ let reviewCount = 0;
|
|
|
try {
|
|
|
const existing = await this.client.findOne<StoredProduct>(VOC_PARSE_CLASSES.product, {
|
|
|
workspaceId: input.workspaceId,
|
|
|
@@ -100,8 +125,42 @@ export class ParseRestCompetitorSyncService {
|
|
|
result.failedProductIds.push(productId);
|
|
|
status = 'failed';
|
|
|
}
|
|
|
+ if (input.includeReviews) {
|
|
|
+ try {
|
|
|
+ const existingReviewCount = await this.client.count(VOC_PARSE_CLASSES.review, {
|
|
|
+ workspaceId: input.workspaceId,
|
|
|
+ platform: input.platform,
|
|
|
+ productId,
|
|
|
+ });
|
|
|
+ if (!input.refreshReviews && existingReviewCount > 0) {
|
|
|
+ result.reviewProductsSkipped += 1;
|
|
|
+ reviewStatus = 'skipped';
|
|
|
+ reviewCount = existingReviewCount;
|
|
|
+ } else {
|
|
|
+ const reviews = [];
|
|
|
+ const maxPages = Math.max(1, Math.min(5, Math.floor(input.reviewPages ?? 1)));
|
|
|
+ for (let page = 1; page <= maxPages; page += 1) {
|
|
|
+ const response = await this.gateway.request<unknown>(JD_PRODUCT_COMMENTS_PATH, {
|
|
|
+ params: { itemId: productId, page },
|
|
|
+ });
|
|
|
+ const adapted = adaptJdReviewResponse(response, productId, page);
|
|
|
+ reviews.push(...adapted.reviews);
|
|
|
+ if (!adapted.hasNextPage) break;
|
|
|
+ }
|
|
|
+ reviewCount = await this.repository.upsertReviews(input.workspaceId, input.platform, productId, reviews);
|
|
|
+ result.reviewProductsSucceeded += 1;
|
|
|
+ result.reviewsUpserted += reviewCount;
|
|
|
+ reviewStatus = 'succeeded';
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ classifySyncFailure(error);
|
|
|
+ result.reviewProductsFailed += 1;
|
|
|
+ result.failedReviewProductIds.push(productId);
|
|
|
+ reviewStatus = 'failed';
|
|
|
+ }
|
|
|
+ }
|
|
|
completed += 1;
|
|
|
- input.onProgress?.({ completed, total: targets.length, productId, status });
|
|
|
+ input.onProgress?.({ completed, total: targets.length, productId, status, reviewStatus, reviewCount });
|
|
|
}
|
|
|
};
|
|
|
const concurrency = Math.max(1, Math.min(8, Math.floor(input.concurrency ?? 3)));
|