projects/org/@biz-lideravto/it/docs/import/TZ_IMPORT.md

ТЗ: Система импорта каталога

← Назад в оглавление

Версия: 1.0
Дата: 2026-02-27
Проект: new.lideravto.ru — Drupal 11.3.3 + Commerce 3.x
Модуль: dru_lider_importer
Аудитория: разработчики Drupal


1. Назначение и область применения

1.1 Цель

Система импорта предназначена для загрузки, обновления и управления каталогом запчастей для грузовых автомобилей на платформе new.lideravto.ru. Система обеспечивает:

1.2 Что система импортирует

1.3 Что система НЕ импортирует


2. Входные данные

2.1 Формат файла

2.2 Структура полей

Поле Тип Обязательное Описание
1 артикул string Нет Внутренний артикул поставщика Bazon. Не является OEM. Может отсутствовать.
2 наименование string Да Наименование детали на русском языке. Максимум 50 символов (ограничение по SEO-заголовку: title = наименование + OEM + марка + модель ≤ 70 символов).
3 марка string Да Марка грузового автомобиля (Scania, Volvo, Mercedes, MAN, DAF, Renault, Iveco, Liebherr).
4 модель string Да Модель грузового автомобиля в пределах марки (R5, FH, Actros, TGX и т.д.).
5 oem string Да Каноничный OEM-номер — чистый, без префиксов/суффиксов. Mercedes без A-prefix, Renault/Volvo без 742-prefix. Если OEM начинается с 0 — ноль сохранять.
6 кросс_номера string Нет Варианты написания и аналоги подходящие для всех моделей данного OEM: с A-буквой, с пробелами, с дефисами, OEM других производителей. Разделитель: запятая. Если аналог подходит только части моделей — заводить отдельной строкой (см. раздел 3.4).
7 производитель string Нет Производитель детали (Bosch, ZF, Knorr-Bremse, SKF и т.д.). Не марка грузовика.
8 состояние string Нет Состояние товара: "новый", "оригинал", "аналог".
9 цена decimal Да Цена в рублях. Формат: число с точкой или запятой как десятичным разделителем.
10 группа string Нет Группа деталей первого уровня (Двигатель, Трансмиссия, Тормозная система и т.д.).
11 система string Нет Система в пределах группы (Топливная система, Система охлаждения и т.д.).
12 узел string Нет Конкретный узел или агрегат (Форсунка, Насос ГУР, Балансир и т.д.).

2.3 Пример строки

LDR-7821;Форсунка DC13 XPI;Scania;R5;9604621423;A9604621423,F00RJ01285,0445120229,96046-21423;Bosch;новый;18500;Двигатель;Топливная система;Форсунка;Подходит для: Scania R5, Scania G5, Scania P5, Scania F5

Разбор:
- артикул: LDR-7821
- наименование: Форсунка DC13 XPI ← до 50 символов
- марка: Scania
- модель: R5 ← основная модель (для карточки)
- OEM: 9604621423 ← каноничный, чистый (без A-prefix, без пробелов)
- кросс_номера: A9604621423,F00RJ01285,0445120229,96046-21423 ← ВСЕ варианты как ищут люди
- производитель: Bosch
- состояние: новый
- цена: 18500
- группа: Двигатель
- система: Топливная система
- узел: Форсунка
- комментарий: Подходит для: Scania R5, Scania G5, Scania P5, Scania F5 ← совместимость

2.4 Требования к качеству входных данных

Критические ошибки (прекращают импорт строки):
- Пустое поле наименование
- Пустое поле марка
- Пустое поле модель
- Пустое поле oem И пустое поле артикул одновременно
- Некорректная цена (не число, отрицательное, ноль)

Предупреждения (строка импортируется с флагом):
- Наименование состоит только из стоп-слова (см. раздел 3.2)
- OEM содержит известные ошибки (нормализуется автоматически, пишется предупреждение)
- Поле кросс_номера содержит нечитаемые символы

Допустимые ситуации (нет предупреждения):
- Пустые поля: артикул, кросс_номера, производитель, состояние, группа, система, узел
- Различные форматы цены: 18500, 18500.00, 18 500, 18500,00


3. Обработка данных

3.1 Нормализация OEM

Нормализация применяется к полю oem каждой строки перед любой другой обработкой.

Алгоритм normalize_oem() — полная реализация на Python:

import re

