Маршрутизация пациентов в телемедицине: архитектура, алгоритмы и API-интеграция | AdminWiki

Маршрутизация пациентов в телемедицине: архитектура, алгоритмы и API-интеграция

20 июля 2026 12 мин. чтения

Архитектура системы маршрутизации: ключевые компоненты

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

В ядре архитектуры находятся три компонента. API Gateway принимает внешние запросы от веб-интерфейсов и мобильных приложений, проводит аутентификацию и направляет вызов к сервису маршрутизации. Сервис маршрутизации содержит всю логику подбора врача. Брокер сообщений обеспечивает асинхронную связь с остальной экосистемой: сервисами расписаний, уведомлений и аналитики.

Типичный сценарий выглядит так. Пациент нажимает кнопку «Получить консультацию». Запрос через Gateway попадает в сервис маршрутизации. Сервис собирает данные: профиль пациента, список дежурных врачей, их текущую загрузку из кеша. Алгоритм за 50–100 мс определяет врача. Результат возвращается пациенту, а событие о назначении через брокер уходит в сервисы видеоконференций и календарей. Если свободных врачей нет, запрос ставится в очередь с уведомлением пациента о примерном времени ожидания.

Сервис маршрутизации: ядро логики распределения

Сервис маршрутизации - это stateless-приложение, которое можно развернуть в нескольких экземплярах за балансировщиком. Его API состоит из двух основных эндпоинтов.

POST /route принимает JSON с идентификатором пациента, типом запроса (плановая или экстренная консультация) и предпочитаемой специализацией врача. В ответ возвращается объект с назначенным врачом, временем начала консультации и ссылкой на видеокомнату. Статус-код 202 означает, что запрос принят, но врач ещё не назначен - пациент поставлен в очередь.

GET /status/{id} позволяет клиенту опрашивать состояние своего запроса. Возможные статусы: queued, matched, scheduled, failed. Для real-time обновлений используется WebSocket-соединение, которое сервер инициирует при изменении статуса.

Внутренняя логика сервиса опирается на быстрый доступ к данным. Информация о врачах, их слотах и текущей загрузке хранится в Redis. Выбор Redis обусловлен скоростью операций чтения (менее 1 мс) и встроенными структурами данных: Sorted Sets для хранения слотов по времени, Lists для очередей пациентов. При старте сервис загружает снапшот данных из основной базы и подписывается на поток изменений через брокер сообщений. Это гарантирует, что решение о маршрутизации принимается на актуальных данных без запросов к медленной дисковой БД.

Интеграция с брокером сообщений для событийной модели

Синхронные REST-вызовы хороши для прямых запросов, но для реакции на изменения в системе нужна событийная модель. Apache Kafka выполняет роль центральной нервной системы: все значимые события публикуются в топики, а заинтересованные сервисы подписываются на них.

Основные топики для системы маршрутизации:

  • doctor.availability.updated - врач изменил рабочие часы или заблокировал слот. Сервис маршрутизации обновляет данные в Redis.
  • appointment.booked - консультация подтверждена обеими сторонами. Триггерит создание видеоконференции и событие в календаре.
  • patient.no_show - пациент не подключился к консультации в течение N минут. Врач освобождается, слот может быть переиспользован.
  • doctor.overloaded - загрузка врача превысила порог. Сервис временно исключает его из пула доступных.

Каждый сервис маршрутизации входит в свою consumer group, что гарантирует доставку каждого события ровно в один экземпляр сервиса. При падении одного пода Kafka автоматически перебалансирует партиции на оставшиеся. Это даёт надёжную гарантию обработки событий без дублирования действий.

Алгоритм подбора врача: учёт часовых поясов и загрузки

Алгоритм - это сердце системы. Он должен быть детерминированным, быстрым и справедливым. На вход подаются: идентификатор пациента, требуемая специализация и признак срочности. На выходе - идентификатор врача или статус постановки в очередь.

Шаг первый: определение временной зоны пациента. Система берёт её из профиля пользователя. Если профиль не заполнен, используется геолокация по IP. Все расчёты времени ведутся в UTC, преобразование в локальное время происходит на клиентской стороне.

Шаг второй: фильтрация врачей. Из пула исключаются специалисты не той специализации, врачи в нерабочее время и те, чья загрузка достигла максимума. Рабочее время врача хранится как набор правил: день недели, начало и конец рабочего дня в его локальной зоне. При загрузке в Redis правила раскрываются в конкретные UTC-слоты на ближайшие 7 дней.

Шаг третий: расчёт загрузки. Для каждого кандидата вычисляется коэффициент: количество активных консультаций делится на максимально допустимое. Активной считается консультация, которая длится прямо сейчас или начнётся в ближайшие 15 минут. Время начала берётся из подтверждённых назначений в Redis.

