HTTP-сервис 1С: принять заказ ровно один раз
Курьер нажал «Оплатить», сайт отправил заказ, но не дождался ответа 1С и повторил запрос. Если обработчик сразу создаёт документ, в базе появятся два заказа.
Фраза «доставить ровно один раз» звучит как настройка транспорта, но в распределённой системе её обеспечивает прикладной код. Сеть умеет потерять ответ после успешной записи, балансировщик — повторить запрос, а человек — нажать кнопку ещё раз. Поэтому входящий сервис должен распознавать уже обработанное событие и возвращать прежний результат.
Что делает обработчик, а чего не делает
HTTP-обработчик — пограничный слой. Он читает заголовки и тело, проверяет размер и тип содержимого, превращает JSON в нейтральную структуру и вызывает прикладной метод. Поиск контрагента, создание документа и расчёт цен не должны жить внутри маршрута: эти операции понадобятся при ручном повторе и обработке очереди.
Для нашего магазина определим POST /integration/v1/orders/paid. Отправитель обязан передать X-Correlation-ID; если его нет, сервис создаёт новый для диагностики. Ответ всегда содержит этот идентификатор.
Регистр принятых событий
Создадим информационный регистр с уникальным измерением EventID. В ресурсах храним статус, ссылку на созданный объект, хеш исходного тела и краткую ошибку. Хеш защищает от редкого, но опасного случая: партнёр повторно использовал тот же идентификатор с другим содержимым.
- Проверить синтаксис и обязательные поля без транзакции.
- Начать короткую транзакцию и заблокировать запись по
EventID. - Если событие завершено и хеш совпадает — вернуть сохранённый ответ.
- Если идентификатор тот же, а хеш другой — ответить 409 Conflict.
- Создать заказ и зафиксировать результат одной транзакцией.
Функция ОбработатьОплату(Событие, ХешТела) Экспорт
НачатьТранзакцию();
Попытка
Блокировка = Новый БлокировкаДанных;
Элемент = Блокировка.Добавить("РегистрСведений.ПринятыеСобытия");
Элемент.УстановитьЗначение("EventID", Событие.event_id);
Блокировка.Заблокировать();
Прежний = НайтиПринятоеСобытие(Событие.event_id);
Если Прежний <> Неопределено Тогда
Если Прежний.ХешТела <> ХешТела Тогда
ВызватьИсключение "EVENT_ID_REUSED";
КонецЕсли;
ЗафиксироватьТранзакцию();
Возврат Прежний.Результат;
КонецЕсли;
Заказ = СоздатьЗаказПоСобытию(Событие);
СохранитьРезультатСобытия(Событие.event_id, ХешТела, Заказ.Ссылка);
ЗафиксироватьТранзакцию();
Возврат Заказ.Ссылка;
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
КонецФункции
Коды ответа — обещание отправителю
| 201 | заказ создан впервые | повторять не нужно |
| 200 | повтор события, возвращён прежний результат | считать доставленным |
| 400 | JSON не разобран | исправить формат |
| 409 | event_id повторно использован с другим телом | разобрать конфликт |
| 422 | формат верен, бизнес-данные неприемлемы | исправить данные |
| 503 | временная зависимость недоступна | повторить позже тем же ключом |
Не возвращайте 200 вместе с текстом ошибки. Многие клиенты смотрят только на класс статуса и навсегда пометят событие доставленным. По той же причине не стоит отвечать 500 на ожидаемую ошибку в данных: отправитель будет бессмысленно повторять её.
Быстрый ответ или синхронная запись?
Если создание заказа занимает секунды и не зависит от медленных сервисов, синхронный сценарий понятен. Для тяжёлой обработки лучше сохранить сообщение во входящую очередь, ответить 202 Accepted и вернуть URL статуса. Но 202 означает только приём на обработку, а не успешный заказ — это должно быть явно записано в контракте.
Аварийный тест: после записи заказа искусственно оборвите соединение до отправки HTTP-ответа. Повтор тем же
event_idобязан вернуть ссылку на первый документ, а не создать новый.