Синхронізація каталогу товарів між сайтом та маркетплейсами
Утримувати актуальний каталог на 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; // повторити через 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 робочих дні.







