Адаптеры и мультипровайдерность¶
[ОП] Требование: система должна одновременно работать с несколькими поставщиками по трём независимым осям. [ТЗ] Дополнительный мотив: тиражирование на ≥ 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 недоступен | Полевой сегмент в автономном режиме | Авария сегмента на посту |
Молчаливая деградация недопустима
Общее правило для всех строк таблицы: система никогда не показывает устаревшие данные как актуальные. Отсутствие обновлений отображается явно. В приёмном отделении «карета показана не там, где она есть» опаснее, чем «положение кареты неизвестно» — во втором случае оператор знает, что нужно позвонить.