def normalize_oem(oem: str) -> str:
    """
    Нормализует OEM-номер детали:
    - Заменяет кириллические буквы на латинские (визуально идентичные)
    - Удаляет разделители (пробелы, дефисы, точки, запятые)
    - Приводит к верхнему регистру
    - Снимает A-prefix у Mercedes (A + ровно 10 цифр)
    - Снимает PE-suffix у DAF
    - Снимает 742-prefix у Renault/Volvo

    Returns: нормализованный OEM или пустую строку если входные данные некорректны
    """
    if not oem or not isinstance(oem, str):
        return ''

    oem = oem.strip()

    # Шаг 1: Замена кириллических букв на латинские (визуально идентичные)
    cyrillic_to_latin = {
        'А': 'A',  # кириллическая А → латинская A
        'В': 'B',  # кириллическая В → латинская B
        'Е': 'E',  # кириллическая Е → латинская E
        'О': 'O',  # кириллическая О → латинская O
        'Р': 'R',  # кириллическая Р → латинская R
        'С': 'C',  # кириллическая С → латинская C
        'Х': 'X',  # кириллическая Х → латинская X
        'а': 'a',  # строчные тоже
        'в': 'b',
        'е': 'e',
        'о': 'o',
        'р': 'r',
        'с': 'c',
        'х': 'x',
    }
    for cyr, lat in cyrillic_to_latin.items():
        oem = oem.replace(cyr, lat)

    # Шаг 2: Удаление разделителей (пробел, дефис, точка, запятая, слэш)
    oem = re.sub(r'[\s\-\.\,\/]', '', oem)

    # Шаг 3: Приведение к верхнему регистру
    oem = oem.upper()

    # Шаг 4: Снятие A-prefix у Mercedes
    # Паттерн: буква A + ровно 10 цифр (и больше ничего)
    if re.match(r'^A\d{10}$', oem):
        oem = oem[1:]

    # Шаг 5: Снятие PE-suffix у DAF
    # Паттерн: строка заканчивается на PE, длина > 4 символа
    if oem.endswith('PE') and len(oem) > 4:
        oem = oem[:-2]

    # Шаг 6: Снятие 742-prefix у Renault/Volvo
    # Паттерн: начинается с 742, итоговый номер (без 742) длиннее 6 символов
    if re.match(r'^742', oem) and len(oem) > 10:
        candidate = oem[3:]
        if len(candidate) >= 7:
            oem = candidate

    return oem


def normalize_oem_list(oem_string: str) -> list[str]:
    """
    Нормализует строку с несколькими OEM (поле кросс_номера).
    Разделители: запятая, пробел, точка с запятой.
    Returns: список нормализованных OEM, пустые строки исключены.
    """
    if not oem_string:
        return []

    # Разделяем по запятой, пробелу или точке с запятой
    raw_list = re.split(r'[,\s;]+', oem_string.strip())

    result = []
    for raw in raw_list:
        normalized = normalize_oem(raw)
        if normalized:
            result.append(normalized)

    return result

Хранение в базе данных:

Для каждого OEM хранятся три значения:

Поле Пример Описание
oem_original A9604621423 Как пришло от поставщика (не изменяется)
oem_normalized 9604621423 После применения normalize_oem()
oem_canonical 9604621423 Главный OEM (при конфликтах нормализаций)

3.2 Валидация наименований

Список стоп-слов — слова, при которых наименование из одного слова считается неполным и требует уточнения:

// dru_lider_importer/src/Service/NamingValidator.php
const STOP_WORDS = [
    'Кронштейн',
    'Болт',
    'Гайка',
    'Шпилька',
    'Кнопка',
    'Накладка',
    'Прокладка',
    'Датчик',
    'Клапан',
    'Шланг',
    'Фильтр',
    'Трубка',
    'Хомут',
    'Пружина',
    'Втулка',
    'Подшипник',
    'Кольцо',
    'Заглушка',
    'Крышка',
    'Патрубок',
    'Уплотнение',
    'Манжета',
    'Сальник',
];

Правило применения стоп-слов:

  1. Привести наименование к нижнему регистру для сравнения
  2. Если наименование (после trim) полностью совпадает с одним из стоп-слов → запустить автофикс
  3. Если наименование начинается со стоп-слова, но содержит ещё слова → наименование допустимо (например, "Кронштейн балансира" — не стоп)

