Из-за временных ограничений на территории РФ наблюдаются проблемы с оплатой. Если платёж не проходит, оставьте запрос в службу поддержки.Служба поддержки работает 24/7 — мы всегда на связи по вопросам хостинга и серверов.Открыт прием заявок на аренду выделенных серверов и размещение оборудования в дата-центре.Напоминаем: рекомендуем включить резервное копирование для дополнительной защиты данных.Доступна новая линейка VPS/VDS с NVMe-дисками и увеличенной производительностью.Технические работы на части серверов завершены. Все сервисы работают в штатном режиме.
Статья9 мин чтенияПросмотры0

Дельта в API: как исключить повторное прибавление количества товара

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

Комментарии 0

Фиолетовый контейнер с посылками и жёлтая посылка, остановленная у приёмного барьера на складе
В этой статье

Если команда означает «прибавить 3», сетевой повтор должен прийти с тем же operation_id, а получатель — вернуть прежний результат, не меняя количество единиц товара второй раз. Если команда означает «установить 8», точный повтор сам по себе безопаснее, но устаревший снимок способен затереть более новую продажу. Поэтому дельту и абсолютное значение нельзя смешивать в одном неявном поле.

На маленьком примере начальное количество товара равно 5 единицам. Первая поставка добавляет 3 единицы. Без защиты два одинаковых запроса дают 11. С журналом операций первый запрос фиксирует 8, а второй получает результат duplicate и оставляет 8 единиц товара. Сеть иногда повторяет просьбу — она не упрямая, ей просто не пришёл ответ.

Запрос к API

Точный повтор

Отдельный риск

adjust +3

Без operation_id снова прибавит 3

Один документ поставки учтён дважды

set 8

Снова оставит 8

Старый снимок может затереть более новое изменение

Почему HTTP-метода недостаточно

В RFC 9110 идемпотентной названа операция, у которой несколько одинаковых запросов имеют тот же задуманный эффект, что и один. PUT относится к идемпотентным методам по определению, но бизнес-смысл поля всё равно важен: тело «delta: 3» не превращается в абсолютное количество единиц товара только из-за выбранного глагола.

RFC 5789 отдельно указывает, что PATCH не является идемпотентным сам по себе, хотя запрос можно спроектировать идемпотентным. Для изменения от известной версии спецификация рекомендует условный запрос; в HTTP для этого подходит, например, If-Match с сильным ETag.

Условия учебной проверки

Проверка выполнена 31 октября 2026 года в CPython 3.12. Модель работает только в памяти: не открывает сеть, не пишет файлы и не подключается к базе. Это проверка механизма, а не испытание конкретной редакции 1С-Битрикс или конфигурации 1С.

  • один SKU: SKU-BLUE-M; исходное количество товара — 5 единиц, версия 1;

  • adjust прибавляет дельту к количеству единиц товара, set задаёт абсолютное значение;

  • operation_id уникален для бизнес-операции, а не для сетевой попытки;

  • точный повтор возвращает сохранённый результат; тот же ID с другим телом отклоняется;

  • Получатель сравнивает expected_version запроса с текущей версией товара. Если версии различаются, он отклоняет устаревшую команду set и сохраняет текущее количество товара.

Код модели

from __future__ import annotations from dataclasses import dataclass @dataclass(frozen=True) class Update: operation_id: str sku: str mode: str value: int expected_version: int | None = None class Conflict(Exception): pass class Catalog: def __init__(self) -> None: self.stock = {"SKU-BLUE-M": 5} self.version = {"SKU-BLUE-M": 1} self.applied: dict[str, tuple[Update, tuple[str, int, int]]] = {} def naive_adjust(self, sku: str, delta: int) -> int: self.stock[sku] += delta return self.stock[sku] def apply(self, update: Update) -> tuple[str, int, int]: previous = self.applied.get(update.operation_id) if previous is not None: previous_update, previous_result = previous if previous_update != update: raise Conflict("operation_id_reused_with_different_payload") return ("duplicate", previous_result[1], previous_result[2]) if update.sku not in self.stock: raise KeyError("unknown_sku") current_version = self.version[update.sku] if update.expected_version is not None and update.expected_version != current_version: raise Conflict("version_conflict") if update.mode == "adjust": self.stock[update.sku] += update.value elif update.mode == "set": if update.value < 0: raise ValueError("negative_stock") self.stock[update.sku] = update.value else: raise ValueError("unknown_mode") self.version[update.sku] += 1 result = ("applied", self.stock[update.sku], self.version[update.sku]) self.applied[update.operation_id] = (update, result) return result def show(label: str, result: object) -> None: print(f"{label}: {result}") naive = Catalog() show("naive first +3", naive.naive_adjust("SKU-BLUE-M", 3)) show("naive retry +3", naive.naive_adjust("SKU-BLUE-M", 3)) assert naive.stock["SKU-BLUE-M"] == 11 safe = Catalog() delta = Update("receipt-42", "SKU-BLUE-M", "adjust", 3) show("safe first +3", safe.apply(delta)) show("safe retry +3", safe.apply(delta)) assert safe.stock["SKU-BLUE-M"] == 8 assert safe.version["SKU-BLUE-M"] == 2 try: safe.apply(Update("receipt-42", "SKU-BLUE-M", "adjust", 4)) except Conflict as error: show("same id, changed payload", error) else: raise AssertionError("changed payload must be rejected") # A later sale changes the authoritative value after the receipt was applied. safe.stock["SKU-BLUE-M"] -= 1 safe.version["SKU-BLUE-M"] += 1 show("after later sale", (safe.stock["SKU-BLUE-M"], safe.version["SKU-BLUE-M"])) try: safe.apply(Update("snapshot-43", "SKU-BLUE-M", "set", 6, expected_version=2)) except Conflict as error: show("stale absolute value", error) else: raise AssertionError("stale absolute value must be rejected") snapshot = Update("snapshot-44", "SKU-BLUE-M", "set", 6, expected_version=3) show("current absolute value", safe.apply(snapshot)) show("current absolute retry", safe.apply(snapshot)) assert safe.stock["SKU-BLUE-M"] == 6 assert safe.version["SKU-BLUE-M"] == 4

