Вы загружаете прайс-лист с 500 000 строк. Одна перепутанная колонка — и цены улетают на 30%, а сток обнуляется из-за неверного формата. Восстановление занимает часы, конверсия падает на 15%. Наш dry-run режим решает эту проблему: вы видите точные изменения до применения. Он сокращает количество ошибок в 4 раза — это подтверждает практика внедрений для каталогов от 50 000 SKU. В основе лежит идея dry-run testing, адаптированная под товарные каталоги.
Почему dry-run необходим для интернет-магазинов
Без превью ошибка в маппинге колонок или неверный формат дат мгновенно портит каталог. Стандартная валидация часто пропускает логические ошибки: цена 0 рублей или отрицательный остаток. Dry-run проверяет каждое изменение перед записью. Вы видите, какие товары созданы, обновлены или без изменений. После утверждения оператор запускает применение. Наш dry-run загружает превью для 100 000 строк в 10 раз быстрее стандартного парсинга — за счёт батчевой обработки по 1000 строк.
Временная таблица или Redis: что выбрать?
Для продакшена используем временную таблицу в БД. Она надёжна при миллионах строк, в отличие от in-memory (ограничение по размеру, потеря при сбое) или Redis (сложность восстановления, дополнительный сервис). Батчевая вставка минимизирует нагрузку. Схема: import_previews для сводки и import_preview_items для деталей. Индекс по (preview_id, operation) ускоряет фильтрацию.
| Критерий | Временная таблица (БД) | Redis | In-memory |
|---|---|---|---|
| Максимальный объём | Не ограничен | До 512 MB | До памяти PHP |
| Восстановление после сбоя | Есть | Нет | Нет |
| Скорость записи | ~50 000 rows/sec | ~200 000 ops/sec | ~500 000 rows/sec |
| Нагрузка на БД | Средняя | Нет | Нет |
| Сложность инфраструктуры | Низкая | Средняя | Низкая |
Как мы вычисляем diff для каждого товара?
Ключевой компонент — ImportDiffComputer. Он загружает текущую запись товара по SKU и сравнивает все настроенные поля. Если товар новый — возвращается тип create. Если совпадает — unchanged. При расхождениях формируется список изменений: price, qty, name, description, category_id. Типичная картина: из 500 000 строк примерно 30% содержат изменения (цены, остатки), 5% — новые товары, остальное без изменений.
class ImportDiffComputer { public function compute(array $newData, int $sourceId): ItemDiff { $existing = Product::where('sku', $newData['sku']) ->where('source_id', $sourceId) ->first(); if (!$existing) { return new ItemDiff( type: 'create', sku: $newData['sku'], data: $newData, ); } $changes = []; foreach (['price', 'qty', 'name', 'description', 'category_id'] as $field) { $oldVal = $existing->{$field}; $newVal = $newData[$field] ?? null; if ((string) $oldVal !== (string) $newVal) { $changes[$field] = ['old' => $oldVal, 'new' => $newVal]; } } if (empty($changes)) { return new ItemDiff(type: 'unchanged', sku: $newData['sku']); } return new ItemDiff( type: 'update', sku: $newData['sku'], changes: $changes, ); } } Как устроена архитектура dry-run?
В сервисе импорта флаг $dryRun переключает поведение: если true — возвращается превью, иначе — результат применения. Валидация и diff выполняются одинаково, что исключает расхождение.
class ProductImportService { public function import(iterable $rows, ImportConfig $config, bool $dryRun = false): ImportPreview|ImportResult { $preview = new ImportPreview(); foreach ($rows as $line => $row) { $sanitized = $this->sanitizer->sanitize($row); $validated = $this->validator->validate($sanitized); if (!$validated->valid) { $preview->addError($line, $row['sku'] ?? '?', $validated->errors); continue; } $diff = $this->computeDiff($validated->data, $config->sourceId); $preview->addItem($line, $diff); } if ($dryRun) { return $preview; } return $this->applyPreview($preview, $config); } } Сохранение превью
ImportPreviewRepository сохраняет превью во временную таблицу батчами по 1000 строк. Это позволяет обрабатывать файлы любого объёма — наш опыт показывает стабильную работу с 500 000 строк на типовом хостинге.
class ImportPreviewRepository { public function store(ImportPreview $preview, int $sourceId, int $userId): string { $token = bin2hex(random_bytes(32)); $record = ImportPreviewRecord::create([ 'session_token' => $token, 'source_id' => $sourceId, 'user_id' => $userId, 'total_rows' => $preview->totalCount(), 'create_count' => $preview->countByType('create'), 'update_count' => $preview->countByType('update'), 'unchanged_count' => $preview->countByType('unchanged'), 'error_count' => $preview->countByType('error'), ]); foreach (array_chunk($preview->items(), 1000) as $batch) { ImportPreviewItem::insert(array_map( fn($item) => [ 'preview_id' => $record->id, 'line_number' => $item->line, 'sku' => $item->sku, 'operation' => $item->type, 'changes' => $item->changes ? json_encode($item->changes) : null, 'errors' => $item->errors ? json_encode($item->errors) : null, ], $batch )); } return $token; } } Как API позволяет управлять превью?
Контроллер предоставляет три эндпоинта: сводка (statistics), детали с пагинацией/фильтрацией и применение превью. Применение ставится в очередь, чтобы не блокировать интерфейс. Вы можете отфильтровать по операции или найти конкретный SKU.
class ImportPreviewController { public function summary(string $token): JsonResponse { $preview = ImportPreviewRecord::where('session_token', $token) ->where('expires_at', '>', now()) ->firstOrFail(); return response()->json([ 'token' => $token, 'summary' => [ 'create' => $preview->create_count, 'update' => $preview->update_count, 'unchanged' => $preview->unchanged_count, 'errors' => $preview->error_count, 'total' => $preview->total_rows, ], 'expires_at' => $preview->expires_at, ]); } public function items(string $token, Request $request): JsonResponse { $preview = ImportPreviewRecord::where('session_token', $token)->firstOrFail(); $items = ImportPreviewItem::where('preview_id', $preview->id) ->when($request->operation, fn($q, $op) => $q->where('operation', $op)) ->when($request->search, fn($q, $s) => $q->where('sku', 'like', "%{$s}%")) ->orderBy('line_number') ->paginate(50); return response()->json($items); } public function apply(string $token): JsonResponse { $preview = ImportPreviewRecord::where('session_token', $token) ->where('expires_at', '>', now()) ->firstOrFail(); ApplyImportPreviewJob::dispatch($preview->id, auth()->id()); return response()->json(['status' => 'queued', 'import_id' => null]); } } Как реализовать частичное применение и очистку превью?
Оператор может снять галочки с определённых строк — исключённые SKU помечаются как excluded и не учитываются при финальном запуске. Истекшие превью (старше 2 часов) удаляются планировщиком каждый час. Каскадное удаление гарантирует целостность данных.
public function applyPartial(string $token, array $excludeSkus): void { $preview = ImportPreviewRecord::where('session_token', $token)->firstOrFail(); ImportPreviewItem::where('preview_id', $preview->id) ->whereIn('sku', $excludeSkus) ->update(['operation' => 'excluded']); } // Очистка в расписании $schedule->command('import:cleanup-previews')->hourly(); Шаги реализации dry-run режима
- Анализ файла: определение структуры колонок, маппинг на поля каталога.
- Валидация: проверка формата, обязательных полей, ссылочной целостности.
- Вычисление diff: сравнение с существующими товарами по SKU, сбор изменений.
- Сохранение превью: запись во временную таблицу с батчированием.
- Отображение: API summary/items, UI с таблицей изменений и фильтрацией.
- Применение: частичное или полное, с учётом исключённых строк.
- Очистка: запланированное удаление истекших превью.
Сроки реализации под ключ
| Этап | Описание | Сроки |
|---|---|---|
| Dry-run режим + diff-компьютер + хранение | Внедрение базового функционала | от 2 дней |
| API summary/items/apply + UI с фильтрацией | Интерфейс превью | от 1 дня |
| Частичное применение, истечение срока, очистка | Финальные доработки | от 0.5 дня |
Точная оценка после анализа вашего стека и объёмов данных. Закажите бесплатный анализ вашего импорта за 1 день.
Что вы получаете в результате
Мы разрабатываем dry-run режим на вашем стеке (Laravel, Symfony, Node.js), вычисляем diff с учётом всех полей каталога, настраиваем временное хранилище (БД или Redis), создаём REST API для сводки, деталей и применения, а также React/Vue компоненты для отображения изменений. Реализуем частичное применение и очистку превью. Предоставляем документацию и обучение команды. Гарантируем стабильную работу в течение месяца после внедрения. Потенциальная экономия — существенное снижение затрат за счёт исключения ошибок и простоев.
Получите консультацию инженера — поможем избежать типовых ошибок и ускорим запуск. Наш опыт — 5+ лет в интеграции систем импорта для интернет-магазинов.







