Реалізація автоматичного оновлення описів та характеристик товарів
Описи та характеристики товарів — це контент, який постачальники регулярно розширюють та уточняють. Новий ГОСТ, виправлені технічні параметри, додані сертифікати — все це повинно потрапляти в каталог без ручної роботи редакторів. При цьому якщо контент-менеджер вручну переписав опис — автоматика не повинна його затирати.
Структура даних для керованого оновлення
Ключовий принцип: розділяти джерело даних (постачальник) та фінальний контент (що показується на сайті), з прапором «вручну відредаговано».
CREATE TABLE product_content (
product_id int REFERENCES products(id),
source varchar(30), -- supplier_id або 'manual'
field varchar(50), -- description | spec_weight | spec_color ...
value text,
is_manual_override boolean DEFAULT false,
supplier_value text, -- останнє значення від постачальника
updated_at timestamptz,
PRIMARY KEY (product_id, field)
);
При автооновленні: якщо is_manual_override = true — оновлюємо тільки supplier_value, але не value. Контент-менеджер бачить розбіжність в інтерфейсі та вирішує, чи прийняти зміну постачальника.
Джерела описів
XML-фід постачальника
Більшість виробничих компаній надають XML з розширеними атрибутами:
<product article="ABC-123">
<description lang="uk"><![CDATA[Детальний опис...]]></description>
<attributes>
<attribute name="weight" unit="kg">2.5</attribute>
<attribute name="color">Чорний</attribute>
<attribute name="material">Нержавіюча сталь</attribute>
</attributes>
</product>
Парсер на PHP:
class XmlDescriptionSource implements DescriptionSourceInterface
{
public function fetch(): iterable
{
$xml = new \XMLReader();
$xml->open($this->url);
while ($xml->read()) {
if ($xml->nodeType === \XMLReader::ELEMENT && $xml->name === 'product') {
$node = new \SimpleXMLElement($xml->readOuterXml());
yield $this->parseProduct($node);
}
}
$xml->close();
}
private function parseProduct(\SimpleXMLElement $node): array
{
$data = [
'sku' => (string) $node['article'],
'description' => (string) $node->description,
'attributes' => [],
];
foreach ($node->attributes->attribute as $attr) {
$data['attributes'][(string) $attr['name']] = (string) $attr;
}
return $data;
}
}
XMLReader читає файл потоком — не завантажує весь XML у пам'ять, що критично при каталогах від 100 000 позицій.
API з частковим оновленням
Якщо постачальник надає endpoint змін:
GET /products/updates?fields=description,attributes&since=2024-01-15T10:00:00Z
Повертає тільки товари, у яких змінилося хоча б одне з указаних полів — істотно скорочує обсяг обробки.
Оновлення характеристик з нормалізацією
Постачальник надсилає характеристики у власному форматі — потрібно привести до внутрішньої схеми сайту:
class AttributeNormalizer
{
// Маппінг імен атрибутів постачальника → внутрішні ключі
private array $nameMap = [
'weight' => 'spec_weight_kg',
'маса' => 'spec_weight_kg',
'вага нето' => 'spec_weight_kg',
'color' => 'spec_color',
'колір' => 'spec_color',
];
public function normalize(string $supplierName, mixed $value): ?array
{
$key = $this->nameMap[mb_strtolower(trim($supplierName))] ?? null;
if (!$key) return null;
return ['key' => $key, 'value' => $this->castValue($key, $value)];
}
private function castValue(string $key, mixed $raw): mixed
{
return match(true) {
str_starts_with($key, 'spec_weight') => (float) str_replace(',', '.', $raw),
default => (string) $raw,
};
}
}
Job-ланцюжок для оновлення контенту
Оновлення описів тяжче за оновлення цін — контент великий, потрібно нормалізувати атрибути, порівнювати з override-флагами. Оптимальна схема: окрема чергу з низьким паралелізмом.
class UpdateProductDescriptionsJob implements ShouldQueue
{
public int $tries = 3;
public int $backoff = 60; // секунди між повторами
public function handle(
DescriptionSourceInterface $source,
AttributeNormalizer $normalizer,
ContentUpdater $updater,
): void {
foreach ($source->fetch() as $item) {
$productId = Product::where('sku', $item['sku'])->value('id');
if (!$productId) continue;
// Опис
$updater->updateField($productId, 'description', $item['description']);
// Характеристики
foreach ($item['attributes'] as $name => $value) {
$normalized = $normalizer->normalize($name, $value);
if ($normalized) {
$updater->updateField($productId, $normalized['key'], $normalized['value']);
}
}
}
}
}
Логіка ContentUpdater
class ContentUpdater
{
public function updateField(int $productId, string $field, mixed $newValue): void
{
$existing = ProductContent::where([
'product_id' => $productId,
'field' => $field,
])->first();
if (!$existing) {
ProductContent::create([
'product_id' => $productId,
'field' => $field,
'value' => $newValue,
'supplier_value' => $newValue,
]);
return;
}
// Завжди оновлюємо supplier_value для відображення розбіжності
$existing->supplier_value = $newValue;
// Перезаписуємо тільки якщо немає ручного оверрайду
if (!$existing->is_manual_override) {
$existing->value = $newValue;
}
$existing->updated_at = now();
$existing->save();
}
}
Розписання та пріоритети
| Тип даних | Частота | Причина |
|---|---|---|
| Характеристики (розміри, вага) | Раз на день | Мінливі рідко |
| Описи | Раз на день | Великий обсяг, не срочно |
| Статуси сертифікатів | Раз на тиждень | Змінюються ще рідше |
| Ціни | Кожні 15–30 хв | Висока волатильність |
Інтерфейс розбіжностей в админці
Якщо value != supplier_value AND is_manual_override = true, показувати в інтерфейсі товару попередження: «Постачальник змінив значення. Поточне: X, нове від постачальника: Y. Прийняти?» з кнопками «Прийняти» та «Залишити».
Тривалість реалізації
- Одне XML-джерело, оновлення опису та атрибутів без оверрайдів — 2–3 дні
- Нормалізатор атрибутів з таблицею маппінгу + механізм оверрайду — +2 дні
- Панель розбіжностей в админці — +1–2 дні