Шаг четвёртый: выбор врача с минимальным коэффициентом загрузки. При равенстве показателей предпочтение отдаётся тому, у кого дольше не было назначений - это предотвращает неравномерное распределение.

Псевдокод алгоритма на Python:

def find_best_doctor(patient_id, specialization, is_emergency):
    patient_tz = get_patient_timezone(patient_id)
    now_utc = datetime.now(timezone.utc)
    candidates = get_available_doctors(specialization, now_utc)
    
    if not candidates:
        return enqueue_patient(patient_id, specialization)
    
    loads = {doc.id: calculate_load(doc.id, now_utc) for doc in candidates}
    best = min(candidates, key=lambda d: (loads[d.id], d.last_assigned_at))
    
    assign_doctor(patient_id, best.id, now_utc)
    return best

Крайние случаи обрабатываются явно. Если подходящих врачей нет, пациент попадает в очередь с приоритетом. Каждые 30 секунд очередь пересматривается - вдруг освободился врач или изменилась загрузка. Пациенту через WebSocket отправляется уведомление с примерным временем ожидания, рассчитанным по средней длительности консультаций.

Работа с часовыми поясами: практические рекомендации

Ошибки в работе с часовыми поясами - самая частая причина некорректной маршрутизации. Врач в Москве указал доступность «09:00–17:00 Europe/Moscow». Пациент из Нью-Йорка хочет записаться. Если не привести оба времени к единой шкале, консультация может быть назначена на 3 часа ночи для врача.

Правило простое: всё хранится в UTC, всё отображается в локальной зоне. Слот доступности врача конвертируется в UTC на этапе сохранения. Когда пациент запрашивает консультацию, его локальное время используется только для отображения в интерфейсе. Серверные расчёты идут исключительно в UTC.

Для работы с зонами в Python используйте библиотеку pytz или встроенный zoneinfo (Python 3.9+). Пример конвертации слота врача:

from datetime import datetime, time
from zoneinfo import ZoneInfo

moscow_tz = ZoneInfo("Europe/Moscow")
utc_tz = ZoneInfo("UTC")

# Врач указал начало рабочего дня в 9:00 по Москве
local_start = datetime.combine(date.today(), time(9, 0), tzinfo=moscow_tz)
utc_start = local_start.astimezone(utc_tz)
# utc_start теперь 6:00 UTC - это значение сохраняется в БД

Переход на летнее/зимнее время обрабатывается автоматически, если использовать IANA-идентификаторы зон (например, Europe/Moscow), а не фиксированные смещения вроде UTC+3. Фиксированное смещение не учитывает сезонные изменения и приведёт к ошибкам дважды в год.

Метрики загрузки и стратегии балансировки

Загрузка врача - это не просто количество активных консультаций. Система учитывает три метрики с разными весами. Текущее количество консультаций - базовая метрика, вес 0.5. Средняя длительность консультаций этого врача за последние 7 дней - отражает стиль работы, вес 0.3. Процент отказов или переносов - сигнал о перегрузке или низкой вовлечённости, вес 0.2.

Итоговый коэффициент загрузки нормализуется от 0 до 1. Врач с коэффициентом выше 0.8 временно исключается из маршрутизации для плановых консультаций, но остаётся доступным для экстренных.

Стратегий балансировки несколько, и выбор зависит от специфики клиники. Least Connections - стандартный выбор, минимизирует время ожидания. Round Robin с весами подходит, когда врачи имеют разную пропускную способность: терапевт принимает 4 пациента в час, узкий специалист - 2. Веса задаются в конфигурационном файле сервиса и могут меняться без перевыпуска кода. Взвешенная по приоритету стратегия резервирует часть слотов для экстренных пациентов, чтобы срочные запросы не ждали в общей очереди.

Приоритизация пациентов: встраивание правил в маршрутизацию

Не все запросы на консультацию равнозначны. Система определяет три уровня приоритета на основе триажа - быстрой оценки состояния пациента при заполнении формы. Экстренный уровень: симптомы указывают на угрозу жизни или необратимые последствия. Высокий: состояние требует внимания врача в течение часа. Обычный: плановая консультация или повторный приём.

Правила обработки жёстко закодированы в логике сервиса. Экстренный запрос получает первого освободившегося врача нужной специализации, даже если для этого прерывается перерыв или сдвигается плановая консультация. Если свободных врачей нет, экстренный запрос ожидает не более 2 минут, после чего система эскалирует его на любого дежурного врача смежной специализации. Высокий приоритет допускает ожидание до 10 минут, обычный - до 30 минут, после чего пациенту предлагается запись на другое время.

