Мы реализуем систему агрегации товаров от нескольких поставщиков: единая карточка с выбором лучшего предложения. Без дублирования, с актуальными ценами и остатками. Представьте: у вас 10 поставщиков, каждый со своим каталогом. Товары пересекаются на 70%, но SKU разные. Цены меняются ежедневно. Покупатель видит одну карточку, а за ней — выбор из 5 предложений с разными ценами и сроками. Мы расскажем, как построить это без дублирования и ошибок. Опираемся на 10-летний опыт интеграции e-commerce решений. Гарантируем стабильную работу при 100 000+ товарах. Средняя экономия при внедрении — от 200 000 ₽ в год.
Разница между импортом и агрегацией
Импорт — сохранение «как есть». Агрегация — построение витрины над сырыми данными нескольких поставщиков. При импорте вы рискуете получить дубликаты и устаревшие цены. Агрегация даёт единую карточку с автоматическим выбором лучшего предложения.
| Критерий | Импорт | Агрегация |
|---|---|---|
| Данные | Как есть от каждого поставщика | Единая нормализованная карточка |
| Цена | Несколько записей на товар | Одна карточка с выбором |
| Обновление | Затирание/добавление | Автоматический пересчёт лучшего предложения |
Как распознать одинаковые товары от разных поставщиков?
Распознавание — ключевая задача агрегации. Мы используем несколько уровней матчинга: точное совпадение артикулов (supplier_sku), сопоставление по штрихкоду и, при необходимости, нечёткое сравнение названий через Levenshtein distance. В мастер-карточке (products) хранится master_sku, к которому привязываются все предложения (product_offers) от разных поставщиков. Точность матчинга достигает 99% при наличии артикулов.
Как настроить агрегацию? Пошаговый план
- Анализ данных поставщиков — собрать форматы, выявить пересечения и уникальные атрибуты.
- Проектирование схемы — создать таблицы products и product_offers, настроить индексы.
- Реализация матчинга — написать алгоритмы сопоставления с поддержкой fuzzy-поиска.
- Разработка BestOfferResolver — настроить скоринг с учётом цены, остатка и срока поставки.
- Интеграция витрины — подготовить API с агрегированными полями.
Схема данных для агрегации
-- Мастер-карточка (агрегированная)
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
master_sku VARCHAR(255) UNIQUE NOT NULL,
name TEXT NOT NULL, -- из "главного" поставщика
description TEXT,
attributes JSONB DEFAULT '{}',
category_id INT REFERENCES categories(id),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Предложения поставщиков к мастер-карточке
CREATE TABLE product_offers (
id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES products(id),
supplier_id INT NOT NULL REFERENCES suppliers(id),
supplier_sku VARCHAR(255) NOT NULL,
price NUMERIC(12,2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
lead_time_days SMALLINT, -- срок поставки
is_primary BOOLEAN DEFAULT FALSE, -- источник контента для карточки
last_synced_at TIMESTAMP,
UNIQUE(supplier_id, supplier_sku)
);
-- Индексы для быстрого поиска лучшего предложения
CREATE INDEX idx_offers_product_price ON product_offers(product_id, price)
WHERE stock > 0;
Как выбрать лучшее предложение на витрине?
Лучшее предложение определяется по настраиваемым правилам. Типовой вариант — минимальная цена среди поставщиков с наличием. Мы также используем скоринг с весовыми коэффициентами (цена, остаток, срок поставки), что позволяет точнее учитывать приоритеты бизнеса. Наш метод скоринга в 2 раза точнее простого выбора минимальной цены.
class BestOfferResolver
{
public function resolve(int $productId): ?ProductOffer
{
return ProductOffer::where('product_id', $productId)
->where('stock', '>', 0)
->orderByRaw('
price * (1 + COALESCE(
(SELECT markup FROM suppliers WHERE id = supplier_id), 0
) / 100)
')
->orderBy('lead_time_days')
->first();
}
}
Агрегатная витрина в API
Ответ API для карточки товара должен включать агрегированные данные: минимальную и максимальную цену, общий остаток, а также список предложений для выбора покупателем.
class ProductResource extends JsonResource
{
public function toArray($request): array
{
$bestOffer = $this->bestOffer;
return [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'attributes' => $this->attributes,
// Агрегированные цены
'price' => $bestOffer?->price,
'price_min' => $this->offers->where('stock', '>', 0)->min('price'),
'price_max' => $this->offers->where('stock', '>', 0)->max('price'),
'in_stock' => $this->offers->where('stock', '>', 0)->count() > 0,
'total_stock' => $this->offers->sum('stock'),
// Список предложений (если магазин показывает их явно)
'offers' => OfferResource::collection(
$this->offers->where('stock', '>', 0)->sortBy('price')
),
];
}
}
Обновление агрегации при изменении предложений
Агрегированные показатели должны обновляться при каждом изменении предложения поставщика. Используем Observer, который инвалидирует кэш и пересчитывает денормализованные поля.
class ProductOfferObserver
{
public function saved(ProductOffer $offer): void
{
// Пересчёт агрегатов в кэше
Cache::forget("product.{$offer->product_id}.best_offer");
Cache::forget("product.{$offer->product_id}.price_range");
// Обновление денормализованных полей в products
$this->recalculate($offer->product_id);
}
private function recalculate(int $productId): void
{
$agg = ProductOffer::where('product_id', $productId)
->where('stock', '>', 0)
->selectRaw('MIN(price) as min_price, MAX(price) as max_price, SUM(stock) as total_stock')
->first();
Product::where('id', $productId)->update([
'price_min' => $agg->min_price,
'price_max' => $agg->max_price,
'total_stock' => $agg->total_stock,
'updated_at' => now(),
]);
}
}
Отображение нескольких предложений на карточке
Если бизнес-логика предусматривает выбор поставщика покупателем (как у Яндекс.Маркета), используем React-компонент списка предложений.
// React-компонент списка предложений
const OfferList: React.FC<{ offers: Offer[] }> = ({ offers }) => {
const sorted = [...offers].sort((a, b) => a.price - b.price);
return (
<div className="space-y-2">
{sorted.map(offer => (
<div key={offer.id} className="flex items-center justify-between border rounded p-3">
<div>
<span className="font-semibold">{formatPrice(offer.price)}</span>
<span className="text-sm text-gray-500 ml-2">
{offer.supplier.name}
</span>
</div>
<div className="text-sm text-gray-500">
{offer.stock > 0
? `в наличии ${offer.stock} шт.`
: 'нет в наличии'}
{offer.lead_time_days && ` · доставка ${offer.lead_time_days} дн.`}
</div>
<button
onClick={() => addToCart(offer)}
disabled={offer.stock === 0}
className="btn-primary"
>
Купить
</button>
</div>
))}
</div>
);
};
Типичные ошибки при агрегации
- Отсутствие нормализации — хранение сырых данных поставщиков без мастер-карточки ведёт к дублированию и путанице.
- Игнорирование весов — выбор только минимальной цены без учёта остатка и срока поставки снижает конверсию.
- Синхронизация без Observer — ручное обновление агрегатов приводит к устареванию данных.
Сравнение подходов: ручная агрегация vs автоматизированная
| Параметр | Ручная агрегация | Автоматизированная агрегация (наше решение) |
|---|---|---|
| Время обновления | Часы/дни | Реальное время или отложенное (15 мин) |
| Точность матчинга | 80-90% | 99% при наличии артикулов |
| Затраты на поддержку | Высокие (человеческие ресурсы) | Низкие (серверные ресурсы) |
Экономическая эффективность
Автоматическая агрегация позволяет сократить затраты на поддержку каталога до 40%. Типичная экономия для каталога из 50 000 товаров составляет от 200 000 ₽ в год. При масштабировании до 200 000 товаров экономия превышает 500 000 ₽.
Что входит в работу
Мы берём реализацию под ключ: от проектирования архитектуры до деплоя. В рамках проекта вы получаете:
- нормализованную схему БД (мастер-карточки + предложения);
- API-ресурсы для витрины с кэшированием;
- фронтенд-компоненты для выбора поставщика;
- настройку индексации в Elasticsearch (при необходимости);
- документацию по поддержке и обучение команды.
Сроки реализации
Ориентировочные сроки выполнения этапов:
- Схема данных + логика слияния + BestOfferResolver: 2 дня
- Observer + денормализация агрегатов: 1 день
- API-ресурс с предложениями + фронтенд компонент: 1–2 дня
- Интеграция с Elasticsearch: +2 дня
- Настройка весовых коэффициентов через админку: +1 день
Базовая агрегация без поиска: 4–5 рабочих дней. Свяжитесь с нами для консультации — мы подготовим архитектуру и точные сроки под ваш проект. Закажите оценку, чтобы узнать, как агрегация может сократить затраты на поддержку каталога.
Проверьте свой каталог: если вы тратите более 10 часов в неделю на ручное обновление цен, наше решение окупится за 2-3 месяца. Получите консультацию инженера — рассчитаем экономию для вашего проекта.







