Версия: 1.0
Дата: 2026-02-27
Проект: new.lideravto.ru — Drupal 11.3.3 + Commerce 3.x
Аудитория: разработчики Drupal
Все модули находятся в /it/lider-drupal/tst/ и ещё не задеплоены в production (/app/).
205 тестов написаны и проходят.
tst/modules/custom/
├── dru_lider_catalog 284 KB — каталог товаров
├── dru_lider_models 120 KB — модели грузовиков
├── dru_lider_importer 76 KB — импорт CSV
├── dru_lider_parts 72 KB — запчасти и OEM
├── dru_lider_compatibility 64 KB — матрица совместимости
├── dru_lider_seo 28 KB — SEO и редиректы
├── dru_lider_setup 20 KB — установка
├── dru_lider_products 16 KB — расширение Commerce
└── dru_lider_frontend 4 KB — шаблон и меню
dru_lider_setup
└── инициализирует всё окружение
dru_lider_models
└── taxonomy: бренды, модели, поколения, системы
dru_lider_parts
└── зависит от: dru_lider_models
└── OEM нормализация, хранение
dru_lider_catalog
└── зависит от: dru_lider_parts, dru_lider_models
└── content type lider_part, страницы деталей
dru_lider_products
└── зависит от: dru_lider_catalog
└── Commerce Product + Variation
dru_lider_compatibility
└── зависит от: dru_lider_catalog, dru_lider_models
└── связи деталь ↔ модель
dru_lider_importer
└── зависит от: все выше
└── CSV → Drupal entities
dru_lider_seo
└── зависит от: dru_lider_catalog, dru_lider_compatibility
└── canonical, redirects, sitemap
dru_lider_frontend
└── зависит от: все выше (данные из БД)
└── шаблоны, меню, вёрстка
Что делает:
Устанавливает начальную конфигурацию Drupal для проекта. Создаёт vocabulary, типы контента, поля, начальные taxonomy terms для брендов и моделей. Запускается один раз при деплое.
Статус: ✅ Готов к использованию
Что делать: Использовать как есть. Запуск:
drush en dru_lider_setup
drush lider:setup:install
Ключевые команды:
- drush lider:setup:install — полная установка
- drush lider:setup:check — проверка конфигурации
Что делает:
Taxonomy vocabularies для структуры грузовых автомобилей:
- lider_brands — бренды (Scania, Volvo, Mercedes...)
- lider_models — модели (R5, FH, Actros...)
- lider_generations — поколения (5-я серия, MP4...)
- lider_systems — системы (Двигатель, КПП, Мосты...)
- lider_nodes — узлы (Форсунка, Насос, Балансир...)
Статус: ✅ Готов, небольшая доработка
Что добавить:
- Поле canonical_priority в lider_brands — порядок приоритета моделей для выбора canonical URL (массив: ["R", "G", "P", "F"] для Scania)
- Поле engine_family в lider_models — к какому семейству двигателей относится (для donor expansion)
Оценка доработки: 15 минут
Файл конфига после доработки: it/data/references/canonical_priority.yaml
# canonical_priority.yaml — порядок выбора canonical модели
scania:
priority: [R, G, P, F, L]
description: "R — магистральный, самый массовый"
volvo:
priority: [FH, FM, FMX, FL, FE]
description: "FH — флагманский тягач"
mercedes:
priority: [Actros, Arocs, Antos, Axor, Atego]
description: "Actros — основной магистральный"
man:
priority: [TGX, TGS, TGM, TGL, TGA]
description: "TGX — тяжёлый магистральный"
daf:
priority: [XF, CF, LF]
description: "XF — флагман DAF"
renault:
priority: [T, C, K, D, Master]
description: "T — основной магистральный"
iveco:
priority: [Stralis, Trakker, Eurocargo, Daily]
description: "Stralis — основная магистральная линейка"
Что делает:
Entity lider_oem для хранения OEM-номеров:
- Поля: oem_original, oem_normalized, oem_canonical
- Метод normalize() — реализация алгоритма нормализации
- Связь с lider_part node
Статус: ✅ Готов, мелкая доработка
Что добавить:
- Поле brand_hint — подсказка для нормализации (например, "Mercedes" помогает определить A-prefix)
- Метод detectBrand() по паттерну OEM
Оценка доработки: 10 минут
Что делает:
Content type lider_part — основная страница детали:
- Все поля карточки товара
- Отображение: блок основных данных, блок совместимости, блок кросс-номеров
- Breadcrumbs, page title
Статус: ✅ Готов к использованию
Что добавить:
- Поле needs_review (boolean) для флага проверки наименования — 5 минут
Что делает:
Commerce Product типа lider_spare_part:
- Product variation с полями: SKU (= OEM), цена (RUB), состояние (новый/аналог/оригинал)
- Add to cart форма
- Интеграция с Drupal Commerce checkout
Статус: ✅ Готов к использованию
Что делать: Использовать как есть.
Что делает:
Entity lider_compatibility — связи деталь ↔ модель:
- Поля: part_id, brand_id, model_id, source, confidence
- Создаёт записи из прямых данных CSV
Статус: ⚠️ Частично готов — есть прямые связи, нет расширения
Что добавить:
A. Donor Expansion — расширение по донорам
Новый сервис DonorExpander:
// dru_lider_compatibility/src/Service/DonorExpander.php
class DonorExpander {
public function expand(string $oem, string $system): int {
// 1. Найти прямые совместимости для OEM
// 2. Для каждой совместимости найти агрегат (двигатель/коробка)
// 3. Найти все другие грузовики с тем же агрегатом
// 4. Создать записи совместимости с source=donor_expansion
// Вернуть: количество созданных записей
}
}
Файл доноров it/data/references/donors.yaml:
engines:
dc13_xpi_5:
name: "Scania DC13 XPI (5 серия)"
trucks: [scania_r5, scania_g5, scania_p5, scania_f5]
years: "2010-2016"
dc13_xpi_6:
name: "Scania DC13 XPI (6 серия)"
trucks: [scania_r6, scania_g6, scania_p6, scania_s]
years: "2017+"
d13_fh4:
name: "Volvo D13 (FH4)"
trucks: [volvo_fh4, volvo_fm4, volvo_fmx4]
years: "2013-2020"
d13_fh5:
name: "Volvo D13 (FH5)"
trucks: [volvo_fh5, volvo_fm5]
years: "2021+"
om471_mp4:
name: "Mercedes OM471 (MP4)"
trucks: [mercedes_actros_mp4, mercedes_arocs]
years: "2012+"
d2676_euro6:
name: "MAN D2676 Euro 6"
trucks: [man_tgx_e6, man_tgs_e6, man_tgm_e6]
years: "2013+"
gearboxes:
grs895:
name: "Scania GRS895"
trucks: [scania_r5, scania_g5, scania_p5]
grs905:
name: "Scania GRS905"
trucks: [scania_r6, scania_g6]
i_shift_fh4:
name: "Volvo I-Shift (FH4)"
trucks: [volvo_fh4, volvo_fm4]
axles:
rs1344:
name: "Volvo RS1344 (задний мост)"
trucks: [volvo_fh3, volvo_fh4, renault_t]
note: "AB Volvo концерн — общий мост"
Drush команда: drush lider:import:compatibility --source=donors
B. Парсинг комментариев
Метод parseTextCompatibility(string $text): array — на базе регулярных выражений из TZ_IMPORT.md.
Оценка доработки: 60 минут (DonorExpander + donors.yaml + парсинг комментариев)
Что делает:
Batch-импорт из CSV. Читает файл → нормализует OEM → создаёт nodes + Commerce products.
Статус: ⚠️ Частично готов — есть insert, нет delta и валидации
Что добавить:
A. Дельта-логика — сравнение нового прайса с текущей базой
- Читать oem_normalized из БД в memory (bulk query)
- Сравнивать каждую строку CSV — NEW / UPDATED / DISAPPEARED
- Сохранять только изменённое (не писать UNCHANGED)
B. NamingValidator сервис
// dru_lider_importer/src/Service/NamingValidator.php
class NamingValidator {
const STOP_WORDS = ['Кронштейн', 'Болт', 'Гайка', ...];
public function validate(string $name, string $node = '', string $system = ''): ValidationResult {
// Вернуть: исправленное имя + флаг needs_review
}
}
C. Отчёт импорта
- Подсчёт статистики по всем статусам
- Форматированный вывод в CLI
- Сохранение в lider_import_log
Оценка доработки: 45 минут
Что делает:
- Canonical tags (<link rel="canonical" href="self">)
- URL aliases по шаблону /zapchasti/{brand}/{model_or_any}/{system}/{name}-{oem}/
- XML Sitemap (все страницы деталей — они все canonical)
- 301 редиректы для format-error OEM
Правило URL:
- 1 бренд, 1 модель → /scania/r5/...
- 1 бренд, 2+ модели → /scania/any/...
- 2+ бренда → /priority-brand/any/... (Scania → Volvo → Mercedes → MAN → DAF → Renault → Iveco → Liebherr)
Статус: ⚠️ Частично готов — базовый canonical, нужен алгоритм ANY, нужен OemNormalizerSubscriber
Что добавить:
A. OemNormalizerSubscriber — НОВЫЙ КОМПОНЕНТ, ВЫСОКИЙ ПРИОРИТЕТ
Динамический обработчик format-error редиректов. Один класс вместо 2 400 статических правил. Обрабатывает только ошибки формата OEM (A-prefix, PE-suffix, 742-prefix, дефисы) → 301 на canonical URL.
// dru_lider_seo/src/EventSubscriber/OemNormalizerSubscriber.php
namespace Drupal\dru_lider_seo\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpFoundation\RedirectResponse;
class OemNormalizerSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents(): array {
return [KernelEvents::REQUEST => ['onRequest', 30]];
}
public function onRequest(RequestEvent $event): void {
$path = $event->getRequest()->getPathInfo();
// Проверяем что путь — страница детали
if (!preg_match('|^/zapchasti/[^/]+/[^/]+/[^/]+/[^/]+-([^/]+)/$|', $path, $matches)) {
return;
}
$oem_from_url = $matches[1];
$oem_normalized = $this->normalizeOem($oem_from_url);
// Если OEM уже нормализован — ничего не делаем
if ($oem_from_url === $oem_normalized) {
return;
}
// Ищем canonical страницу для нормализованного OEM
$canonical_path = $this->lookupCanonicalPath($oem_normalized);
if (!$canonical_path) {
return; // OEM не найден в базе — пусть 404 обработает Drupal
}
// 301 на canonical
$event->setResponse(new RedirectResponse($canonical_path, 301));
}
private function normalizeOem(string $oem): string {
// Кириллица → латиница
$map = ['А'=>'A','В'=>'B','Е'=>'E','О'=>'O','Р'=>'R','С'=>'C','Х'=>'X'];
$oem = strtr(strtoupper($oem), $map);
// Удалить разделители
$oem = preg_replace('/[\s\-\.\,]/', '', $oem);
// A-prefix Mercedes
if (preg_match('/^A(\d{10})$/', $oem, $m)) $oem = $m[1];
// PE-suffix DAF
if (str_ends_with($oem, 'PE') && strlen($oem) > 4) $oem = substr($oem, 0, -2);
// 742-prefix Renault/Volvo
if (preg_match('/^742(.{7,})$/', $oem, $m)) $oem = $m[1];
return $oem;
}
private function lookupCanonicalPath(string $oem_normalized): ?string {
// Запрос в БД: найти canonical URL alias для нормализованного OEM
// SELECT alias FROM path_alias WHERE oem_canonical = :oem AND is_canonical = 1
return \Drupal::service('dru_lider_seo.canonical_resolver')
->getPathByOem($oem_normalized);
}
}
Регистрация в dru_lider_seo.services.yml:
dru_lider_seo.oem_normalizer_subscriber:
class: Drupal\dru_lider_seo\EventSubscriber\OemNormalizerSubscriber
arguments: ['@dru_lider_seo.canonical_resolver']
tags:
- { name: event_subscriber }
B. CanonicalUrlBuilder — URL по правилу ANY
Алгоритм в CanonicalUrlBuilder:
public function buildCanonicalUrl(array $compatible_brands_models): string {
// Определяем приоритетный бренд
$brand = $this->selectTopPriorityBrand($compatible_brands_models);
// Если для этого бренда только одна модель → конкретная модель в URL
$models_for_brand = $this->getModelsForBrand($compatible_brands_models, $brand);
$model_slug = count($models_for_brand) === 1
? $models_for_brand[0]->slug
: 'any';
return "/zapchasti/{$brand->slug}/{$model_slug}/...";
}
private function selectTopPriorityBrand(array $brands_models): BrandTerm {
// Приоритет: scania > volvo > mercedes > man > daf > renault > iveco > liebherr
$priority = ['scania'=>1,'volvo'=>2,'mercedes'=>3,'man'=>4,'daf'=>5,'renault'=>6,'iveco'=>7,'liebherr'=>8];
usort($brands_models, fn($a,$b) => ($priority[$a->brand->slug]??99) <=> ($priority[$b->brand->slug]??99));
return $brands_models[0]->brand;
}
Оценка доработки: 30 минут
Что делает:
Twig шаблоны для страниц, CSS стили. Пока минимальный скелет.
Статус: ⚠️ Базовый скелет — нужна реализация
Что сделать:
A. Верхнее меню по брендам
- Автогенерируется из taxonomy lider_brands
- Горизонтальное, 8 брендов
- Выбранный бренд подсвечен
B. Левое контекстное меню
- На странице бренда: список моделей этого бренда
- На странице модели: список систем
- На странице системы: список узлов
- Хлебные крошки на каждом уровне
C. Карточка товара — три блока:
Блок 1: Основные данные
<div class="part-main">
<h1>{{ node.title }}</h1>
<div class="part-oem">OEM: {{ node.field_oem_canonical }}</div>
<div class="part-price">{{ product.price }}</div>
{{ form.add_to_cart }}
</div>
Блок 2: Совместимые модели (из lider_compatibility)
<div class="part-compatibility">
<h3>Совместимые модели</h3>
{% for brand, models in compatibility %}
<div class="brand-group">
<strong>{{ brand }}</strong>:
{% for model in models %}
<a href="/zapchasti/{{ brand.slug }}/{{ model.slug }}/">{{ model.name }}</a>
{% endfor %}
</div>
{% endfor %}
</div>
Блок 3: Аналоги (кросс-номера, отфильтрованные по текущей модели)
<div class="part-crosses">
<h3>Аналоги других производителей</h3>
{#
cross_parts передаётся уже отфильтрованным по current_model_id:
- На /scania/r5/... → только кроссы совместимые с R5
- На /scania/g5/... → только кроссы совместимые с G5
Кроссы несовместимые с текущей моделью не показываются.
#}
{% if cross_parts %}
{% for cross in cross_parts %}
{% if cross.has_page %}
<a href="{{ cross.url }}">{{ cross.manufacturer }}: {{ cross.oem }}</a>
{% else %}
<span>{{ cross.manufacturer }}: {{ cross.oem }}</span>
{% endif %}
{% endfor %}
{% else %}
<p class="no-crosses">Аналоги для данной модели не найдены</p>
{% endif %}
</div>
PHP (Block plugin / Controller) — передача отфильтрованных кросс-OEM:
// Контекст модели — из URL (может быть 'any' или конкретный slug)
$model_slug = $this->routeMatch->getParameter('model');
$current_model_id = ($model_slug !== 'any')
? $this->modelResolver->getIdBySlug($model_slug)
: null;
// Запрос: кросс-OEM основного + фильтр по модели
$cross_parts = $this->crossRefService->getCrossesForModel(
oem_canonical: $oem_canonical,
model_id: $current_model_id // null (any) → все кроссы
);
На странице /brand/any/... (current_model_id = null) показываются все кроссы. На странице /brand/r5/... — только кроссы совместимые с R5.
Оценка: 45 минут
dru_lider_seo/src/EventSubscriber/a9604621423), PE-suffix (1653987pe), 742-prefix (7421765026), дефисы (96046-21423), пробелыdru_lider_importer/src/Service/NamingValidator.phpузелdru_lider_compatibility/src/Service/DonorExpander.php| Модуль / Компонент | Действие | Что именно | Оценка |
|---|---|---|---|
| dru_lider_setup | Использовать | Без изменений | — |
| dru_lider_models | Доработать | Поля canonical_priority, engine_family | 15 мин |
| dru_lider_parts | Доработать | Поле brand_hint | 10 мин |
| dru_lider_catalog | Доработать | Поле needs_review | 5 мин |
| dru_lider_products | Использовать | Без изменений | — |
| dru_lider_compatibility | Доработать | DonorExpander, парсинг комментариев | 60 мин |
| dru_lider_importer | Доработать | Дельта-логика, NamingValidator, отчёт | 45 мин |
| dru_lider_seo | Доработать | OemNormalizerSubscriber, canonical по узлу | 30 мин |
| dru_lider_frontend | Реализовать | Меню (верх + лево), карточка товара | 45 мин |
| OemNormalizerSubscriber | Создать | EventSubscriber (код готов) | 20 мин |
| NamingValidator | Создать | Сервис валидации | 15 мин |
| DonorExpander | Создать | Сервис расширения | 30 мин |
| ИТОГО | ~4,5 часа |
Волна 1 (независимые, можно параллельно):
1. Доработка dru_lider_models — поле canonical_priority (15 мин)
2. Создание donors.yaml и canonical_priority.yaml (20 мин)
3. Доработка dru_lider_parts — brand_hint (10 мин)
Волна 2 (после Волны 1):
4. Создание NamingValidator (15 мин)
5. Доработка dru_lider_importer — дельта + валидация + отчёт (45 мин)
6. Создание DonorExpander (30 мин)
Волна 3 (после Волны 2):
7. Создание OemNormalizerSubscriber (20 мин)
8. Доработка dru_lider_seo — canonical алгоритм (30 мин)
9. Реализация dru_lider_frontend — меню + карточка (45 мин)
Волна 4:
10. Деплой всего в /app/ и первый тестовый импорт
Документ подготовлен для проекта new.lideravto.ru
Дата: 2026-02-27