Алгоритм автофикса:

def auto_fix_name(name: str, node_field: str, system_field: str) -> tuple[str, bool]:
    """
    Пытается автоматически улучшить неполное наименование.

    Returns:
        (new_name, needs_review)
        needs_review=True если автофикс не дал достаточного результата
    """
    # Если наименование уже нормальное — возвращаем как есть
    if not is_stop_word_only(name):
        return name, False

    # Попытка 1: добавить поле "узел" если оно информативно
    if node_field and len(node_field) > 3 and node_field.lower() not in [name.lower()]:
        candidate = f"{name} {node_field.lower()}"
        if len(candidate) <= 80:
            return candidate, False  # автофикс успешен

    # Попытка 2: добавить поле "система" если узел пустой
    if system_field and len(system_field) > 3:
        candidate = f"{name} {system_field.lower()}"
        if len(candidate) <= 80:
            return candidate, True  # частичный автофикс, всё равно на проверку

    # Автофикс не удался — оставляем как есть, ставим на проверку
    return name, True

Поведение:

Ситуация Действие
Наименование = одно стоп-слово, узел заполнен Автофикс: {наименование} {узел}, флаг не ставится
Наименование = одно стоп-слово, узел пустой, система заполнена Автофикс частичный: {наименование} {система}, флаг needs_review = true
Наименование = одно стоп-слово, оба поля пустые Наименование без изменений, флаг needs_review = true
Наименование нормальное (больше одного значимого слова) Без изменений, без флага

3.3 Дельта-логика

Дельта-импорт сравнивает новый прайс с текущим состоянием базы данных. Сравнение ведётся по полю oem_normalized.

Статусы строк:

Статус Условие Действие
NEW oem_normalized не найден в БД Создать новый node, Commerce Product, OEM entity
UPDATED_PRICE OEM найден, цена изменилась Обновить цену в Commerce Product Variation
UPDATED_COMPAT OEM найден, марка/модель — новая совместимость Добавить запись в lider_compatibility
DISAPPEARED OEM был в БД, в новом прайсе отсутствует Установить status=out_of_stock в Commerce Product, НЕ удалять
UNCHANGED OEM найден, цена и совместимость не изменились Пропустить (skip), не записывать в БД

Алгоритм дельты:

def calculate_delta(new_rows: list[dict], existing_catalog: dict) -> list[dict]:
    """
    existing_catalog: словарь {oem_normalized: {price, compatibilities: set}}
    Возвращает: список строк со статусом
    """
    delta = []
    new_oems = set()

    for row in new_rows:
        oem_norm = normalize_oem(row['oem'])
        new_oems.add(oem_norm)

        if oem_norm not in existing_catalog:
            delta.append({**row, 'status': 'NEW'})
            continue

        existing = existing_catalog[oem_norm]
        changes = []

        # Проверка цены
        if abs(float(row['цена']) - existing['price']) > 0.01:
            changes.append('PRICE')

        # Проверка совместимости
        compat_key = f"{row['марка']}:{row['модель']}"
        if compat_key not in existing['compatibilities']:
            changes.append('COMPAT')

        if not changes:
            delta.append({**row, 'status': 'UNCHANGED'})
        elif changes == ['PRICE']:
            delta.append({**row, 'status': 'UPDATED_PRICE'})
        elif 'COMPAT' in changes:
            delta.append({**row, 'status': 'UPDATED_COMPAT', 'price_changed': 'PRICE' in changes})

    # Найти исчезнувшие OEM
    disappeared_oems = set(existing_catalog.keys()) - new_oems
    for oem in disappeared_oems:
        delta.append({'oem': oem, 'status': 'DISAPPEARED'})

    return delta

3.4 Извлечение совместимости

Из поля кросс_номера:

Поле содержит OEM-номера других производителей для той же детали. Каждый кросс-номер нормализуется и сохраняется как отдельная OEM entity со связью cross_reference на основной OEM.

Входные данные: "F00RJ01285,0445120229, 1 418 522 006"
После split:    ["F00RJ01285", "0445120229", "1418522006"]
Нормализация:   ["F00RJ01285", "0445120229", "1418522006"]
Результат:      3 кросс-записи связанных с основным OEM

Правило совместимости кросс-номеров:

Кросс-номера из поля кросс_номера автоматически наследуют совместимость основного OEM со всеми моделями. Если аналог подходит только части моделей — его нужно заводить отдельной строкой в CSV:

Случай Метод
F00RJ01285 подходит всем (R5, G5, P5) В поле кросс_номера основного OEM
F00RJ01285 подходит только R5 Отдельная строка: OEM=F00RJ01285, марка=Scania, модель=R5
Вариант написания 96046-21423 Всегда в кросс_номера — это форма написания, не деталь

Отображение аналогов на листинге модели:

При рендеринге листинга модели блок "Аналоги" для каждой детали фильтрует кросс-OEM по совместимости с этой моделью:

-- Аналоги при просмотре списка через /scania/g5/.../
SELECT cross_oem.*
FROM lider_oem cross_oem
JOIN lider_compatibility compat ON compat.part_id = cross_oem.part_id
WHERE cross_oem.cross_ref_of = :main_oem_id    -- кросс основного OEM
  AND compat.model_id = :current_model_id       -- только G5

Результат: в разделе /scania/r5/ и /scania/g5/ блок "Аналоги" может показывать разные наборы кросс-OEM для одной и той же детали.

Из поля комментарий (основной источник совместимости):

Совместимость пишется в поле комментарий в строгом формате:

Подходит для: Scania R5, Scania G5, Scania P5

или

Совместимые модели: Volvo FH4, Volvo FM4, Renault T

Парсинг строгий — по ключевым словам с двоеточием:

COMPAT_PATTERNS = [
    # "Подходит для: Scania R5, Scania G5"  ← основной формат
    r'[Пп]одходит\s+для\s*:\s*([^\n]+)',
    # "Совместимые модели: Volvo FH4, Volvo FM4"
    r'[Сс]овместимые\s+модели\s*:\s*([^\n]+)',
]

def parse_compatibility(comment: str) -> list[dict]:
    """
    Парсит поле комментарий → список совместимостей.
    Формат: "Подходит для: Марка Модель, Марка Модель"
    Модели как в Bazon (те же названия что в полях марка и модель).

    Returns: [{'brand': 'Scania', 'model': 'R5'}, ...]
    """
    result = []
    for pattern in COMPAT_PATTERNS:
        match = re.search(pattern, comment)
        if not match:
            continue

        models_str = match.group(1).strip()
        # Разбиваем по запятой
        model_tokens = [m.strip() for m in models_str.split(',') if m.strip()]

        for token in model_tokens:
            # Ожидаем формат "Марка Модель" (два слова)
            parts = token.split(None, 1)
            if len(parts) == 2:
                result.append({'brand': parts[0], 'model': parts[1]})
            elif len(parts) == 1:
                # Только марка без модели — логируем как предупреждение
                result.append({'brand': parts[0], 'model': None, 'warning': 'no_model'})

        break  # нашли один паттерн — достаточно

    return result

Пример разбора:

Комментарий: "Подходит для: Scania R5, Scania G5, Scania P5, Mercedes Actros MP4"

→ [
    {'brand': 'Scania', 'model': 'R5'},
    {'brand': 'Scania', 'model': 'G5'},
    {'brand': 'Scania', 'model': 'P5'},
    {'brand': 'Mercedes', 'model': 'Actros MP4'},
  ]
→ Создаётся 4 записи lider_compatibility

Важно: Модели в комментарии должны совпадать с taxonomy terms lider_brands и lider_models. Если совпадения нет — запись логируется с предупреждением brand_not_found или model_not_found, но импорт не останавливается.

Результат парсинга сохраняется с флагом source=comment_parsed для аудита.


4. Выходные данные (что создаётся в Drupal)

4.1 Taxonomy Terms

vocabulary: lider_brands
- name: Scania (значение из поля марка)
- field_brand_slug: scania (slug для URL)
- field_canonical_priority: порядок приоритета моделей этого бренда

vocabulary: lider_models
- name: R5
- field_model_slug: r5
- field_brand: reference → lider_brands term
- field_generation: 5th Generation
- field_years: 2004–2016

vocabulary: lider_systems
- name: Топливная система
- field_system_slug: toplivnaya-sistema
- field_group: Двигатель (родительская группа)

vocabulary: lider_nodes (узлы)
- name: Форсунка
- field_node_slug: forsunka

4.2 Drupal Nodes (страницы деталей)

content type: lider_part

