Сверка вместо «удалить всё и вставить заново»: идемпотентный мост 1С ↔ legacy для платежей

Веб Штурм
  • интеграция
  • HMAC
  • идемпотентность
  • PHP
  • MariaDB
  • архитектура
  • case-study

Прямой обмен между и легаси веб-системой — частый источник тихих потерь данных. Классическая схема «выгрузка за дату» работает так: для выбранной даты в учётной таблице сайта удаляются все строки, затем заливаются заново из . Пока система одна и оператор один — терпимо. Как только в одной таблице оказываются платежи нескольких юрлиц и несколько касс — начинаются коллизии, задвоения и ручные обходы.

Мы заменили этот обмен на транзакционную сверку (reconcile) по HTTP+HMAC. Ниже — почему старая схема опасна, как устроена новая и как мы доказали безопасность на боевой записи.

Почему «удалить всё и вставить заново» — мина

Старый обмен шёл через ODBC напрямую в БД сайта и удалял строки по грубому ключу — по сути «первые 16 символов типа документа + номер + год». Отсюда три беды:

  • Грубый ключ. Сверка по году, а не по дню; без точного номера счёта и типа кассы → коллизии разных платежей, которые потом правили руками.
  • Накопление. Схема не удаляла «лишнее» аккуратно — повторные прогоны, другие кассы и другие юрлица накапливались в общей таблице вперемешку.
  • Общая партиция на несколько источников. Несколько баз писали в одну таблицу под пересекающимися именами. Любое «удалить всё за дату» рисковало снести данные соседа.

«Ошибки, которые терпели» — это не фигура речи, а реальные строки, которые бухгалтерия исправляла вручную.

Идея: не «переписать», а «свести»

Новый обмен — это не прямой доступ к БД, а HTTP-эндпоинты на самом легаси-сервере (переиспользуем боевой код подключения к БД, подписи и пересчёта итогов). отправляет набор платежей за дату, сервер сводит его с тем, что уже есть в партиции (база, дата), по точному бизнес-ключу (номер счёта + номер документа + тип кассы + день) и применяет минимальные изменения:

  • совпало по ключу и значениям → noop (ничего);
  • изменилось → replace (старую версию помечаем удалённой, пишем новую);
  • нет в наборе soft-delete;
  • новый документ → insert.

Наши вставки помечаются тегом провенанса (видно, что строка пришла через мост), удаление — append-only: ничего не затирается физически, остаётся аудит-след. Подпись — HMAC-SHA256 от тела запроса плюс отметка времени против повторного воспроизведения.

Результат — идемпотентность: повторный прогон без изменений в 1С не меняет в БД ничего. Это убирает задвоения как класс.

Три предохранителя сверки

Удаление — самая опасная операция, поэтому вокруг неё три защиты.

1. dry-run. Сервер умеет вернуть план (вставлю N, заменю M, удалю K) без записи. Любой первый прогон на новой дате смотрим в dry-run.

2. Гард пустого набора. Если источник прислал ноль платежей, мы НЕ удаляем партицию целиком — это почти всегда сбой выгрузки, а не «реально 0 оплат». Массовое удаление при пустом вводе блокируется (снять можно только явным флагом). Этот гард уже сработал в бою: пустой прогон вместо «удалить 101 строку» удалил ноль.

3. Скоуп по серии документа — ключевое для общей таблицы. У каждой базы/организации свой префикс номера документа (своя «серия»). Сверка удаляет «отсутствующие» строки только в тех сериях, которые реально прислал источник. Прогон одной организации физически не может тронуть строки другой в той же партиции.

Логика сверки платежей: совпало по ключу — noop, значения изменились — replace, нового нет — insert; отсутствующую в наборе строку удаляем только если её серия документа прислана в прогоне, иначе protected — данные соседних источников не трогаются

Как доказали, что не теряем данные

Тестировали не «на словах», а прогоном на копии боевой базы с проверкой каждой строки до и после.

  • Идемпотентность: прогон без изменений → noop по всем строкам, ноль записи.
  • Изменение: правим два документа → replace (старые версии → в архив, новые → с тегом моста).
  • Создание: добавляем документ → insert.
  • Защита соседей: в каждом прогоне строки чужих серий помечены protected — ни одна не ушла в удаление.
  • Краевой случай: документ, ошибочно созданный «не той датой», в выгрузку за нужную дату не попал (дата-скоуп корректен); после исправления даты — попал и добавился. Никаких фантомных строк.

Итог: партиция до и после боевой записи совпадает по всем «чужим» строкам, наши изменения помечены тегом, аудит-след полон, потерь — ноль.

Выводы

  • Сверка по точному ключу вместо delete-all+insert убирает коллизии и задвоения и делает обмен идемпотентным.
  • Append-only + тег провенанса дают честный аудит: видно, что и когда пришло из 1С.
  • Скоуп удаления по серии безопасно решает мультиарендность, когда несколько источников делят одну таблицу — без миграции схемы.
  • dry-run и гард пустого набора — дешёвые предохранители, которые ловят дорогие ошибки до записи.

Если у вас обмен 1С с веб-системой всё ещё работает по принципу «снести и залить заново» — это хороший кандидат на замену сверкой. Как мы делали это для оператора речных круизов →

Часто задаваемые вопросы

Чем сверка (reconcile) лучше «удалить за дату и вставить заново»?
«Удалить всё и вставить» каждый раз переписывает строки и при грубом ключе (например, по году) даёт коллизии и задвоения, которые правят вручную. Сверка считает минимальную разницу: совпало — не трогаем (noop), изменилось — заменяем, нового нет — вставляем. Повторный прогон без изменений не меняет ничего (идемпотентность), а аудит-след сохраняется.
Зачем HMAC, если можно просто POST на эндпоинт?
HMAC-SHA256 от тела запроса — это аутентификация без хранения пароля на стороне 1С в открытом виде и без mTLS-инфраструктуры. Сервер пересчитывает подпись от полученного тела и сверяет: подделать запрос или повторить чужой нельзя без секрета. Плюс отметка времени против replay.
Как не удалить данные другого юрлица, если все пишут в одну таблицу?
Мы привязали удаление к «серии» номера документа. У каждой базы/организации своя серия нумератора. Прогон сверки удаляет «отсутствующие» строки только в тех сериях, которые реально прислал источник. Чужие серии в той же партиции остаются нетронутыми — проверено на боевой записи.
Что такое dry-run в этом контексте?
Режим, в котором сервер считает и возвращает план изменений (сколько вставит, заменит, удалит), но ничего не пишет. Это позволяет посмотреть результат до фактической записи и поймать ошибку охвата — особенно важно для операций удаления.