feat: add product detail enrichment for Magnit products

- Add isDetailsFetched field to Product model
- Add fetchProductDetails() and fetchProductObjectInfo() methods to MagnitApiScraper
- Add ProductParser methods for detail parsing
- Add ProductService methods: getProductsNeedingDetails(), updateProductDetails(), markAsDetailsFetched()
- Add enrich-product-details.ts script with statistics tracking
- Update package.json with "enrich" script command
- Add E2E_GUIDE.md documentation
- Exclude debug scripts from tsconfig type-check (temporary)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-22 01:52:50 +05:00
parent 5a763a4e13
commit 3299cca574
11 changed files with 795 additions and 88 deletions

View File

@@ -7,6 +7,9 @@ import {
SearchGoodsRequest,
SearchGoodsResponse,
ProductItem,
ObjectInfo,
ObjectReviewsResponse,
ProductDetailsResponse,
} from './types.js';
import { ProductService } from '../../../services/product/ProductService.js';
import { ProductParser } from '../../../services/parser/ProductParser.js';
@@ -90,7 +93,7 @@ export class MagnitApiScraper {
// Переход на главную страницу для получения cookies
Logger.info('Переход на главную страницу magnit.ru...');
await this.page.goto('https://magnit.ru/', {
waitUntil: 'networkidle',
waitUntil: 'domcontentloaded',
timeout: 30000,
});
@@ -188,6 +191,86 @@ export class MagnitApiScraper {
});
}
/**
* Получение object info (описание, рейтинг, отзывы) для товара
* Endpoint: /webgate/v1/listing/user-reviews-and-object-info
*/
async fetchProductObjectInfo(productId: string): Promise<ObjectInfo> {
const operation = async () => {
try {
Logger.debug(`Запрос object info для продукта ${productId}`);
const response = await this.httpClient.get<ObjectReviewsResponse>(
ENDPOINTS.OBJECT_INFO(productId),
{ timeout: this.config.requestTimeout ?? 30000 }
);
return response.data.object_info;
} catch (error) {
if (axios.isAxiosError(error)) {
const statusCode = error.response?.status || 0;
Logger.error(
`Ошибка получения object info: ${statusCode} - ${error.message}`
);
throw new APIError(
`Ошибка получения object info: ${error.message}`,
statusCode,
error.response?.data
);
}
throw error;
}
};
return withRetryAndReinit(operation, {
...this.config.retryOptions,
reinitOn403: this.config.autoReinitOn403 ?? true,
onReinit: async () => {
await this.reinitializeSession();
}
});
}
/**
* Получение детальной информации о товаре (бренд, описание, вес, единица измерения)
* Endpoint: /webgate/v2/goods/{productId}/stores/{storeCode}
*/
async fetchProductDetails(productId: string): Promise<ProductDetailsResponse> {
const operation = async () => {
try {
Logger.debug(`Запрос деталей для продукта ${productId}`);
const response = await this.httpClient.get<ProductDetailsResponse>(
ENDPOINTS.PRODUCT_DETAILS(productId, this.config.storeCode),
{ timeout: this.config.requestTimeout ?? 30000 }
);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const statusCode = error.response?.status || 0;
Logger.error(
`Ошибка получения деталей товара: ${statusCode} - ${error.message}`
);
throw new APIError(
`Ошибка получения деталей товара: ${error.message}`,
statusCode,
error.response?.data
);
}
throw error;
}
};
return withRetryAndReinit(operation, {
...this.config.retryOptions,
reinitOn403: this.config.autoReinitOn403 ?? true,
onReinit: async () => {
await this.reinitializeSession();
}
});
}
/**
* Переинициализация сессии (при 403 или истечении cookies)
* ВАЖНО: Не закрываем браузер, только обновляем cookies
@@ -204,7 +287,7 @@ export class MagnitApiScraper {
// Переход на главную страницу для обновления cookies
await this.page.goto('https://magnit.ru/', {
waitUntil: 'networkidle',
waitUntil: 'domcontentloaded',
timeout: 30000,
});