Поле Тип Описание
title string Наименование детали
field_oem_canonical string Главный нормализованный OEM
field_manufacturer string Производитель компонента
field_condition list (string) новый / аналог / оригинал
field_brand reference → lider_brands Марка (основная)
field_system reference → lider_systems Система
field_node_ref reference → lider_nodes Узел
field_needs_review boolean Флаг: требует проверки наименования
field_import_source string Источник: bazon_csv
field_bazon_article string Артикул Bazon
status boolean Опубликовано (true если есть цена)

4.3 Commerce Products

product type: lider_spare_part
- title: наименование (дублирует node.title)
- stores: default store
- variations: одна или несколько вариаций (по цене/состоянию)

product variation type: lider_spare_part_variation
- sku: {oem_normalized} или {oem_normalized}-{состояние}
- price: Amount объект с currency RUB
- field_condition: аналог / оригинал / новый
- status: active (если деталь доступна) или inactive (DISAPPEARED)

4.4 OEM Entities

entity type: lider_oem

Поле Описание
oem_original OEM как пришёл от поставщика
oem_normalized После normalize_oem()
oem_canonical Главный (для поиска, URL)
oem_type main / cross_reference
part_reference reference → lider_part node
brand_hint Марка-производитель (Mercedes, DAF и т.д.) — помогает нормализации

4.5 Compatibility Entities

entity type: lider_compatibility

Поле Описание
part_id reference → lider_part node
brand_id reference → lider_brands taxonomy term
model_id reference → lider_models taxonomy term
source catalog_csv / donor_expansion / text_parsed / manual
confidence high / medium / low

4.6 URL Aliases

Формат: /zapchasti/{brand_slug}/{model_slug}/{system_slug}/{name_slug}-{oem_canonical}/

Правило ANY для {model_slug}:

Совместимость детали {model_slug} Пример URL
1 бренд, 1 модель slug модели (r5) /zapchasti/scania/r5/toplivnaya-sistema/forsunka-9604621423/
1 бренд, 2+ модели any /zapchasti/scania/any/toplivnaya-sistema/forsunka-9604621423/
2+ бренда any + приоритетный бренд /zapchasti/volvo/any/toplivnaya-sistema/forsunka-1765026/

Приоритет бренда при 2+ брендах: Scania → Volvo → Mercedes → MAN → DAF → Renault → Iveco → Liebherr

Правила slug-ификации:
- Кириллица → транслитерация (ГОСТ 7.79-2000)
- Пробелы → дефис
- Множественные дефисы → один дефис
- Точки, запятые, скобки → удаляются
- Максимальная длина slug наименования: 50 символов

4.7 Обработка URL-вариантов: Format-error (Type C)

Правило: Один OEM = один URL. Model-alias страниц нет.

Format-error (HTTP 301 redirect)

Ошибочные написания OEM в URL (A-prefix, дефисы, 742-prefix) → 301 на canonical URL. Обрабатывается OemNormalizerSubscriber. Яндекс и Google эти страницы не индексируют.

/zapchasti/scania/r5/toplivnaya-sistema/forsunka-a9604621423/
→ 301 →
/zapchasti/scania/r5/toplivnaya-sistema/forsunka-9604621423/
/zapchasti/scania/r5/toplivnaya-sistema/forsunka-96046-21423/
→ 301 →
/zapchasti/scania/r5/toplivnaya-sistema/forsunka-9604621423/

Canonical URL возвращает 200 OK. <link rel="canonical"> = self. Страница в sitemap.


5. CLI интерфейс (drush команды)

5.1 Основные команды

# Импорт всего каталога из файла
drush lider:import:catalog \
  [--mode=upsert|insert|dry-run] \
  [--brand=scania] \
  [--limit=100] \
  [--file=path/to/catalog.csv] \
  [--batch=100]

# Импорт совместимости
drush lider:import:compatibility \
  [--source=catalog|donors|comments|all] \
  [--brand=scania]

# Перестройка редиректов
drush lider:import:redirects \
  [--rebuild] \
  [--brand=scania]

# Пересчёт canonical URL
drush lider:import:canonical \
  [--recalculate] \
  [--brand=scania] \
  [--dry-run]

# Только валидация без импорта
drush lider:import:validate \
  --file=path/to/catalog.csv \
  [--verbose]

# Показать статус последнего импорта
drush lider:import:status

# Показать очередь на проверку наименований
drush lider:import:review-queue \
  [--brand=scania] \
  [--limit=50]

