Реализация валидации данных при импорте товаров

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация валидации данных при импорте товаров
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация валидации данных при импорте товаров

Данные от поставщиков — это не контролируемый вход. В прайсе могут быть отрицательные цены, пустые артикулы, неверные единицы измерения, XSS в описаниях и строки вместо чисел. Без валидации это попадает прямо в каталог. Хорошая система валидации задерживает мусор на входе и предоставляет чёткий отчёт о проблемах.

Два уровня валидации

Структурная валидация — проверяет формат и типы данных:

  • Обязательные поля присутствуют
  • Числа являются числами
  • Даты в правильном формате
  • Длина строк в пределах допустимого

Бизнес-валидация — проверяет смысловые правила:

  • Цена не превышает разумный предел
  • SKU уникален в рамках источника
  • Категория существует в системе
  • Изменение цены не превышает допустимый процент

Построение валидатора на основе Laravel Validator

class ProductImportValidator
{
    private array $rules = [
        'sku'         => ['required', 'string', 'max:100'],
        'name'        => ['required', 'string', 'max:500'],
        'price'       => ['required', 'numeric', 'min:0.01', 'max:100000000'],
        'qty'         => ['nullable', 'integer', 'min:0', 'max:9999999'],
        'description' => ['nullable', 'string', 'max:100000'],
        'category'    => ['nullable', 'string', 'max:300'],
        'images'      => ['nullable', 'array', 'max:20'],
        'images.*'    => ['url', 'max:2000'],
        'weight'      => ['nullable', 'numeric', 'min:0', 'max:10000'],
    ];

    public function validate(array $row): ValidationResult
    {
        $validator = \Illuminate\Support\Facades\Validator::make(
            $row,
            $this->rules,
            $this->customMessages()
        );

        $errors = [];
        if ($validator->fails()) {
            $errors = $validator->errors()->toArray();
        }

        // Бизнес-правила
        $errors = array_merge($errors, $this->applyBusinessRules($row));

        return new ValidationResult(
            valid: empty($errors),
            errors: $errors,
            data: $validator->validated(),
        );
    }

    private function applyBusinessRules(array $row): array
    {
        $errors = [];

        // Проверка аномального изменения цены
        if (!empty($row['sku']) && !empty($row['price'])) {
            $existing = Product::where('sku', $row['sku'])->value('price');
            if ($existing && $existing > 0) {
                $change = abs($row['price'] - $existing) / $existing;
                if ($change > 0.5) {
                    $errors['price'][] = "Price change {$change}% exceeds 50% threshold";
                }
            }
        }

        // Проверка на XSS в описании
        if (!empty($row['description'])) {
            $clean = strip_tags($row['description']);
            if ($clean !== $row['description']) {
                $errors['description'][] = 'HTML tags detected in description';
            }
        }

        return $errors;
    }
}

Санитизация данных перед валидацией

Сначала очищаем очевидный мусор, потом валидируем:

class ProductDataSanitizer
{
    public function sanitize(array $raw): array
    {
        return [
            'sku'         => $this->cleanString($raw['sku'] ?? ''),
            'name'        => $this->cleanString($raw['name'] ?? ''),
            'price'       => $this->parseDecimal($raw['price'] ?? null),
            'qty'         => $this->parseInt($raw['qty'] ?? null),
            'description' => $this->sanitizeHtml($raw['description'] ?? ''),
            'weight'      => $this->parseDecimal($raw['weight'] ?? null),
            'images'      => $this->parseImageUrls($raw['images'] ?? []),
        ];
    }

    private function cleanString(?string $value): string
    {
        if ($value === null) return '';
        $value = trim($value);
        $value = preg_replace('/\p{C}/u', '', $value); // невидимые символы
        return mb_substr($value, 0, 1000);
    }

    private function parseDecimal(mixed $value): ?float
    {
        if ($value === null || $value === '') return null;
        $value = str_replace([' ', ',', "\xc2\xa0"], ['', '.', ''], (string) $value);
        return is_numeric($value) ? (float) $value : null;
    }

    private function parseInt(mixed $value): ?int
    {
        $decimal = $this->parseDecimal($value);
        return $decimal !== null ? (int) $decimal : null;
    }

    private function sanitizeHtml(?string $html): string
    {
        if (!$html) return '';
        // Разрешаем только безопасные теги
        return strip_tags($html, '<p><br><b><strong><i><em><ul><ol><li>');
    }

    private function parseImageUrls(mixed $raw): array
    {
        if (is_string($raw)) {
            $raw = array_map('trim', explode(',', $raw));
        }
        return array_values(array_filter(
            (array) $raw,
            fn($url) => filter_var($url, FILTER_VALIDATE_URL)
        ));
    }
}

Пакетная дедупликация SKU в рамках импорта

Один файл может содержать дублирующиеся артикулы (поставщик склеил несколько прайсов):

class ImportDeduplicator
{
    private array $seenSkus = [];

    public function check(string $sku, int $lineNumber): ?string
    {
        if (isset($this->seenSkus[$sku])) {
            return "Duplicate SKU '{$sku}' at line {$lineNumber}, first seen at line {$this->seenSkus[$sku]}";
        }
        $this->seenSkus[$sku] = $lineNumber;
        return null;
    }
}

Стратегии обработки ошибок

Не все ошибки одинаковы — нужно разделить их по критичности:

enum ValidationSeverity: string
{
    case CRITICAL = 'critical'; // строка пропускается
    case WARNING  = 'warning';  // строка импортируется с флагом
    case INFO     = 'info';     // только в лог
}

Правила с критичностью:

Правило Критичность
Пустой SKU CRITICAL
Отсутствует название CRITICAL
Цена = 0 или отрицательная CRITICAL
Цена изменилась >50% WARNING
HTML в описании WARNING
Категория не найдена INFO
Битый URL изображения INFO

Сбор и хранение ошибок

class ImportErrorCollector
{
    private array $errors = [];
    private int   $criticalCount = 0;

    public function add(int $line, string $sku, string $field, string $message, ValidationSeverity $severity): void
    {
        $this->errors[] = compact('line', 'sku', 'field', 'message', 'severity');
        if ($severity === ValidationSeverity::CRITICAL) {
            $this->criticalCount++;
        }
    }

    public function persist(int $importId): void
    {
        if (empty($this->errors)) return;

        // Пишем батчами по 1000 строк
        foreach (array_chunk($this->errors, 1000) as $chunk) {
            ImportError::insert(array_map(
                fn($e) => array_merge($e, ['import_id' => $importId, 'severity' => $e['severity']->value]),
                $chunk
            ));
        }

        ImportRun::find($importId)->update([
            'errors_count'          => count($this->errors),
            'critical_errors_count' => $this->criticalCount,
        ]);
    }
}

Порог отмены импорта

Если слишком много критических ошибок — останавливаем импорт целиком:

private const ABORT_THRESHOLD = 0.20; // 20% критических — стоп

if ($collector->getCriticalRate() > self::ABORT_THRESHOLD) {
    throw new ImportAbortedException(
        "Too many critical errors: {$collector->getCriticalRate()}%"
    );
}

Сроки реализации

  • Структурная валидация (Laravel Validator), санитизация, сбор ошибок — 1 день
  • Бизнес-правила, дедупликация SKU, severity-уровни — +1 день
  • Хранение ошибок в БД, порог отмены, отображение в admin UI — +1 день