Compare commits

...

3 Commits

Author SHA1 Message Date
94ed190869 fixes and shit 2025-07-18 20:50:08 +03:00
f96207c129 prices optimised 2025-07-18 18:12:06 +03:00
7fc55ab9c3 catalog prices fix 2025-07-17 21:22:20 +03:00
6 changed files with 110 additions and 4 deletions

View File

@ -56,6 +56,7 @@ ARG YOOKASSA_SECRET_KEY
ARG PARTSAPI_CATEGORIES_KEY ARG PARTSAPI_CATEGORIES_KEY
ARG PARTSAPI_ARTICLES_KEY ARG PARTSAPI_ARTICLES_KEY
ARG PARTSAPI_MEDIA_KEY ARG PARTSAPI_MEDIA_KEY
ARG PARTSAPI_URL
ARG PARTSINDEX_API_KEY ARG PARTSINDEX_API_KEY
ARG YANDEX_MAPS_API_KEY ARG YANDEX_MAPS_API_KEY
ARG YANDEX_DELIVERY_TOKEN ARG YANDEX_DELIVERY_TOKEN

View File

@ -29,6 +29,7 @@ services:
- PARTSAPI_CATEGORIES_KEY=${PARTSAPI_CATEGORIES_KEY} - PARTSAPI_CATEGORIES_KEY=${PARTSAPI_CATEGORIES_KEY}
- PARTSAPI_ARTICLES_KEY=${PARTSAPI_ARTICLES_KEY} - PARTSAPI_ARTICLES_KEY=${PARTSAPI_ARTICLES_KEY}
- PARTSAPI_MEDIA_KEY=${PARTSAPI_MEDIA_KEY} - PARTSAPI_MEDIA_KEY=${PARTSAPI_MEDIA_KEY}
- PARTSAPI_URL=${PARTSAPI_URL}
- PARTSINDEX_API_KEY=${PARTSINDEX_API_KEY} - PARTSINDEX_API_KEY=${PARTSINDEX_API_KEY}
- YANDEX_MAPS_API_KEY=${YANDEX_MAPS_API_KEY} - YANDEX_MAPS_API_KEY=${YANDEX_MAPS_API_KEY}
- YANDEX_DELIVERY_TOKEN=${YANDEX_DELIVERY_TOKEN} - YANDEX_DELIVERY_TOKEN=${YANDEX_DELIVERY_TOKEN}
@ -79,6 +80,7 @@ services:
- PARTSAPI_CATEGORIES_KEY=${PARTSAPI_CATEGORIES_KEY} - PARTSAPI_CATEGORIES_KEY=${PARTSAPI_CATEGORIES_KEY}
- PARTSAPI_ARTICLES_KEY=${PARTSAPI_ARTICLES_KEY} - PARTSAPI_ARTICLES_KEY=${PARTSAPI_ARTICLES_KEY}
- PARTSAPI_MEDIA_KEY=${PARTSAPI_MEDIA_KEY} - PARTSAPI_MEDIA_KEY=${PARTSAPI_MEDIA_KEY}
- PARTSAPI_URL=${PARTSAPI_URL}
# PartsIndex API # PartsIndex API
- PARTSINDEX_API_KEY=${PARTSINDEX_API_KEY} - PARTSINDEX_API_KEY=${PARTSINDEX_API_KEY}

View File