5.2 Режимы импорта (--mode)

Режим Описание
upsert Создать новые + обновить существующие (рекомендуемый для обновлений)
insert Только создать новые, существующие игнорировать
dry-run Симуляция: показать что будет сделано, ничего не записывать

5.3 Примеры использования

# Полная симуляция нового прайса
drush lider:import:validate --file=/tmp/bazon_new.csv --verbose

# Тестовый импорт первых 100 записей Scania
drush lider:import:catalog --brand=scania --limit=100 --mode=dry-run --file=/tmp/bazon.csv

# Реальный импорт Scania
drush lider:import:catalog --brand=scania --file=/tmp/bazon.csv --mode=upsert

# Полный импорт всех брендов
drush lider:import:catalog --file=/tmp/bazon.csv --mode=upsert

# Расширение совместимости через доноров
drush lider:import:compatibility --source=donors

# Пересчитать canonical только для DAF
drush lider:import:canonical --recalculate --brand=daf

6. Логирование и отчёт

6.1 Формат отчёта после импорта

╔═══════════════════════════════════════════════════════════════════╗
║           ОТЧЁТ ИМПОРТА: lideravto catalog                       ║
║           2026-02-27 14:35:22 UTC                                ║
╠═══════════════════════════════════════════════════════════════════╣
║ Файл:          bazon_prays_2026-02.csv                          ║
║ Режим:         upsert                                            ║
║ Бренд:         all                                               ║
╠═══════════════════════════════════════════════════════════════════╣
║ СТРОКИ CSV:                                                       ║
║   Всего строк в файле:         13 621                            ║
║   Успешно обработано:          13 598                            ║
║   Пропущено (ошибки):              23                            ║
╠═══════════════════════════════════════════════════════════════════╣
║ РЕЗУЛЬТАТЫ:                                                       ║
║   Новых деталей создано:          124                            ║
║   Обновлено цен:                  891                            ║
║   Добавлено связей совместимости:  45                            ║
║   Без изменений (skip):        12 538                            ║
║   Исчезнувших деталей:             12  → out_of_stock            ║
╠═══════════════════════════════════════════════════════════════════╣
║ КАЧЕСТВО ДАННЫХ:                                                  ║
║   Исправлено OEM (A-prefix):       31                            ║
║   Исправлено OEM (PE-suffix):       8                            ║
║   Исправлено OEM (742-prefix):     15                            ║
║   Исправлено OEM (кириллица):       4                            ║
║   Требуют проверки наименований:   38  → /admin/lider/review     ║
╠═══════════════════════════════════════════════════════════════════╣
║ CANONICAL:                                                        ║
║   Пересчитан canonical:            89 деталей                    ║
║   Новых редиректов создано:        89                            ║
║   Sitemap перестроен               ✓                             ║
╠═══════════════════════════════════════════════════════════════════╣
║ ВРЕМЯ:                                                            ║
║   Нормализация OEM:            0:00:45                           ║
║   Загрузка в БД:               0:08:30                           ║
║   Пересчёт canonical:          0:02:15                           ║
║   Итого:                       0:11:30                           ║
╠═══════════════════════════════════════════════════════════════════╣
║ ОШИБКИ (23):                                                      ║
║   [строка 1247] пустое наименование: пропущена                   ║
║   [строка 3891] некорректная цена '18 500,00 руб': исправлено    ║
║   ... (21 ещё) → /admin/reports/lider-import-log/latest          ║
╚═══════════════════════════════════════════════════════════════════╝

6.2 Уровни логирования

Уровень Что пишется
ERROR Строки пропущенные из-за критических ошибок
WARNING Исправленные OEM, частичные автофиксы наименований
INFO Сводная статистика (создано / обновлено / пропущено)
DEBUG Каждая обработанная строка (включать только при отладке)

6.3 Хранение логов


7. Обработка ошибок

7.1 Таблица ошибок и реакций