Результат запуска

naive first +3: 8 naive retry +3: 11 safe first +3: ('applied', 8, 2) safe retry +3: ('duplicate', 8, 2) same id, changed payload: operation_id_reused_with_different_payload after later sale: (7, 3) stale absolute value: version_conflict current absolute value: ('applied', 6, 4) current absolute retry: ('duplicate', 6, 4)

Проверка

Ожидалось

Получено

Наивный +3 дважды

Показать ошибку повтора

11 вместо 8

Тот же operation_id и тело

Не менять количество единиц товара

duplicate, 8, версия 2

Тот же ID, другая дельта

Отклонить конфликт

operation_id_reused_with_different_payload

Устаревший set 6

Не затереть продажу

version_conflict, осталось 7

Актуальный set 6 и повтор

Применить один раз

6, версия 4; затем duplicate

Все утверждения завершились успешно, процесс вернул код 0. Значимый крайний случай здесь не декоративный: после поставки модель списывает одну единицу продажей, поднимает версию с 2 до 3 и только затем получает старый снимок. Без проверки версии такой set мог бы молча переписать более новое состояние.

Как оформить контракт интеграции

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

  • Передавайте стабильный идентификатор товара или торгового предложения. SKU-BLUE-M в модели — учебная строка; в проекте нужно проверить фактическое поле, вариант товара и правила сопоставления.

  • Разделите команды явно: adjust с дельтой и set с абсолютным значением. Поле quantity без режима заставляет получателя угадывать, а угадывание плохо масштабируется вместе с ассортиментом.

  • Генерируйте operation_id на уровне документа поставки, корректировки или другого бизнес-события. Повторная доставка того же события сохраняет ID; новая операция получает новый.

  • Храните вместе с ID отпечаток значимых полей и результат. Совпавший ID с другим телом — конфликт. Применять новую дельту в этом случае нельзя.

  • Изменение количества товара и запись operation_id должны быть атомарными. Иначе сбой между двумя записями оставит окно: количество товара уже изменено, а доказательства обработки ещё нет.

  • Для set передавайте ожидаемую версию или используйте условие If-Match, если API поддерживает ETag. После конфликта нужно перечитать состояние и принять решение, а не повторять старый снимок вслепую.

До выбора режима полезно отдельно определить источник окончательного остатка. Этот вопрос разобран в статье «Остаток в 1С и на сайте не совпадает: где должна жить окончательная цифра». Здесь мы решаем более узкую задачу: как не применить одну уже выбранную команду дважды.

Тут есть подвох: срок хранения ключей

Если удалить operation_id раньше, чем исчезнут повторы из очереди, старый запрос снова станет новым. Универсального срока нет: он должен быть длиннее максимального возраста сообщения, окна повторов клиента и ручного возврата из очереди. Срок, очистку и объём журнала стоит включить в контракт интеграции, а не оставлять настройкой «потом разберёмся».

Журнал операций также не заменяет проверку прав. Технический пользователь должен иметь доступ только к нужному каталогу, складу и операции. Названия сущностей, доступность API и модель прав зависят от редакции и версии 1С-Битрикс, модуля магазина и конкретного способа обмена — их нужно сверить по проекту до реализации.

Критерии приёмки

  1. Первый adjust +3 переводит остаток 5 → 8 и сохраняет operation_id с результатом.

  2. Точный сетевой повтор возвращает тот же бизнес-результат и не создаёт второе движение.

  3. Повтор с тем же ID, но другой дельтой отклоняется как конфликт и попадает в наблюдаемую диагностику.

  4. После нового движения устаревший set не перезаписывает остаток; клиент получает конфликт версии.

  5. Неизвестный SKU, недостаточные права и отказ хранилища дают явную ошибку, а не частично применённую операцию.

  6. Метрики различают applied, duplicate и conflict, чтобы безопасные повторы не маскировали реальные расхождения.

Для приёмки конкретной интеграции повторите эти сценарии на изолированном стенде с фактической редакцией 1С-Битрикс, модулем, направлением обмена, складами, торговыми предложениями и правами технического пользователя. Учебная модель доказывает поведение алгоритма, но не притворяется внедрением у клиента — у неё, к счастью, нет отдела закупок.

Обсуждение 0

Делись опытом и задавай вопросы. Комментарии без ссылок появляются после проверки редактором.

Пока никто не написал. Начни обсуждение.