@ -3018,7 +3018,9 @@ export const resolvers = {
lang, lang,
limit, limit,
page, page,
q q,
params,
hasParams: !!params
}) })
// Преобразуем строку params в объект если передан // Преобразуем строку params в объект если передан
@ -3026,9 +3028,12 @@ export const resolvers = {
if (params) { if (params) {
try { try {
parsedParams = JSON.parse(params); parsedParams = JSON.parse(params);
console.log('📝 Разобранные параметры фильтрации:', parsedParams);
} catch (error) { } catch (error) {
console.warn('⚠️ Не удалось разобрать параметры фильтрации:', params); console.warn('⚠️ Не удалось разобрать параметры фильтрации:', params);
} }
} else {
console.log('📝 Параметры фильтрации отсутствуют');
} }
const entities = await partsIndexService.getCatalogEntities(catalogId, groupId, { const entities = await partsIndexService.getCatalogEntities(catalogId, groupId, {
@ -3064,8 +3069,102 @@ export const resolvers = {
} }
console.log('✅ Получены товары каталога:', entities.list.length) console.log('✅ Получены товары каталога:', entities.list.length)
console.log('🔍 Начинаем серверную фильтрацию по ценам...')
return entities // Глобальный кэш для результатов проверки цен (персистентный между запросами)
if (!global.priceCache) {
global.priceCache = new Map<string, { hasPrice: boolean, timestamp: number }>()
}
const priceCache = global.priceCache as Map<string, { hasPrice: boolean, timestamp: number }>
const CACHE_TTL = 5 * 60 * 1000 // 5 минут
const getCachedPriceResult = (code: string, brand: string): boolean | null => {
const key = `${code}_${brand}`
const cached = priceCache.get(key)
if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) {
return cached.hasPrice
}
return null
}
const cachePriceResult = (code: string, brand: string, hasPrice: boolean): void => {
const key = `${code}_${brand}`
priceCache.set(key, { hasPrice, timestamp: Date.now() })
}
// Фильтруем товары на сервере - проверяем наличие цен в AutoEuro
const filteredEntities: any[] = []
const batchSize = 20 // Увеличенный размер батча для скорости
for (let i = 0; i < entities.list.length; i += batchSize) {
const batch = entities.list.slice(i, i + batchSize)
// Проверяем цены для каждого товара в батче параллельно
const priceCheckPromises = batch.map(async (entity) => {
try {
// Сначала проверяем кэш
const cachedResult = getCachedPriceResult(entity.code, entity.brand.name);
if (cachedResult !== null) {
if (cachedResult) {
console.log(`💨 Кэш: товар ${entity.code} (${entity.brand.name}) имеет цену`);
return entity;
} else {
console.log(`💨 Кэш: товар ${entity.code} (${entity.brand.name}) не имеет цены`);
return null;
}
}
const searchResult = await autoEuroService.searchItems({
code: entity.code,
brand: entity.brand.name,
with_crosses: false,
with_offers: true
})
// Проверяем есть ли предложения с валидной ценой
const hasValidPrice: boolean = Boolean(searchResult.success &&
searchResult.data &&
searchResult.data.length > 0 &&
searchResult.data.some(offer =>
offer.price &&
parseFloat(offer.price.toString()) > 0
))
// Кэшируем результат
cachePriceResult(entity.code, entity.brand.name, hasValidPrice);
if (hasValidPrice) {
console.log(`✅ Товар ${entity.code} (${entity.brand.name}) имеет цену`);
return entity;
} else {
console.log(`❌ Товар ${entity.code} (${entity.brand.name}) не имеет цены`);
return null;
}
} catch (error) {
console.error(`❌ Ошибка проверки цены для ${entity.code}:`, error);
return null // Исключаем товары с ошибками
}
})
// Ждем результаты для текущего батча
const batchResults = await Promise.all(priceCheckPromises)
// Добавляем только товары с ценами
filteredEntities.push(...batchResults.filter(entity => entity !== null))
// Убираем задержку между батчами для максимальной скорости
// if (i + batchSize < entities.list.length) {
// await new Promise(resolve => setTimeout(resolve, 50))
// }
}
console.log(`✅ Серверная фильтрация завершена. Товаров с ценами: ${filteredEntities.length} из ${entities.list.length}`)
// Возвращаем отфильтрованный результат
return {
...entities,
list: filteredEntities
}
} catch (error) { } catch (error) {
console.error('❌ Ошибка в GraphQL resolver partsIndexCatalogEntities:', error) console.error('❌ Ошибка в GraphQL resolver partsIndexCatalogEntities:', error)
throw new Error('Не удалось получить товары каталога') throw new Error('Не удалось получить товары каталога')

View File

@ -37,7 +37,7 @@ class PartsAPIService {
private mediaApiKey: string; private mediaApiKey: string;
constructor() { constructor() {
this.baseURL = 'https://api.partsapi.ru'; this.baseURL = process.env.PARTSAPI_URL || 'https://api.partsapi.ru';
// Получаем ключи API из переменных окружения // Получаем ключи API из переменных окружения
this.categoriesApiKey = process.env.PARTSAPI_CATEGORIES_KEY || ''; this.categoriesApiKey = process.env.PARTSAPI_CATEGORIES_KEY || '';

View File

@ -155,7 +155,7 @@ interface CacheEntry<T> {
} }
class PartsIndexService { class PartsIndexService {
private baseURL = 'https://api.parts-index.com/v1'; private baseURL = process.env.PARTSAPI_URL+"/v1" || 'https://api.parts-index.com/v1';
private apiKey = 'PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE'; private apiKey = 'PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE';
// Простой in-memory кэш // Простой in-memory кэш

View File

@ -69,6 +69,10 @@ PARTSAPI_MEDIA_KEY=230d8c7118a36cc6d36d72681b76982b
# API ключ для PartsIndex (каталог автотоваров) # API ключ для PartsIndex (каталог автотоваров)
PARTSINDEX_API_KEY=PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE PARTSINDEX_API_KEY=PI-E1C0ADB7-E4A8-4960-94A0-4D9C0A074DAE
# ===== PARTSAPI =====
# URL для API Parts-Index
PARTSAPI_URL=https://api.parts-index.com
# ===== S3 ДОПОЛНИТЕЛЬНЫЕ НАСТРОЙКИ ===== # ===== S3 ДОПОЛНИТЕЛЬНЫЕ НАСТРОЙКИ =====
# Альтернативное название бакета (если отличается от AWS_BUCKET_NAME) # Альтернативное название бакета (если отличается от AWS_BUCKET_NAME)
AWS_S3_BUCKET=your_s3_bucket_name AWS_S3_BUCKET=your_s3_bucket_name