Ситуация Тип Действие
Файл не найден CRITICAL Остановить импорт, уведомить
Файл не UTF-8 ERROR Попытка конвертации из cp1251/win1251, если не удалось — остановить
Неправильный разделитель ERROR Попытка автоопределения разделителя
Нет заголовка (первая строка — данные) WARNING Продолжить, предупредить
Пустое поле наименование ERROR Пропустить строку
Пустое поле oem ERROR Пропустить строку если нет артикула; если артикул есть — создать с артикулом как временным ID
Пустые поля марка или модель ERROR Пропустить строку
Некорректная цена WARNING Попытка парсинга (18 500,00 руб18500); если не удалось — пропустить
Очень длинное наименование (>255 символов) WARNING Усечь до 255, залогировать
OEM с ошибкой (A-prefix, PE-suffix и т.д.) WARNING Нормализовать, залогировать исправление
Дубликат OEM в пределах одного файла WARNING Обработать оба как UPDATED_COMPAT (добавить совместимость)
Таймаут БД при записи ERROR Повторить 3 раза с паузой 5с; если не удалось — записать в retry-очередь
3 ошибки подряд в одном батче CRITICAL Остановить батч, сообщить администратору, продолжить со следующего батча

7.2 Retry-очередь

Записи не прошедшие из-за временных ошибок (таймаут, блокировка БД) помещаются в таблицу lider_import_retry и повторно обрабатываются по cron через 15 минут.


8. Производительность

8.1 Параметры батч-обработки

Параметр Значение Описание
Batch size 100 строк Размер одного батча
Memory limit 512 MB Минимум для нормальной работы
Max execution time 300 сек Для PHP CLI (drush) — без ограничения
DB connection timeout 30 сек Таймаут подключения к MySQL

8.2 Ожидаемое время выполнения

Операция Строк Ожидаемое время
Валидация CSV 13 621 30 сек
Нормализация OEM 13 621 45 сек
Дельта-расчёт 13 621 1 мин
Загрузка NEW в БД ~124 20 сек
Обновление цен ~891 40 сек
Добавление совместимости ~45 10 сек
Пересчёт canonical 9 212 3 мин
Генерация редиректов ~89 новых 30 сек
Итого ~7–12 мин

Первоначальный полный импорт (9 212 новых записей): ~45–60 минут.

8.3 Индексы БД

Перед импортом проверить наличие индексов:

-- Критически важны для производительности дельты:
CREATE INDEX IF NOT EXISTS idx_lider_oem_normalized ON lider_oem (oem_normalized);
CREATE INDEX IF NOT EXISTS idx_lider_compat_part ON lider_compatibility (part_id, brand_id, model_id);

9. Критерии приёмки

9.1 Чеклист: система импорта готова к эксплуатации

Функциональность:
- [ ] drush lider:import:validate выявляет все 6 типов ошибок OEM и сообщает о них
- [ ] drush lider:import:catalog --mode=dry-run не вносит изменений в БД
- [ ] Первоначальный импорт 9 212 OEM завершается без ошибок
- [ ] После импорта 9 212 страниц /zapchasti/{brand}/{model}/... возвращают 200 OK
- [ ] После импорта OEM с ошибкой (A-prefix) — canonical URL без буквы A
- [ ] Дельта-импорт: цена изменилась в CSV → изменилась на сайте
- [ ] Дельта-импорт: OEM исчез из CSV → статус out_of_stock, страница осталась (200 OK)
- [ ] Дельта-импорт: новый OEM → новая страница создана

SEO:
- [ ] Каждая страница детали возвращает 200 и имеет тег <link rel="canonical"> с самим собой (canonical = self)
- [ ] Страница с 1 моделью имеет URL вида /brand/model/..., с 2+ моделями — /brand/any/...
- [ ] Format-error URL (ошибочный OEM — A-prefix, дефисы, 742-prefix) возвращают 301 на правильный URL

Качество данных:
- [ ] Детали с наименованием из стоп-листа получают флаг needs_review
- [ ] Очередь проверки доступна в /admin/lider/review
- [ ] После снятия флага деталь не возвращается в очередь при следующем импорте (если наименование не изменилось в CSV)

Производительность:
- [ ] Дельта-импорт 13 621 строк завершается за ≤ 15 минут
- [ ] Потребление памяти PHP не превышает 512 MB
- [ ] Нет N+1 запросов к БД (проверить через drush devel:sql или EXPLAIN)

Логирование:
- [ ] Отчёт содержит все метрики из раздела 6.1
- [ ] Ошибки с номерами строк CSV записываются в лог
- [ ] Лог доступен через /admin/reports/lider-import-log/latest


Документ подготовлен для проекта new.lideravto.ru
Дата: 2026-02-27


← Назад в оглавление