Перейти к содержанию

Адаптеры и мультипровайдерность

[ОП] Требование: система должна одновременно работать с несколькими поставщиками по трём независимым осям. [ТЗ] Дополнительный мотив: тиражирование на ≥ 6 УЗ Бреста и района, где состав вендоров отличается.

Три оси и почему они независимы

flowchart TB
  subgraph CORE["Ядро 3H — о вендорах не знает"]
    CANON["Каноническая модель<br/>на базе HL7 FHIR R4"]
  end

  subgraph A1["Ось 1 · Координаты (pull)"]
    G1["GPS-сервер вендора СМП"]
    G2["Сервер маршрутизации<br/>стороннего ПО"]
    G3["…"]
  end

  subgraph A2["Ось 2 · API СМП (push)"]
    S1["ИС «МРМ бригады СМП»<br/><b>первый интегрируемый</b>"]
    S2["Эрикполь"]
    S3["МАП"]
    S4["Лекарь"]
  end

  subgraph A3["Ось 3 · МИС"]
    M1["АИС «Медик»<br/><b>первая интегрируемая</b>"]
    M2["…"]
  end

  G1 & G2 & G3 --> GACL["GeoAdapter<br/>интерфейс"]
  S1 & S2 & S3 & S4 --> SACL["SmpAdapter<br/>интерфейс"]
  M1 & M2 --> MACL["MisAdapter<br/>интерфейс"]
  GACL & SACL & MACL --> CANON

Оси независимы, потому что вендор СМП и поставщик координат — не одно и то же лицо. У одного вендора координаты собираются на его собственном GPS-сервере, доступ к которому предоставляется через него же. У другого координаты берутся напрямую от отдельного софта, занимающегося маршрутизацией и отображением карет. Жёсткая связка «вендор СМП → его координаты» сломалась бы на первом же УЗ с другим сочетанием.

Anti-Corruption Layer

[ADR-2] Каждый вендор получает адаптер, переводящий его формат в каноническую модель. Ядро работает только с канонической моделью.

flowchart LR
  V["Формат вендора<br/>(свой у каждого)"] --> ACL["ACL-адаптер"]
  ACL --> N["Нормализация:<br/>коды, единицы,<br/>часовые пояса,<br/>идентификаторы"]
  N --> C["Каноническая модель<br/>FHIR R4"]
  C --> CORE["Бизнес-логика"]
  CORE -.->|"никогда не обращается<br/>напрямую"| V

Что именно нормализует ACL

Это не переименование полей. Реальные расхождения между вендорами:

Расхождение Пример Как решается
Единицы измерения Температура °C / °F; давление мм рт. ст. / кПа Приведение к UCUM (http://unitsofmeasure.org) — единая система в канонической модели
Кодировки состояний «тяжёлое» / severe / код 3 Маппинг на справочник канонических кодов; неизвестный код → явная ошибка, не молчаливый null
Часовые пояса Время без смещения / UTC / локальное Всё приводится к моменту времени с явным смещением; хранение в UTC
Идентификаторы пациента Личный номер / номер полиса / внутренний ID вендора Каноническая модель хранит все с указанием системы (identifier.system), а не выбирает один
Полнота данных Один вендор шлёт диагноз, другой только повод вызова Каноническая модель допускает отсутствие; обязательность проверяется на уровне бизнес-правил, а не парсинга
Гранулярность обновлений Полный снимок состояния / только изменённые показатели Адаптер приводит к событийной модели: каждое обновление — набор наблюдений с временной меткой

Неизвестный код — это ошибка, а не пустое значение

Соблазн при нормализации — подставить null или значение по умолчанию, когда код вендора не найден в справочнике. Для медицинских данных это недопустимо: незамеченная потеря значения витального показателя опаснее явного отказа. Правило: неизвестный код → сообщение принимается и сохраняется сырым, но помечается флагом неполной нормализации и попадает в отчёт для наладки адаптера.

Контракты адаптеров

[ПР] Три интерфейса, которые обязан реализовать адаптер вендора.

SmpAdapter — ось API СМП (push)

interface SmpAdapter {
  readonly providerCode: string;

  /** Проверка подлинности источника: mTLS-сертификат, подпись тела, IP. */
  authenticate(request: Request): ProviderIdentity;

  /** Стабильный идентификатор сообщения для идемпотентности (ADR-3). */
  messageUid(payload: Buffer): string;

  /** Определение одного из 5 типов входящих сообщений. */
  detectType(payload: Buffer): MessageType;

  /** Перевод в каноническую модель.
   *  Не бросает исключение на неизвестных кодах — помечает NormalizationIssue. */
  toCanonical(payload: Buffer): CanonicalMessage;

  /** Ответ вендору на transport_request в его формате. */
  buildResponse(decision: BedDecision): Buffer;
}

GeoAdapter — ось координат (pull)

interface GeoAdapter {
  readonly providerCode: string;
  readonly pollIntervalSec: number;    // свой темп у каждого источника
  readonly rateLimitPerMin: number | null;

  /** Опрос сервера координат. Возвращает позиции с меткой времени
   *  источника (не времени опроса) и оценкой точности. */
  fetchPositions(vehicleIds: string[]): Promise<VehiclePosition[]>;

  /** Доступность источника — для индикации деградации на пульте. */
  health(): Promise<ProviderHealth>;
}

MisAdapter — ось МИС

interface MisAdapter {
  readonly providerCode: string;

  /** Создание обращения в приёмный покой (FR-T-23). */
  pushAppeal(appeal: Appeal): Promise<MisAppealRef>;

  /** Ручная передача тяжёлого пациента: сбор бригады (FR-T-11). */
  requestTeam(handoff: TeamHandoff): Promise<MisTeamRef>;

  /** Витальные показатели в ЭМК (FR-M-2). */
  pushObservations(patientRef: MisPatientRef, obs: Observation[]): Promise<void>;
}

Полный DDL — в «Схеме БД».

Параметр Назначение
axis Ось: smp / geo / mis. Один вендор может присутствовать на двух осях как две записи
adapter_class Какой ACL-класс инстанцировать. Реестр адаптеров в коде, выбор — из БД
settings.poll_interval_sec Темп опроса для geo
settings.rate_limit Ограничение частоты, если вендор его требует
settings.tz Часовой пояс вендора, если он шлёт время без смещения
settings.code_map_version Версия справочника кодов для этого вендора

Стратегия ввода нового вендора

[ПР] Порядок, снимающий главный риск интеграции — недоступность вендора на стадии разработки (R-1).

flowchart LR
  S1["1 · Сбор фикстур<br/>реальные примеры<br/>сообщений вендора"] --> S2["2 · Симулятор вендора<br/>воспроизводит протокол<br/>локально"]
  S2 --> S3["3 · Реализация ACL<br/>против симулятора"]
  S3 --> S4["4 · Контрактные тесты<br/>фикстуры → каноническая<br/>модель, покрытие кодов"]
  S4 --> S5["5 · Стенд с вендором<br/>сверка поведения"]
  S5 --> S6["6 · Промышленная<br/>эксплуатация"]

Шаги 1–4 не требуют доступности вендора и выполняются в этапе 1. Шаги 5–6 привязаны к этапу 3. Такое разделение обязательно: паспорт помещает интеграцию с МРМ СМП в этап 3, тогда как разработка «Триажа» идёт в этапе 1 — без симулятора этап 1 невозможно ни завершить, ни протестировать.

Симулятор вендора — поставляемый артефакт, а не временный костыль

После ввода в эксплуатацию симулятор остаётся нужен: для регрессионного тестирования, для обучения персонала без реальных пациентов и для воспроизведения инцидентов. Он вынесен в backlog как отдельная задача с оценкой, а не спрятан внутри задачи интеграции. См. E2-2 в backlog.

Деградация при отказе провайдера

[ПР] Отказ одного источника не должен останавливать приём пациентов.

Отказ Поведение системы Что видит оператор
Сервер координат недоступен Приём медданных продолжается; последняя известная позиция сохраняется с меткой давности Карета на карте серым, подпись «координаты устарели N мин»
Вендор СМП молчит по активному направлению Направление остаётся активным; включается детектор тишины Карточка помечена «нет обновлений N мин»
МИС недоступна при передаче обращения Обращение сохраняется локально, ставится в очередь на повтор с backoff Индикатор «ожидает передачи в МИС», ручной повтор
Шлюз RS-485 недоступен Полевой сегмент в автономном режиме Авария сегмента на посту

Молчаливая деградация недопустима

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