Перейти к содержимому

Товары на торговой площадке - фид, остатки, заказы обратно

Выводим каталог на торговую площадку так, чтобы цены и наличие там совпадали с сайтом, а заказы возвращались в те же процессы, что и обычные.

Механика

Площадка забирает каталог не так, как поисковик. Ей нужен строгий набор полей: идентификатор, название, цена, адрес, картинка, признак наличия, категория по её собственному дереву. Каталог, собранный под сайт, этим требованиям почти никогда не соответствует сразу.

Данные идут двумя разными потоками. Полное описание товаров меняется редко и уезжает фидом раз в сутки; цена и наличие меняются постоянно и требуют быстрого канала - отдельного файла или вызовов API. Смешивать их в один поток означает либо устаревшие остатки, либо ежечасную сборку всего каталога.

Обратный поток - заказы. Покупатель оформляет их на площадке, а магазину они приходят вызовом или выгрузкой, и дальше должны стать обычными заказами сайта: со своим номером, статусами, отгрузкой и учётом остатков.

Расхождения стоят денег. Цена в фиде выше реальной раздражает покупателя, ниже - обязывает продать по ней; товар, которого нет, приводит к отмене и штрафу. Поэтому сверка того, что отдали, с тем, что площадка показывает, - не украшение, а часть работы.

Наконец, категории площадки живут своей жизнью. Их дерево не совпадает с каталогом магазина, сопоставление делается руками один раз и потом поддерживается: новая категория на сайте не появляется у площадки сама.

Стоит заранее решить, кто в магазине отвечает за площадку. Технически это выгрузка, но по сути - второй канал продаж со своими ценами, своими остатками и своей ответственностью за отмены. Задача, оставленная разработчику целиком, заканчивается тем, что решения про цены принимает тот, кто их не считает.

Шаги

  1. Прочитать требования площадки к полям и категориям до всякой разработки.
  2. Дополнить каталог недостающими полями и сопоставить категории.
  3. Собирать фид по расписанию в файл, а не отдавать сборку по запросу.
  4. Завести быстрый поток остатков и цен с частотой, которую площадка допускает.
  5. Принимать заказы площадки и превращать их в обычные заказы сайта.
  6. Сверять ежедневно: что отдали, что принято и что показывает площадка.

Код

Проверяем каталог на обязательные поля:

$missing = [];
foreach ($items as $item) {
foreach (['NAME', 'PRICE', 'PICTURE', 'CATEGORY'] as $field) {
if (empty($item[$field])) { $missing[$field][] = $item['ID']; }
}
}
printf("без картинки: %d, без категории: %d\n", count($missing['PICTURE'] ?? []),
count($missing['CATEGORY'] ?? []));

Площадка требует полей, которых в каталоге может не быть. Список товаров без картинки или без сопоставленной категории собирают до разработки: он и определяет объём подготовительной работы.

Собираем фид в файл по расписанию:

// задание по расписанию: собираем во временный файл и подменяем готовый
$tmp = $_SERVER['DOCUMENT_ROOT'] . '/upload/feed/market.tmp.xml';
$out = $_SERVER['DOCUMENT_ROOT'] . '/upload/feed/market.xml';
$fh = fopen($tmp, 'w');
foreach ($this->iterateItems() as $item) { fwrite($fh, $this->renderItem($item)); }
fclose($fh);
rename($tmp, $out); // подмена готового файла одним движением

Фид собирают по расписанию, а не на каждый запрос. Подмена готового файла переименованием важна не меньше: площадка не должна скачать наполовину собранный фид.

Отдаём остатки и цены отдельным потоком:

$rows = \Bitrix\Catalog\ProductTable::getList([
'select' => ['ID', 'QUANTITY', 'QUANTITY_RESERVED'],
'filter' => ['>=TIMESTAMP_X' => $lastRun], // только изменившееся
])->fetchAll();
foreach ($rows as $r) {
$stocks[] = ['id' => $r['ID'], 'count' => max(0, $r['QUANTITY'] - $r['QUANTITY_RESERVED'])];
}

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

Превращаем заказ площадки в заказ сайта:

$order = \Bitrix\Sale\Order::create(SITE_ID, $userId);
$basket = \Bitrix\Sale\Basket::create(SITE_ID);
foreach ($external['items'] as $line) {
$item = $basket->createItem('catalog', $line['product_id']);
$item->setFields(['QUANTITY' => $line['count'], 'CURRENCY' => 'RUB',
'PRICE' => $line['price'], 'CUSTOM_PRICE' => 'Y']); // цену диктует площадка
}
$order->setBasket($basket);
$order->setField('XML_ID', 'mp-' . $external['id']); // защита от дублей
$order->save();

