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

ERD

СУБД: PostgreSQL 16 (обоснование — в «Стеке»). Логи пишутся в ту же БД (FR-C-1). Персональные данные в patients — с ограниченным доступом и шифрованием чувствительных полей, см. «Безопасность».

Схема разбита на пять предметных областей — одна диаграмма на область читается, одна общая на 40 таблиц не читается.

Область 1 — Конфигурация и справочники

Основа тиражируемости: УЗ, отделения и вендоры — данные, а не код (FR-C-6).

erDiagram
  FACILITIES ||--o{ DEPARTMENTS : "содержит"
  FACILITIES ||--o{ PROVIDERS : "подключены к"
  FACILITIES ||--o{ USERS : "работают в"
  PROVIDERS ||--o{ PROVIDER_ENDPOINTS : "имеет"
  PROVIDERS ||--o{ PROVIDER_CREDENTIALS : "имеет"
  DEPARTMENTS ||--o{ WARDS : "содержит"
  WARDS ||--o{ BEDS : "содержит"
  USERS ||--o{ USER_ROLES : "имеет"
  ROLES ||--o{ USER_ROLES : "назначена"

  FACILITIES {
    bigint id PK
    string code
    string name
    string timezone
  }
  PROVIDERS {
    bigint id PK
    bigint facility_id FK
    string code
    enum axis "smp|geo|mis"
    string adapter_class
    boolean is_active
    jsonb settings
  }
  DEPARTMENTS {
    bigint id PK
    bigint facility_id FK
    string code
    string name
    string profile "профиль отделения"
  }

Область 2 — «Триаж»: транспортный эпизод

Группы данных 1–5. Пациент едет; система принимает сообщения и следит за каретой.

erDiagram
  PROVIDERS ||--o{ INBOUND_MESSAGES : "источник"
  INBOUND_MESSAGES ||--o| BED_REQUESTS : "порождает"
  INBOUND_MESSAGES ||--o| TRANSPORT_NOTIFICATIONS : "порождает"
  INBOUND_MESSAGES ||--o{ VITAL_OBSERVATIONS : "порождает"
  INBOUND_MESSAGES ||--o| DELIVERIES : "порождает"

  PATIENTS ||--o{ TRANSPORT_NOTIFICATIONS : "везут"
  AMBULANCES ||--o{ TRANSPORT_NOTIFICATIONS : "перевозит"
  PRACTITIONERS ||--o{ TRANSPORT_NOTIFICATIONS : "бригада"
  TRANSPORT_NOTIFICATIONS ||--o{ VITAL_OBSERVATIONS : "состояние в пути"
  TRANSPORT_NOTIFICATIONS ||--o| DELIVERIES : "завершается"
  TRANSPORT_NOTIFICATIONS ||--o{ TRANSPORT_EVENTS : "события статуса"
  AMBULANCES ||--o{ AMBULANCE_LOCATIONS : "положение"
  PROVIDERS ||--o{ LOCATION_REQUESTS : "опрашивается"
  LOCATION_REQUESTS ||--o{ AMBULANCE_LOCATIONS : "результат"

  INBOUND_MESSAGES {
    bigint id PK
    bigint provider_id FK
    string message_uid UK "идемпотентность"
    enum message_type
    jsonb raw_body "сырой Bundle"
    enum status
    timestamptz received_at
  }
  BED_REQUESTS {
    bigint id PK
    string external_call_id
    enum decision "accepted|rejected|conditional"
    string decision_reason
    bigint decided_by FK
  }
  TRANSPORT_NOTIFICATIONS {
    bigint id PK
    string external_call_id UK
    bigint patient_id FK
    bigint ambulance_id FK
    enum status
    enum smp_priority
    string reason_text
    timestamptz transport_started_at
  }
  VITAL_OBSERVATIONS {
    bigint id PK
    bigint notification_id FK
    string loinc_code
    numeric value_num
    string unit_ucum
    timestamptz effective_at "ключ партиции"
  }
  DELIVERIES {
    bigint id PK
    bigint notification_id FK
    timestamptz delivered_at "отсечка СМП"
    timestamptz accepted_at "приём в УЗ"
    bigint accepted_by FK
  }

Две отметки времени доставки, а не одна

delivered_at — момент по данным СМП (transport_complete), accepted_at — подтверждение медработником приёмного отделения. Разница между ними — время ожидания машины у приёмника, управленчески значимая метрика. Одно поле вместо двух сделало бы её неизмеримой. См. «Жизненный цикл».

Область 3 — «Триаж»: обращение, сортировка, очередь

Группы данных 6–7. Здесь сходятся оба потока — СМП и самообращения (ADR-5).

erDiagram
  APPEALS ||--o| TRANSPORT_NOTIFICATIONS : "если по СМП"
  PATIENTS ||--o{ APPEALS : "обращается"
  APPEALS ||--o{ TRIAGE_ASSESSMENTS : "оценивается"
  APPEALS ||--o| TRIAGE_QUEUE : "стоит в очереди"
  APPEALS ||--o{ APPEAL_EVENTS : "события"
  APPEALS ||--o| MIS_HANDOFFS : "передаётся в МИС"
  APPEALS ||--o| MEDIK_TEAM_HANDOFFS : "сбор бригады"
  TRIAGE_ASSESSMENTS }o--|| TRIAGE_RULE_VERSIONS : "по версии правил"
  TRIAGE_RULES ||--o{ TRIAGE_RULE_VERSIONS : "версионируется"
  TRIAGE_ASSESSMENTS ||--o{ TRIAGE_ASSESSMENT_INPUTS : "входные значения"

  APPEALS {
    bigint id PK
    bigint facility_id FK
    bigint patient_id FK "NULL до оформления"
    bigint notification_id FK "NULL для самообращений"
    enum source "smp|self"
    enum status
    timestamptz opened_at
    timestamptz closed_at
  }
  TRIAGE_ASSESSMENTS {
    bigint id PK
    bigint appeal_id FK
    enum color "red|yellow|green"
    string ticket_code "К43|В34|П45"
    enum mode "rules|manual|mass_casualty"
    bigint rule_version_id FK
    bigint assessed_by FK
    timestamptz assessed_at
  }
  TRIAGE_QUEUE {
    bigint id PK
    bigint appeal_id FK,UK
    enum color
    int priority_rank
    timestamptz enqueued_at
    timestamptz called_at
    timestamptz sla_deadline
  }
  TRIAGE_RULE_VERSIONS {
    bigint id PK
    bigint rule_id FK
    int version
    jsonb conditions
    enum result_color
    boolean is_active
    bigint approved_by FK
  }
  MIS_HANDOFFS {
    bigint id PK
    bigint appeal_id FK
    enum kind "appeal|team_request"
    enum status "pending|sent|failed"
    string mis_reference
    int retry_count
  }

Почему сортировка — отдельная таблица, а не поле в обращении

Пациента сортируют более одного раза: при поступлении, при ухудшении в очереди, при пересортировке в режиме массового поступления. История оценок с указанием версии правил и автора — единственный способ разобрать случай постфактум («почему ему дали жёлтый в 9:15»). Поле color в appeals хранило бы только последнее значение и уничтожало бы историю.

TRIAGE_ASSESSMENT_INPUTS фиксирует, какие значения были на входе правила — без этого воспроизвести решение невозможно, потому что витальные показатели меняются.

Область 6 — Журналы и аудит

erDiagram
  USERS ||--o{ AUDIT_LOG : "действия"
  PROVIDERS ||--o{ INTEGRATION_LOG : "обмен"
  USERS ||--o{ SECURITY_EVENTS : "события ИБ"

  AUDIT_LOG {
    bigint id PK
    bigint user_id FK
    string action
    string entity_type
    bigint entity_id
    jsonb before_state
    jsonb after_state
    inet ip_address
    timestamptz created_at "ключ партиции"
  }
  INTEGRATION_LOG {
    bigint id PK
    bigint provider_id FK
    enum direction "in|out"
    string operation
    int http_status
    int duration_ms
    text error_text
    timestamptz created_at "ключ партиции"
  }
  SECURITY_EVENTS {
    bigint id PK
    enum event_type
    bigint user_id FK
    inet source_ip
    boolean is_success
    jsonb details
    timestamptz created_at "ключ партиции"
  }

[ТЗ] SECURITY_EVENTS — прямое требование: регистрация событий информационной безопасности с централизованным сбором и хранением не менее одного года (FR-C-2). Отдельно от AUDIT_LOG, потому что у них разные сроки хранения, разные потребители и разные права доступа.

Быстрорастущие таблицы

Три таблицы определяют объём БД и требуют партиционирования по времени:

Таблица Драйвер роста Оценка
AMBULANCE_LOCATIONS Карет в пути × частота опроса Средняя
VITAL_OBSERVATIONS Направлений × частота health_status Средняя
SECURITY_EVENTS + AUDIT_LOG + INTEGRATION_LOG Все действия, хранение ≥ 1 года Средняя

Расчёт ёмкости и политики ретеншна — в «Нефункциональных требованиях». DDL с партициями — в «Схеме БД».