Техническая реализация - приоритетная очередь в Redis. Используется структура Sorted Set, где score - это timestamp с учётом приоритета: для экстренных вычитается 900 секунд (15 минут штрафа к обычному времени), для высоких - 300 секунд. При добавлении в очередь пациент получает тот же timestamp, но за счёт штрафа экстренные запросы всегда оказываются в начале выборки. Сервис маршрутизации раз в 5 секунд забирает верхний элемент очереди и пытается найти врача.

API-интеграция с внешними сервисами

Сервис маршрутизации не существует в вакууме. Его ценность раскрывается в связке с инструментами, которыми уже пользуются врачи и пациенты. Три ключевые интеграционные точки: видеоконференции, календари и каналы уведомлений. Для каждой реализован унифицированный интерфейс, что позволяет заменять провайдера без изменения ядра системы.

Создание видеоконференции через REST API

Сразу после назначения врача сервис маршрутизации публикует событие appointment.booked. Обработчик этого события вызывает API провайдера видеоконференций. Рассмотрим интеграцию с Zoom API как наиболее распространённый сценарий.

Аутентификация происходит по OAuth 2.0 с Server-to-Server приложением. Токен кешируется в Redis с TTL на 10 минут меньше срока жизни, чтобы избежать лишних запросов к Zoom. Создание конференции - один POST-запрос к /v2/users/me/meetings с телом:

{
  "topic": "Консультация с врачом-терапевтом",
  "type": 2,
  "start_time": "2026-07-20T14:00:00Z",
  "duration": 30,
  "timezone": "UTC",
  "settings": {
    "waiting_room": true,
    "join_before_host": false
  }
}

Ответ содержит join_url и start_url. Первый отправляется пациенту, второй - врачу. Оба URL сохраняются в карточке назначения в основной БД.

Обработка ошибок критична. При статусе 429 (слишком много запросов) включается экспоненциальный retry с начальной задержкой 1 секунда и максимумом 60 секунд. При таймауте или 5xx ошибке срабатывает Circuit Breaker: после 5 последовательных ошибок вызовы к Zoom прекращаются на 30 секунд, назначение временно обслуживается без видеоссылки с пометкой «ссылка будет отправлена позже». Подробнее о паттернах отказоустойчивости - в руководстве по архитектуре высоконагруженных систем.

Синхронизация с календарями врачей

Расписание врача - это источник истины для маршрутизации. Двусторонняя синхронизация с Google Calendar гарантирует, что система знает о всех занятых слотах, даже если врач вручную добавил личную встречу.

При назначении консультации сервис создаёт событие в календаре врача через Google Calendar API. В теле запроса передаются: заголовок (обезличенный, без ФИО пациента), время начала и конца, ссылка на видеоконференцию. В ответ Google возвращает идентификатор события, который сохраняется в системе для будущих обновлений.

Для получения изменений из календаря используется механизм webhook. Google отправляет POST-уведомление на эндпоинт сервиса маршрутизации при любом изменении в календаре врача. Сервис запрашивает дельту изменений и обновляет слоты в Redis. Защита от конфликтов реализована через оптимистическую блокировку: каждый слот имеет версию, и при записи проверяется, не изменился ли он с момента чтения. Если слот уже занят другим назначением, текущая операция откатывается, а пациенту подбирается новое время.

Уведомления через WebSocket и чат-боты

Пациент должен знать, что происходит с его запросом, без необходимости обновлять страницу. WebSocket-соединение устанавливается при создании запроса на маршрутизацию и поддерживается до завершения консультации. Сервер отправляет JSON-сообщения с типом события и полезной нагрузкой:

{"event": "status_changed", "data": {"status": "matched", "eta": 120}}

Статусы, которые получает пациент: searching - идёт поиск врача, queued - поставлен в очередь с указанием примерного времени ожидания, matched - врач найден, scheduled - консультация подтверждена с указанием времени и ссылки, reminder - напоминание за 5 минут до начала.

Дублирующий канал - Telegram-бот. При назначении врача бот отправляет пациенту сообщение: «Врач-терапевт ждёт вас в 14:00 МСК. Ссылка на консультацию: ...». Формат сообщения настраивается в конфигурации сервиса. Для врачей бот присылает уведомление о новом пациенте за 2 минуты до начала с краткой информацией: специализация, приоритет, длительность.

Обеспечение отказоустойчивости и обработка исключений

Система маршрутизации - это критичный компонент. Её падение означает остановку приёма пациентов. Отказоустойчивость закладывается на трёх уровнях: инфраструктурном, прикладном и информационном.