Заказ с площадки становится обычным заказом сайта. Внешний номер в поле связи - обязательная часть: повторная выгрузка того же заказа не должна создавать второй.

Сверяем отданное с показанным:

$sent = $this->countItems();
$accepted = (int)$report['accepted']; // из отчёта кабинета площадки
printf("отдано %d, принято %d, отклонено %d\n", $sent, $accepted, $sent - $accepted);
// расхождение больше процента - повод читать отчёт об ошибках построчно

Сверка отвечает на вопрос, который фид задать не может. Площадка отклоняет часть позиций молча, и узнать об этом можно только из её отчёта, а не из своего кода. Отчёт стоит забирать тем же заданием, что и отправляет выгрузку: тогда число отклонённых позиций попадает в журнал сразу, а не всплывает через месяц вопросом «почему этих товаров там нет».

Сопоставляем категории один раз:

// своя таблица соответствия: раздел каталога - категория площадки
$map = CategoryMapTable::getList(['select' => ['SECTION_ID', 'MARKET_CATEGORY']])->fetchAll();
$byId = array_column($map, 'MARKET_CATEGORY', 'SECTION_ID');
if (!isset($byId[$sectionId])) {
AddMessage2Log('нет категории площадки для раздела ' . $sectionId, 'vendor.shop');
}

Дерево площадки не совпадает с каталогом магазина. Соответствие держат отдельной таблицей и жалуются в журнал на каждый несопоставленный раздел: молчание здесь означает товары, которые просто не попадут в выдачу площадки.

Ограничения

Требования площадок меняются без согласования с вами. Новое обязательное поле, изменённое дерево категорий, другая частота обновлений - всё это приходит письмом и требует работы в оговорённый срок, иначе выгрузка останавливается.

Скорость обновления ограничена самой площадкой. Даже если магазин готов отдавать остатки каждую минуту, принимать их будут с той частотой, которую она допускает, и обещать покупателю мгновенную точность нельзя.

Ответственность за цену и наличие остаётся на магазине. Ошибка в фиде - это не техническая мелочь, а обязательство продать по указанной цене или отмена заказа со штрафом, поэтому проверки перед отправкой стоят дороже красивого кода.

Первый месяц работы площадки почти всегда уходит на притирку. Отклонённые позиции, неверные категории, спорные отмены - это не ошибки разработки, а обычный ход дел, и закладывать на него время полезнее, чем обещать магазину запуск за один вечер.

Продажи на площадке и на сайте делят один склад. Пока остатки не разведены, один и тот же товар продаётся дважды, и разбирается это уже с покупателем, а не в коде.

Типичные проблемы

Площадка приняла половину товаров.

У части позиций нет обязательных полей или сопоставленной категории. Список таких товаров собирают до самой первой выгрузки на площадку.

На площадке остатки вчерашние.

Наличие уезжает вместе с полным фидом раз в сутки. Остатки и цены отдают отдельным и заметно более быстрым потоком.

Один заказ площадки создан на сайте дважды.

При приёме не проверяется внешний номер заказа. Его пишут в поле связи заказа и ищут там перед созданием нового.

Площадка скачала повреждённый фид.

Файл отдавался прямо во время сборки. Готовый фид подменяют переименованием, а не пишут поверх.

Цена на площадке ниже, чем на сайте.

Фид собран до пересчёта скидок или по другому типу цены. Цену для площадки считают тем же кодом, что и на витрине.

Частые вопросы

Фид или вызовы API площадки?

Фидом отдают описание каталога, вызовами - остатки, цены и заказы. Большинство площадок требует и то, и другое.

Как часто пересобирать фид?

Раз в сутки ночью для описаний и раз в час-два для остатков. Точные пределы задаёт сама площадка.

Что делать с заказами площадки в отчётах?

Помечать их источником и считать отдельно. Смешанные с сайтовыми, они портят и конверсию, и средний чек.

Кто отвечает за отклонённые позиции?

Магазин: площадка только сообщает причину в отчёте. Отчёт стоит читать после каждой выгрузки.

Нужен ли отдельный склад для площадки?

Если продажи заметные - да, иначе один товар продаётся дважды. Остатки для площадки разводят складами.

Смежное

Первоисточник