Синхронизация каталога товаров между сайтом и маркетплейсами
Поддерживать актуальный каталог на 3–5 маркетплейсах вручную — нереальная задача при большом ассортименте. Система синхронизации передаёт новые товары, обновляет изменения в описаниях и характеристиках, снимает с продажи удалённые позиции.
Схема данных маппинга
CREATE TABLE marketplace_product_mappings (
id BIGSERIAL PRIMARY KEY,
product_id BIGINT REFERENCES products(id),
marketplace TEXT, -- 'ozon', 'wb', 'ym'
external_id TEXT, -- ID на маркетплейсе
external_sku TEXT, -- SKU на маркетплейсе (может отличаться)
status TEXT, -- 'active', 'pending', 'error', 'removed'
last_synced_at TIMESTAMPTZ,
sync_hash CHAR(64), -- SHA-256 синхронизируемых полей
error_message TEXT,
UNIQUE (product_id, marketplace)
);
Детектирование изменений
class ProductChangeDetector
{
// Поля, изменение которых требует пересинхронизации
private array $trackFields = [
'name', 'description', 'brand', 'sku', 'price',
'category_id', 'attributes', 'images'
];
public function hasChanges(Product $product, string $marketplace): bool
{
$mapping = $product->marketplaceMappings()->where('marketplace', $marketplace)->first();
if (!$mapping) return true; // новый товар — нужна синхронизация
$currentHash = $this->computeHash($product);
return $currentHash !== $mapping->sync_hash;
}
private function computeHash(Product $product): string
{
$data = $product->only($this->trackFields);
$data['images'] = $product->images->pluck('url')->sort()->values()->all();
return hash('sha256', json_encode($data, JSON_SORT_KEYS));
}
}
Маппинг категорий
Каждый маркетплейс имеет собственное дерево категорий. Без маппинга невозможно разместить товар:
class CategoryMapper
{
// Хранится в БД, управляется через UI
public function getMarketplaceCategory(int $siteCategoryId, string $marketplace): ?int
{
return DB::table('category_mappings')
->where('site_category_id', $siteCategoryId)
->where('marketplace', $marketplace)
->value('marketplace_category_id');
}
// Для Ozon используем поиск по названию
public function suggestOzonCategory(string $categoryName): array
{
return Http::withHeaders($this->ozonHeaders)
->post('https://api-seller.ozon.ru/v1/description-category/search', [
'language' => 'DEFAULT',
'query' => $categoryName,
])
->json('result');
}
}
Обработчик синхронизации
class CatalogSyncJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue;
public int $tries = 3;
public int $backoff = 300; // retry через 5 минут
public function handle(): void
{
$products = Product::where('active', true)->get();
$detector = app(ProductChangeDetector::class);
foreach (['ozon', 'wb', 'ym'] as $marketplace) {
$toSync = $products->filter(fn($p) => $detector->hasChanges($p, $marketplace));
$toSync->chunk(50)->each(function ($chunk) use ($marketplace) {
$adapter = $this->getAdapter($marketplace);
foreach ($chunk as $product) {
try {
$adapter->upsertProduct($product);
$this->updateMapping($product, $marketplace, 'active');
} catch (Exception $e) {
$this->updateMapping($product, $marketplace, 'error', $e->getMessage());
Log::error("Catalog sync failed", compact('marketplace', 'e'));
}
}
});
}
}
}
Дашборд состояния синхронизации
-- Текущее состояние каталога по маркетплейсам
SELECT
marketplace,
COUNT(*) FILTER (WHERE status = 'active') AS active,
COUNT(*) FILTER (WHERE status = 'pending') AS pending,
COUNT(*) FILTER (WHERE status = 'error') AS errors,
MAX(last_synced_at) AS last_sync
FROM marketplace_product_mappings
GROUP BY marketplace;
Сроки
Система синхронизации каталога для 3 маркетплейсов с дашбордом: 18–24 рабочих дня.