На инфраструктурном уровне сервис маршрутизации разворачивается минимум в трёх репликах в разных зонах доступности. Балансировщик с health-check проверяет эндпоинт /health каждые 5 секунд и выводит нездоровые поды из ротации. Redis работает в режиме Sentinel с автоматическим переключением на реплику при отказе мастера. Kafka имеет фактор репликации 3 и минимальный in-sync replica count 2, что гарантирует сохранность событий при потере одного брокера.

На прикладном уровне каждый внешний вызов обёрнут в Circuit Breaker. Библиотека Resilience4j отслеживает долю ошибок в скользящем окне. При превышении порога в 50% ошибок за 30 секунд цепь размыкается на 60 секунд. Это предотвращает каскадные отказы, когда недоступность Zoom API замедляет весь сервис маршрутизации. Состояние разомкнутой цепи логируется, и администратор получает алерт.

Retry с экспоненциальной задержкой применяется для временных сбоев: сетевых таймаутов, кратковременной недоступности внешнего API. Три попытки с задержками 1с, 2с, 4с покрывают большинство transient-ошибок. Если все попытки исчерпаны, сообщение попадает в Dead Letter Queue - специальный топик Kafka, откуда его можно переиграть после восстановления сервиса.

Мониторинг завязан на три ключевые метрики, которые экспортируются в Prometheus. Процент успешных маршрутизаций за 5-минутное окно: падение ниже 95% триггерит алерт. Медианное время ответа эндпоинта /route: рост выше 200 мс указывает на проблемы с Redis или алгоритмом. Количество пациентов в очереди: резкий скачок сигнализирует о нехватке врачей или сбое в их расписаниях. Готовые конфигурации алертов и дашбордов мы разбирали в руководстве по мониторингу маршрутизации.

Логирование каждого этапа маршрутизации - обязательное требование для аудита и отладки. Каждый запрос получает trace id, который пробрасывается через все сервисы. В лог попадают: идентификатор пациента, параметры запроса, список рассмотренных врачей с их загрузкой, выбранный врач, время обработки. При жалобе пациента на долгое ожидание инженер за минуту восстанавливает полную картину по trace id.

Модель данных для хранения информации о маршрутизации

Правильно спроектированная схема данных ускоряет запросы и упрощает логику приложения. Основное хранилище - реляционная БД (PostgreSQL), кеширующий слой - Redis. Выбор PostgreSQL обусловлен необходимостью строгой консистентности для данных расписаний и назначений, а также богатыми возможностями работы с временными рядами.

Ключевые сущности и их связи:

  • Patient: id (UUID), timezone (varchar, IANA-идентификатор), priority_tier (enum: emergency, high, normal), created_at.
  • Doctor: id (UUID), specialization (varchar), timezone (varchar), max_concurrent_consultations (integer), is_active (boolean).
  • AvailabilitySlot: id (UUID), doctor_id (FK), start_utc (timestamp with time zone), end_utc (timestamp with time zone), is_booked (boolean), version (integer для оптимистической блокировки).
  • RouteRequest: id (UUID), patient_id (FK), status (enum), assigned_doctor_id (FK, nullable), priority (enum), created_at, resolved_at.
  • Consultation: id (UUID), route_request_id (FK), doctor_id (FK), patient_id (FK), scheduled_start_utc, actual_start_utc, actual_end_utc, status (enum: scheduled, in_progress, completed, no_show, cancelled).

Индексы критичны для производительности. Составной индекс на AvailabilitySlot (doctor_id, start_utc, is_booked) ускоряет поиск свободных слотов конкретного врача. Индекс на RouteRequest (status, created_at) нужен для мониторинга очереди. Частичный индекс WHERE status = 'queued' на этой же таблице делает выборку ожидающих пациентов мгновенной даже при миллионах записей.

В Redis данные хранятся в денормализованном виде для максимальной скорости чтения. Слоты доступности - Sorted Set с ключом slots:{doctor_id}, где score - UTC timestamp начала слота, а значение - JSON с деталями. Текущая загрузка врача - простое строковое значение load:{doctor_id} с TTL 60 секунд, обновляемое при каждом назначении. Очередь пациентов - Sorted Set queue:{specialization}, где score - приоритетный timestamp. Такое разделение между PostgreSQL (источник истины) и Redis (оперативный кеш) даёт баланс надёжности и скорости.

Для развёртывания описанной архитектуры потребуется облачная инфраструктура с поддержкой Kubernetes и управляемых БД. Timeweb Cloud предоставляет готовые кластеры Kubernetes, managed PostgreSQL и Redis, что сокращает время от проектирования до продакшена.

Поделиться:
Сохранить гайд? В закладки браузера