Реализация синхронизации остатков с дропшиппинг-поставщиком
Несинхронизированные остатки — главная операционная проблема дропшиппинга. Покупатель оформляет и оплачивает заказ, а поставщик сообщает: товара нет. Возврат, недовольный клиент, репутационные потери. Синхронизация должна работать достаточно часто, чтобы этот сценарий не возникал.
Стратегии синхронизации
| Стратегия | Когда применять | Точность |
|---|---|---|
| Полный импорт по расписанию | FTP/CSV, нет API остатков | Низкая (задержка до 24ч) |
| Дельта-синхронизация | API с поддержкой updated_since |
Средняя (задержка 15–60 мин) |
| Realtime webhook | Поставщик поддерживает push | Высокая (секунды) |
| Realtime check при добавлении в корзину | Любой API | Высокая для критичных моментов |
Для большинства проектов используется комбинация: дельта-синхронизация каждые 30–60 минут + проверка в реальном времени при добавлении в корзину.
Модель данных
Schema::create('dropship_stock_log', function (Blueprint $table) {
$table->id();
$table->foreignId('dropship_product_id')->constrained();
$table->integer('prev_stock');
$table->integer('new_stock');
$table->string('source')->default('sync'); // sync | webhook | realtime_check
$table->timestamp('recorded_at');
$table->index(['dropship_product_id', 'recorded_at']);
});
Job полной синхронизации
class FullStockSyncJob implements ShouldQueue
{
public $timeout = 1800; // 30 минут для крупных каталогов
public function handle(): void
{
$suppliers = Supplier::where('is_active', true)->get();
foreach ($suppliers as $supplier) {
SyncSupplierStockJob::dispatch($supplier)->onQueue('stock-sync');
}
}
}
class SyncSupplierStockJob implements ShouldQueue
{
public $tries = 3;
public $backoff = [60, 300, 900];
public function handle(SupplierConnectorFactory $factory): void
{
$connector = $factory->make($this->supplier);
$page = 1;
$updated = 0;
do {
$products = $connector->getStockLevels($page, 500);
foreach ($products as $item) {
$dropshipProduct = DropshipProduct::where([
'supplier_id' => $this->supplier->id,
'supplier_sku' => $item->sku,
])->first();
if (!$dropshipProduct) continue;
if ($dropshipProduct->supplier_stock !== $item->stock) {
$prevStock = $dropshipProduct->supplier_stock;
$dropshipProduct->update([
'supplier_stock' => $item->stock,
'synced_at' => now(),
]);
// Обновляем остаток в каталоге магазина
if ($dropshipProduct->product) {
$this->updateProductAvailability($dropshipProduct->product, $item->stock);
}
// Логируем изменение
DropshipStockLog::create([
'dropship_product_id' => $dropshipProduct->id,
'prev_stock' => $prevStock,
'new_stock' => $item->stock,
'source' => 'sync',
'recorded_at' => now(),
]);
$updated++;
}
}
$page++;
} while (count($products) === 500);
Log::info('Stock sync completed', [
'supplier' => $this->supplier->slug,
'updated' => $updated,
]);
}
private function updateProductAvailability(Product $product, int $newStock): void
{
$wasAvailable = $product->stock > 0;
$isAvailable = $newStock > 0;
$product->update(['stock' => $newStock]);
// Уведомляем подписчиков «Сообщить о поступлении», если товар появился
if (!$wasAvailable && $isAvailable) {
event(new ProductBackInStockEvent($product));
}
}
}
Проверка в реальном времени при добавлении в корзину
Периодическая синхронизация снижает риск, но не исключает его полностью. Добавляем проверку при добавлении в корзину:
class AddToCartAction
{
public function execute(Product $product, int $quantity, Cart $cart): void
{
// Для дропшиппинг-товаров проверяем остаток у поставщика
if ($product->dropshipProduct && $this->shouldDoRealtimeCheck($product)) {
$connector = SupplierConnectorFactory::make($product->dropshipProduct->supplier);
$result = $connector->checkStock($product->dropshipProduct->supplier_sku);
// Обновляем кэш
$product->dropshipProduct->update([
'supplier_stock' => $result->stock,
'synced_at' => now(),
]);
if ($result->stock < $quantity) {
throw new InsufficientStockException(
available: $result->stock,
requested: $quantity,
);
}
}
$cart->addItem($product, $quantity);
}
private function shouldDoRealtimeCheck(Product $product): bool
{
// Проверяем в реальном времени, если последняя синхронизация была более 15 минут назад
$lastSync = $product->dropshipProduct->synced_at;
return !$lastSync || $lastSync->diffInMinutes(now()) > 15;
}
}
Дельта-синхронизация
Если API поставщика поддерживает параметр updated_since, полный импорт заменяется дельта-синхронизацией:
public function getDeltaStock(\DateTimeInterface $since): array
{
$response = $this->http->get($this->endpoint . '/stock', [
'query' => [
'updated_since' => $since->format('c'),
'fields' => 'sku,stock,updated_at',
],
'headers' => $this->authHeaders(),
]);
return json_decode($response->getBody(), true)['items'] ?? [];
}
Webhook от поставщика
Если поставщик поддерживает push-уведомления об изменении остатков:
// routes/api.php
Route::post('/webhooks/supplier/{supplier:slug}/stock', SupplierStockWebhookController::class)
->middleware('verify.supplier.signature');
class SupplierStockWebhookController
{
public function __invoke(Request $request, Supplier $supplier): JsonResponse
{
foreach ($request->input('items', []) as $item) {
UpdateDropshipStockJob::dispatch($supplier, $item['sku'], $item['stock']);
}
return response()->json(['ok' => true]);
}
}
Расписание
// Полная синхронизация — раз в сутки (ночью)
$schedule->job(FullStockSyncJob::class)->dailyAt('03:00')->withoutOverlapping();
// Дельта-синхронизация — каждые 30 минут
$schedule->job(DeltaStockSyncJob::class)->everyThirtyMinutes()->withoutOverlapping();
Сроки
Полная синхронизация по расписанию — 2 рабочих дня. Дельта-синхронизация + realtime-проверка при добавлении в корзину — 3–4 рабочих дня. Webhook-интеграция (если поддерживается поставщиком) — ещё 1 день.







