20 KiB
title
| title |
|---|
| Расписание врачей — синхронизация и отображение (Backend + Cabinet) |
Расписание врачей: синхронизация и отображение
Полный разбор того, как в проекте работает расписание врачей: от Infoclinica MIS до карточки на сайте. Охватывает
apps/backend,apps/cabinetи роль adminPanel.Связанные документы:
0. TL;DR
В проекте нет единого batch-sync всех слотов всех врачей. Вместо этого работают два независимых механизма:
| Что | Как обновляется | Где хранится |
|---|---|---|
Слоты времени (09:00–09:30 и т.д.) |
По запросу из Infoclinica MIS | Backend: таблица schedule (кэш 5 мин). Cabinet: не хранит, тянет live через /api/interval |
Ближайшая дата приёма (nearestDate) |
Пакетно, раз в час (cron) | Backend: idoctor → location / specialist через SQL-функции; Cabinet читает location_view |
Cabinet и backend — параллельные потребители Infoclinica. Cabinet не вызывает backend GET /specialist/schedule.
Источник правды по слотам — Infoclinica MIS (MIS_URL / MIS env, обычно widget.sovamed.ru).
1. Общая архитектура
flowchart TB
subgraph MIS [Infoclinica MIS]
API1["GET /api/reservation/intervals"]
API2["GET /api/reservation/schedule"]
API3["GET /specialists/doctors"]
API4["POST /api/reservation/anonymous-reserve"]
end
subgraph Backend [apps/backend]
EP1["GET /specialist/schedule"]
Handler["GetScheduleMessageHandler"]
Cache["ScheduleCacheService → schedule"]
Cron["upload:doctors 0/1"]
Idoctor[(idoctor)]
Location[(location)]
SP["update_specialist() / update_location()"]
end
subgraph Cabinet [apps/cabinet]
EP2["GET /api/interval"]
Stimulus["checkSchedule_controller.js"]
Views[(specialist_view / location_view)]
end
subgraph Admin [adminPanel + backend API]
AdminAPI["GET /idoctor/list\nCRUD specialist/location"]
end
EP1 --> Handler --> Cache
Handler --> API1
Cache --> API1
Cron --> API3 --> Idoctor --> SP --> Location
Stimulus --> EP2
EP2 --> API1
EP2 --> API2
AdminAPI --> Idoctor
Views -.->|"nearestDate для сортировки"| Location
2. Два типа «расписания»
2.1. Слоты (интервалы времени)
Бизнес-смысл: пациент видит конкретные свободные окна для записи.
Поведение: данные не синхронизируются заранее. При открытии карточки врача frontend запрашивает MIS (напрямую или через прокси backend/cabinet). Ответ содержит:
workDate— дата приёма;isFree— есть ли свободные слоты в этот день;intervals[]— массив{ time, schedident, isFree, rnum? }.
Кэш: короткий (5 мин в backend, HTTP-кэш в cabinet), только чтобы снизить нагрузку на MIS.
2.2. Ближайшая дата (nearestDate)
Бизнес-смысл: «ближайшая запись — 28 мая», сортировка врачей «по времени приёма», фильтр по диапазону дат.
Поведение: реальная синхронизация — hourly cron тянет список врачей из MIS, сохраняет nearestDate в idoctor, затем SQL-функции обновляют location и view'ы cabinet.
Не используется для построения сетки слотов — слоты всегда live из MIS.
3. Backend (apps/backend)
3.1. API слотов: GET /specialist/schedule
Точка входа: SpecialistController::specialistSchedule
Параметры (ScheduleDto):
| Параметр | Тип | Описание |
|---|---|---|
st, en |
integer | Границы периода в формате Ymd (например 20250525) |
dcode |
integer | Код врача в Infoclinica |
filial |
integer | ID филиала (в query к MIS уходит как filialId) |
onlineMode |
boolean | 0 — очный приём, 1 — онлайн |
Query string для MIS и ключа кэша:
// apps/backend/src/Dto/ScheduleDto.php
return http_build_query([
'st' => $this->st,
'en' => $this->en,
'dcode' => $this->dcode,
'onlineMode' => $this->onlineMode,
'filialId' => $this->filial,
]);
3.2. Цепочка вызовов
SpecialistController::specialistSchedule()
→ SpecialistService::getSchedule(ScheduleDto)
→ MessageBus dispatch GetScheduleMessage (sync transport)
→ GetScheduleMessageHandler::__invoke()
→ ScheduleCacheService::getCachedSchedule() // TTL 5 мин
→ InfoclinicaClientService::getSchedule() // при промахе
→ ScheduleCacheService::saveSchedule()
Подробнее: schedule-messenger.md, schedule-cache.md.
3.3. Запрос в MIS
InfoclinicaClientService вызывает:
GET {MIS_URL}/api/reservation/intervals?st=...&en=...&dcode=...&onlineMode=...&filialId=...
Ответ нормализуется в структуру:
{
"schedule": {
"<depnum>": {
"<Ymd>": {
"schedident": "...",
"dcode": "...",
"filial": 1,
"isFree": true,
"intervals": [
{ "time": "09:00-09:30", "isFree": true }
]
}
}
},
"nearestDate": { "<depnum>": 20250525 }
}
3.4. Таблица schedule — кэш слотов
Одна строка = один интервал (не «расписание врача целиком»).
| Поле | Назначение |
|---|---|
dcode, department, filial |
Привязка к врачу / отделению / филиалу |
schedident |
ID расписания в MIS (нужен для записи) |
workdate |
Дата приёма |
time |
Интервал "HH:MM-HH:MM" |
intervalIsFree |
Свободен ли слот |
onlineMode |
Очный / онлайн |
queryString |
Ключ кэша (полный query string запроса) |
createdAt |
Время записи в кэш |
priceInfo |
JSON, только для онлайн-расписания |
TTL: 5 минут. При saveSchedule старые строки с тем же queryString + onlineMode удаляются перед insert.
Очистка: команда app:schedule:clear-cache (по умолчанию старше 24 ч). В cron не подключена.
3.5. Batch-sync nearestDate: upload:doctors
Команда: php bin/console upload:doctors [onlineMode]
onlineMode=0— очные врачи;onlineMode=1— онлайн.
Алгоритм:
- Читает активные отделения из
department. - Для каждого отделения вызывает MIS:
чанками по 300 записей.
GET /specialists/doctors?departments={did}&onlineMode={0|1}&firstrow=&lastrow= - Upsert в
idoctorпо ключуdcode + department + onlineMode. - Поля:
dcode,name,department,filial,nearestDate,onlineMode.
Подробнее: sync-doctors-reviews.md.
3.6. Hourly cron
Файл scripts/cron.hourly.sh:
docker exec -t -u www-data php84 php bin/console upload:doctors 0
docker exec -t -u www-data php84 php bin/console upload:doctors 1
docker exec -t pgsql psql -U sova_api -d sova_api -c "SELECT public.update_specialist();"
docker exec -t pgsql psql -U sova_api -d sova_api -c "SELECT public.update_location();"
sequenceDiagram
participant Cron as cron.hourly.sh
participant Cmd as upload:doctors
participant MIS as Infoclinica MIS
participant DB as PostgreSQL
Cron->>Cmd: upload:doctors 0 (offline)
Cron->>Cmd: upload:doctors 1 (online)
loop каждое активное отделение
Cmd->>MIS: GET /specialists/doctors
MIS-->>Cmd: dcode, nearestDate, ...
Cmd->>DB: upsert idoctor
end
Cron->>DB: SELECT update_specialist()
Cron->>DB: SELECT update_location()
Важно: функции update_specialist() и update_location() вызываются из cron, но их исходников нет в репозитории — они живут в production PostgreSQL. Без них nearestDate в cabinet не обновится, хотя idoctor будет актуален.
3.7. Роль adminPanel
Admin panel не синхронизирует слоты. Она настраивает метаданные:
| Поле | Сущность | Назначение |
|---|---|---|
displaySchedule |
specialist |
Показывать ли виджет расписания |
scheduleText |
specialist |
Текст под расписанием |
dcodes |
specialist |
Коды врача в Infoclinica |
dcode, department, filial, onlineMode |
location |
Привязка врача к MIS |
nearestDate |
location |
Можно задать вручную или взять из idoctor |
Backend API:
GET /idoctor/list— список синхронизированных врачей из Infoclinica (для модалки «Добавить расписание из Инфоклиники»);POST/PUT /specialist/{id}/location/*— CRUD локаций.
Без правильной location frontend не знает, какой dcode / филиал / отделение запрашивать у MIS.
3.8. Запись на приём (backend)
После выбора слота:
POST /reservation/anonymous-reserve
→ SpecialistService::createAnonymousReserve()
→ GetAnonymousReserveRequestMessageHandler
→ InfoclinicaClientService::anonymousReserve()
→ POST {MIS_URL}/api/reservation/anonymous-reserve
Поля из слота: schedident, workDate, time, dcode, filial, rnum.
Подробнее: anonymous-reserve.md.
4. Cabinet (apps/cabinet)
Cabinet — Symfony + Twig + Stimulus. Не SPA. Расписание на публичном сайте (cabinet.sovamed.ru / список врачей) реализовано здесь.
4.1. Принцип: live-запросы, не backend API
Cabinet не вызывает apps/backend /specialist/schedule. Свой прокси:
GET /api/interval?doctor=...&department=...&filial=...&startInterval=2025-05-25&endInterval=2025-06-01&onlineMode=0&update=true
Контроллер: PublicAPIController::interval()
Два параллельных запроса в MIS:
| MIS endpoint | Назначение |
|---|---|
GET /api/reservation/schedule |
Сетка рабочих дней (когда врач принимает) |
GET /api/reservation/intervals |
Конкретные слоты внутри дней |
Результат мержится: для каждого свободного дня из schedule подтягиваются intervals с тем же schedident.
HTTP-кэш: CachingHttpClient + store в var/HttpClient (короткий, не PostgreSQL).
4.2. Диапазон дат на странице врачей
SpecialistController::index():
- если в фильтре задан
specialist_search[current_date]— используется он; - иначе сегодня → +7 дней.
Даты передаются в DOM как data-st / data-en на .specialist-items и используются Stimulus-контроллером.
4.3. Клиент: checkSchedule_controller.js
При загрузке карточки врача (если specialist.infoclinica == true):
- Читает
dcode,filial,department,onlineModeиз DOM / селекта клиники. - Вызывает
GET /api/interval. - Рендерит до 6 свободных слотов на карточке.
- Кнопка «Все даты» открывает модалку (
specialistView_controller.js) с навигацией по неделям — тот же/api/interval.
Формат значения селекта клиники: dcode:filial:department:onlineMode:infoclinica
Twig: templates/specialist/_item.html.twig — монтирует data-controller="checkSchedule".
4.4. Врачи без Infoclinica
Если specialist.infoclinica == false (Bitrix HIDE_TIMETABLE или нет привязки к MIS):
- контроллер
checkScheduleне монтируется; - вместо слотов — кнопка «Записаться» → заявка в Bitrix CRM (
uslugi_controller.js).
4.5. nearestDate в cabinet
Cabinet читает nearestDate из read-only view location_view (LocationView entity):
- сортировка «по времени приёма»;
- фильтр по диапазону дат в поиске;
- не для построения сетки слотов.
View обновляется SQL-функциями backend cron (update_location()).
4.6. Запись на приём (cabinet)
- Клик по слоту →
record.js→ форма записи. - Авторизованный пользователь:
webSDK.scheduleRecReserve()напрямую в Infoclinica SDK (widget.sovamed.ru). - Анонимный:
POST /api/anonymous-reserve→ MIS/api/reservation/anonymous-reserve.
4.7. Legacy: старые CLI-команды cabinet
В apps/cabinet/src/Command/ есть устаревший путь полной синхронизации в локальные таблицы:
php bin/console app:Infoclinica schedules # → таблица Schedule
php bin/console app:Infoclinica intervals # → таблица Interval
php bin/console upload:doctorsInfoclinica # → таблица Idoctor
Статус: legacy. Миграция Version20250907100913 удалила таблицы specialist, location, idoctor из cabinet. Runtime UI перешёл на DB views (specialist_view, location_view), batch-sync — в backend (upload:doctors).
Текущий cabinet не читает локальные Schedule / Interval для UI.
5. Сравнение Backend vs Cabinet
| Аспект | Backend | Cabinet |
|---|---|---|
| Endpoint слотов | GET /specialist/schedule |
GET /api/interval |
| MIS endpoints | только /intervals |
/schedule + /intervals |
| Кэш слотов | PostgreSQL schedule, 5 мин |
HTTP cache var/HttpClient |
Batch sync nearestDate |
upload:doctors + SQL functions |
Читает location_view |
| Frontend | API для внешних клиентов | Stimulus JS на Twig-страницах |
| Запись | /reservation/anonymous-reserve |
/api/anonymous-reserve + WrSDK |
6. Ключевые файлы
Backend
| Файл | Роль |
|---|---|
src/Controller/SpecialistController.php |
GET /specialist/schedule |
src/Service/Specialist/SpecialistService.php |
dispatch messenger |
src/MessageHandler/GetScheduleMessageHandler.php |
cache + MIS |
src/Service/ScheduleCache/ScheduleCacheService.php |
TTL 5 мин |
src/Service/Client/InfoclinicaClientService.php |
HTTP к MIS |
src/Command/UploadDoctorsCommand.php |
hourly sync nearestDate |
src/Entity/Schedule.php, Idoctor.php, Location.php |
модели |
scripts/cron.hourly.sh |
orchestration |
Cabinet
| Файл | Роль |
|---|---|
src/Controller/PublicAPIController.php |
GET /api/interval |
assets/controllers/checkSchedule_controller.js |
слоты на карточке |
assets/controllers/specialistView_controller.js |
модалка «Все даты» |
templates/specialist/_item.html.twig |
разметка карточки |
src/Controller/SpecialistController.php |
диапазон дат st/en |
src/Entity/LocationView.php |
nearestDate для фильтров |
7. Отладка
# Batch sync nearestDate
docker exec -u www-data php84 php bin/console upload:doctors 0 -v
docker exec pgsql psql -U sova_api -d sova_api -c "SELECT dcode, nearest_date, online_mode FROM idoctor LIMIT 10;"
# Статистика кэша слотов backend
docker exec -u www-data php84 php bin/console app:schedule:clear-cache --stats
# Прямой запрос backend API
curl "http://localhost:8081/specialist/schedule?st=20250525&en=20250601&dcode=XXX&filial=1&onlineMode=0"
# Cabinet proxy
curl "http://localhost:8082/api/interval?doctor=XXX&department=YYY&filial=1&startInterval=2025-05-25&endInterval=2025-06-01&onlineMode=0"
8. Известные нюансы
-
GetScheduleMessageHandlerвызываетgetSchedule($queryString, $isOnlineMode)с двумя аргументами, аInfoclinicaClientService::getSchedule()в репозитории принимает один — потенциальная runtime-ошибка; проверить на deploy-окружении. -
update_specialist()/update_location()критичны для hourly sync, но не в git — только в production PostgreSQL. -
app:schedule:clear-cacheне в cron — старые строки вscheduleкопятся, но не отдаются (TTL проверяется при чтении). -
Cabinet и backend не связаны по слотам — изменения в backend cache не влияют на cabinet.
-
Legacy-команды cabinet (
app:Infoclinica schedules/intervals) ссылаются на удалённые таблицы — не использовать для текущего UI.
9. Ментальная модель
| Задача | Механизм | Хранение | Триггер |
|---|---|---|---|
| Показать слоты на сайте | Live-запрос в MIS | Не хранится (HTTP cache) | Открытие карточки врача |
| API слотов для внешних клиентов | Live + DB cache 5 мин | schedule |
GET /specialist/schedule |
| Сортировка «по ближайшей записи» | Batch sync | idoctor → location → views |
Hourly cron |
| Привязка врача к MIS | Admin + sync | location, specialist.dcodes |
Admin panel + cron |
| Запись на приём | Push в MIS | record (локально) |
POST anonymous-reserve |