Что происходит с обменом сайта и 1С после запуска, когда в него попадают реальные заказы, цены и остатки? Разбор шести проблем на живом проекте — для разработчиков, интеграторов и владельцев e-commerce-проектов.
Обмен с 1С обычно принимают в тот день, когда на витрине впервые появились товары. Каталог приехал, картинки на месте, тестовый заказ упал в учёт, всё работает, подписываем акт. Проблемы начинаются через месяц, и выглядят они не как падение. У части позиций вместо цены ноль. Менеджер не находит заказ по номеру, который видит в 1С. В учёте с утра лежат две копии ночных заказов, и никто не знает, какая настоящая.
Дальше разбор по живому проекту: b2b-портал оптового поставщика отделочных материалов, около полутора тысяч позиций, три бренда, тридцать человек в офисе. Каталог, цены, остатки и заказы ходят между сайтом и 1С по расписанию. Портал писали мы целиком, обмен тоже, так что грабли ниже собственные, а не пересказанные с чужих конференций.
Речь не про то, как поднять обмен. Про то, что с ним происходит потом, когда через него пошли настоящие данные и настоящие люди.
Больше не нужно искать и обзванивать каждое диджитал-агентство
Создайте конкурс на workspace.ru – получите предложения от участников CMS Magazine по цене и срокам. Это бесплатно и займет 5 минут. В каталоге 15 617 диджитал-агентств, готовых вам помочь – выберите и сэкономьте до 30%.
Создать конкурс →
Штатный протокол обмена 1С с сайтом открытый, его придумали «1С» и «1С-Битрикс», и он занимает полторы страницы. Инициатор всегда 1С, сайт только отвечает.
Выгрузка каталога идёт четырьмя шагами. Сначала 1c_exchange.php?type=catalog&mode=checkauth, в ответ три строки: success, имя cookie, значение cookie. Дальше mode=init, и сайт сообщает два своих условия: zip=yes или zip=no и file_limit=<число байт>, то есть максимальный размер куска, который он готов принять за один запрос. Потом mode=file&filename=... с содержимым в POST, и наконец mode=import&filename=..., на который сайт отвечает либо progress (повтори тот же запрос ещё раз), либо success. На любой ошибке первая строка ответа failure, ниже текст.
Заказы ездят обратно почти так же: type=sale&mode=query отдаёт заказы сайта в 1С, после успешной записи 1С стучит mode=success, а изменения заказов уезжают на сайт через mode=file.
Сеанс целиком:
1С сайт
| type=catalog&mode=checkauth ------> |
| <------ success / имя cookie / значение cookie
| type=catalog&mode=init ------> |
| <------ zip=yes|no / file_limit=<байт>
| type=catalog&mode=file ------> | POST, кусками по file_limit
| <------ success |
| type=catalog&mode=import ------> |
| <------ progress (повторить тот же запрос)
| type=catalog&mode=import ------> |
| <------ success |
| |
| type=sale&mode=query ------> | заказы с сайта в 1С
| type=sale&mode=success ------> |
| type=sale&mode=file ------> | статусы заказов обратно на сайт
Инициатор всегда слева. Со стороны сайта не начинается ни один шаг: если обмен встал, кнопки «запустить» у вас нет, есть расписание в 1С и человек, который до неё дойдёт.
Вот и весь протокол. Три слова в ответе, четыре режима, XML по стандарту CommerceML 2. Этой части посвящена половина статей про интеграцию с 1С, и именно она почти никогда не ломается.
Ломается всё, что протокол не описывает: что значит пришедшее значение, сколько раз можно повторить сеанс, какое поле чьё. Дальше шесть таких мест, все с настоящими симптомами.
Пустое значение это не значениеСимптом: на карточках товара вместо цены ноль. Причём вчера цена была.
Механизм. В 1С у части номенклатуры базовая цена просто не заполнена: позиция заводится под заказ, цену ставит менеджер руками при согласовании. В выгрузку такая позиция всё равно попадает, и приезжает она с нулём. На стороне сайта стоит обычный на вид код:
$item->price = (float) $offer['ЦенаЗаЕдиницу']; $item->save();
Он честно записывает то, что приехало. Ноль перетирает цену, которую туда положили руками или предыдущей выгрузкой, и витрина показывает товар по нулю.
Неочевидное здесь вот что. Обмен молча смешивает три разных состояния, которые для бизнеса означают противоположные вещи:
поля не было в файле вообще (1С про него ничего не сказала),
поле пришло пустым (в 1С оно не заполнено),
поле пришло нулём, и ноль это настоящая цена (акция, подарок, сопутствующий товар за рубль).
Пока код читает (float), все три превращаются в 0.0. Починка не в том, чтобы дописать if ($price > 0), хотя мы именно так и сделали в первый час: это заплатка, которая на следующей неделе выстрелит на позиции, которую действительно отдают бесплатно.
Правильный вопрос звучит иначе: кто хозяин каждого поля. Цена закупки, розничная цена, остаток, название, картинка, описание могут приходить из разных мест, и часть полей редактирует менеджер на сайте. Как только это записано (хоть таблицей в вики, хоть константой в коде), поведение при пустом значении становится очевидным: у поля, чей источник 1С, пустое значение означает «очистить», у поля, чей источник сайт, оно означает «не трогать».
И отдельно про проверку. После правки мы не написали «готово», а посчитали результат на боевом: цена появилась у семи карточек из восьми, а у восьмой её нет и в самой 1С. Восьмая это не недоделка, это ровно тот случай, ради которого правило и пишется. Без такого счёта вы не знаете, починили вы что-нибудь или просто перестали видеть симптом.
Симптом: менеджер берёт номер заказа из 1С, вбивает его в поиск на портале и не находит ничего. Заказ при этом есть и открывается по прямой ссылке.
Номер хранился с пробелом на конце. Пробел приехал вместе с данными и лёг в базу как есть, а поиск ищет по точному совпадению.
Такие вещи живут в бэклоге месяцами, потому что не горят: система работает, данные целы, всего-то пробел. А на деле это отказ поиска для человека, который ищет заказы двадцать раз в день.
Из 1С регулярно приезжают концевые пробелы, неразрывный пробел вместо обычного, разный регистр в артикулах, двойные пробелы в названиях. Это не аномалия: поля заполняют люди в интерфейсе, у которого нет валидации.
Лечится нормализацией на границе, и только там. Один слой на входе обмена, который для каждого строкового поля решает, что с ним делать:
function normalizeCode(string $raw): string {
$s = str_replace("\xC2\xA0", ' ', $raw); // NBSP из 1С
$s = preg_replace('/\s+/u', ' ', $s);
return mb_strtoupper(trim($s));
}
Важно не размазать это по коду. Соблазн большой: поправить поиск, чтобы он делал TRIM на лету. Через полгода TRIM будет в семи местах, в восьмом его забудут, и ошибка вернётся уже как «поиск иногда не находит». Нормализованное значение хранится в отдельной колонке, оригинал остаётся рядом нетронутым: он нужен, когда придёт время выгружать данные обратно в 1С, где ждут ровно ту строку, которую оттуда и прислали.
Симптом: за ночь в учёт попала вторая копия заказов.
Причина у нас оказалась скучной: крон запускал выгрузку дважды. Но дело не в кроне. Дело в том, что двойной запуск вообще не должен приводить к дублям, и протокол прямым текстом это подразумевает: ответ progress означает «пришли этот же запрос ещё раз», и таких повторов на большом каталоге десятки. Сеанс обмена в принципе не бывает одноразовым.
Список поводов для повтора длиннее, чем кажется: 1С не получила ответ из-за таймаута и переспросила, сеанс не уложился в окно и наложился на следующий, админ нажал «обменяться сейчас» поверх расписания, файл поехал кусками по file_limit и часть кусков доехала дважды. Ни один из них не поломка.
Значит, идемпотентность нужна не как приятное свойство, а как условие работоспособности. У нас это два простых правила.
Первое: внешний идентификатор из 1С (Ид, тот самый GUID) участвует в уникальном ключе, и запись идёт через upsert, а не через insert:
insert into orders (guid_1c, number, total)
values (:guid, :number, :total)
on conflict (guid_1c) do update
set number = excluded.number,
total = excluded.total;
Второе: на время сеанса берётся блокировка, и второй запуск не встаёт в очередь, а сразу уходит. В PostgreSQL это pg_try_advisory_lock, и он лучше файла-флага ровно тем, что снимается сам при падении процесса. Флаг в файле после падения остаётся лежать, и обмен встаёт до утра, а вы об этом узнаёте от клиента.
Проверяли по факту: в ночь на восьмое сентября выгрузка стартовала один раз. Не «должна стартовать», а стартовала, по журналу запусков.
Ключ это GUID, и он не одинНа витрине три бренда: основной и два отдельных, со своими каталогами и своими ценами. Обмен у каждого свой.
Обычная ошибка при таком раскладе: искать номенклатуру по артикулу или наименованию. Артикул кажется естественным ключом: короткий, человекочитаемый, и почти всегда работает. А потом у двух брендов совпадают артикулы, и один товар начинает перетирать другой вместе с ценой и остатком.
Настоящий ключ в обмене один: Ид из CommerceML, то есть GUID объекта в конкретной базе 1С. Ключевое слово «в конкретной»: у второго бренда своя база, и GUID той же по сути позиции там другой. Как только баз становится больше одной, нужна таблица соответствий (внешняя система, внешний ключ, наш объект), и именно её мы дописывали, подключая второй бренд.
Заодно это ответ на вопрос со стадии оценки: «почему второй бренд стоит почти как первый, обмен-то тот же». С первым брендом ключ один и его можно не осознавать. Со вторым появляется сущность «внешняя система», и её надо протащить через весь обмен: цены, остатки, заказы, поиск дублей.
Статусы заказа приходят позже, чем ждёт менеджерЭто место я считаю самым неочевидным, потому что тут ломается не код, а ожидания, и починить его правкой нельзя.
В протоколе прямым текстом сказано: состояния заказа по оплате и по отгрузке выгружаются на сайт только при полном выполнении операции. Полная оплата, полная отгрузка. До этого момента заказ на сайте считается неоплаченным и неотгруженным. Там же оговорка: заказ, по которому уже прошла оплата или отгрузка, при попытке изменить его в 1С на сайт как изменённый не загрузится.
Теперь представьте оптовый заказ на сорок позиций. Половину отгрузили сегодня, остальное на следующей неделе. В 1С картина честная, в кабинете клиента по-прежнему «в работе», и всё это время он звонит менеджеру. А дизайнер к этому моменту уже нарисовал прогресс отгрузки, потому что заказчик попросил. Нарисовать можно, данных под это нет.
Выходов ровно два, и оба надо обсуждать до вёрстки кабинета, а не после. Либо интерфейс честно показывает то, что даёт протокол: два состояния, без промежуточных. Либо заводится второй канал, отдельная выгрузка по расписанию с реализациями по заказу, и это уже доработка на стороне 1С, то есть чужие часы, чужой подрядчик и чужие сроки.
Общий принцип: прежде чем обещать в интерфейсе поле, проверьте, что оно есть в источнике и приезжает тогда, когда нужно. В смете обмена эта проверка стоит час, а после сдачи макетов она стоит переделку кабинета.
Обмен кончается не в базе, а на экранеПоследний случай красивый именно тем, что обмен в нём отработал идеально.
Данные второго бренда приехали, импорт закончился успехом, в базе всё на месте. Витрина при этом не собралась: сборка страницы отменилась из-за занятой очереди, и на главной просто не появилось восемь направлений каталога. С точки зрения всех логов обмена ничего не произошло, success на месте.
Если между 1С и глазами покупателя стоит кэш, статическая сборка, поисковый индекс или CDN, то «импорт завершён» не значит «человек видит товар». Каждый такой слой это ещё одно место, где данные могут остановиться, и на любом из них нет ошибки: кэш не сломался, он просто старый.
Поэтому финальную проверку мы делаем не по логу импорта, а с той стороны, с которой на портал смотрит клиент. Запрос к витрине, счёт того, что на ней видно, сравнение с тем, что в базе. Восемь направлений на месте, 279 карточек из 383 с розничной ценой, семь из восьми с восстановленной ценой. Числа скучные, но именно они отличают «обмен работает» от «обмен вроде работает».
Чем принимать обменЕсли свести всё это в чеклист приёмки, выйдет пять пунктов, и ни один не звучит как «данные приехали».
Для каждого поля записано, чьё оно: 1С или сайта. Пустое значение из 1С не перетирает поле, которым владеет сайт.
Все строковые поля нормализуются на входе, в одном месте, оригинал сохраняется рядом.
Обмен идемпотентен, повторный запуск проверен руками: запустили дважды, дублей нет.
Ключ сопоставления это GUID, для каждой внешней базы свой, через таблицу соответствий.
Проверка идёт со стороны витрины и в числах, а не по строке success в логе.
Плюс шестой, не технический: у обмена должен быть человек, который смотрит на эти числа каждый день. Обмен с 1С не относится к системам, которые настроил и забыл. Он ломается тихо, без падений и алертов, и обнаруживается обычно менеджером, который час ищет заказ по номеру с пробелом.