feat: migrate to VitePress from monorepo docs, add test-contour section
This commit is contained in:
@@ -0,0 +1,451 @@
|
||||
---
|
||||
title: Расписание врачей — синхронизация и отображение (Backend + Cabinet)
|
||||
---
|
||||
|
||||
# Расписание врачей: синхронизация и отображение
|
||||
|
||||
> Полный разбор того, **как в проекте работает расписание врачей**: от Infoclinica MIS до карточки на сайте. Охватывает `apps/backend`, `apps/cabinet` и роль adminPanel.
|
||||
>
|
||||
> Связанные документы:
|
||||
> - [2.2 Расписание и кеш (Backend)](../apps/backend-scenarios/schedule-cache.md)
|
||||
> - [2.3 GetScheduleMessage](../apps/backend-scenarios/schedule-messenger.md)
|
||||
> - [4.1 Синхронизация врачей (Idoctor)](../apps/backend-scenarios/sync-doctors-reviews.md)
|
||||
> - [Backend: внешние сервисы](../infrastructure/backend-external-services.md) (MIS / Widget API)
|
||||
> - [Cabinet: обзор](./cabinet.md)
|
||||
|
||||
---
|
||||
|
||||
## 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. Общая архитектура
|
||||
|
||||
```mermaid
|
||||
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 и ключа кэша:
|
||||
|
||||
```php
|
||||
// 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](../apps/backend-scenarios/schedule-messenger.md), [schedule-cache.md](../apps/backend-scenarios/schedule-cache.md).
|
||||
|
||||
### 3.3. Запрос в MIS
|
||||
|
||||
`InfoclinicaClientService` вызывает:
|
||||
|
||||
```
|
||||
GET {MIS_URL}/api/reservation/intervals?st=...&en=...&dcode=...&onlineMode=...&filialId=...
|
||||
```
|
||||
|
||||
Ответ нормализуется в структуру:
|
||||
|
||||
```json
|
||||
{
|
||||
"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` — онлайн.
|
||||
|
||||
**Алгоритм:**
|
||||
|
||||
1. Читает активные отделения из `department`.
|
||||
2. Для каждого отделения вызывает MIS:
|
||||
```
|
||||
GET /specialists/doctors?departments={did}&onlineMode={0|1}&firstrow=&lastrow=
|
||||
```
|
||||
чанками по 300 записей.
|
||||
3. Upsert в `idoctor` по ключу `dcode + department + onlineMode`.
|
||||
4. Поля: `dcode`, `name`, `department`, `filial`, **`nearestDate`**, `onlineMode`.
|
||||
|
||||
Подробнее: [sync-doctors-reviews.md](../apps/backend-scenarios/sync-doctors-reviews.md).
|
||||
|
||||
### 3.6. Hourly cron
|
||||
|
||||
Файл `scripts/cron.hourly.sh`:
|
||||
|
||||
```bash
|
||||
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();"
|
||||
```
|
||||
|
||||
```mermaid
|
||||
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](../apps/backend-scenarios/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`):
|
||||
|
||||
1. Читает `dcode`, `filial`, `department`, `onlineMode` из DOM / селекта клиники.
|
||||
2. Вызывает `GET /api/interval`.
|
||||
3. Рендерит до **6 свободных слотов** на карточке.
|
||||
4. Кнопка «Все даты» открывает модалку (`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)
|
||||
|
||||
1. Клик по слоту → `record.js` → форма записи.
|
||||
2. **Авторизованный пользователь:** `webSDK.scheduleRecReserve()` напрямую в Infoclinica SDK (`widget.sovamed.ru`).
|
||||
3. **Анонимный:** `POST /api/anonymous-reserve` → MIS `/api/reservation/anonymous-reserve`.
|
||||
|
||||
### 4.7. Legacy: старые CLI-команды cabinet
|
||||
|
||||
В `apps/cabinet/src/Command/` есть **устаревший** путь полной синхронизации в локальные таблицы:
|
||||
|
||||
```bash
|
||||
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. Отладка
|
||||
|
||||
```bash
|
||||
# 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. Известные нюансы
|
||||
|
||||
1. **`GetScheduleMessageHandler`** вызывает `getSchedule($queryString, $isOnlineMode)` с двумя аргументами, а `InfoclinicaClientService::getSchedule()` в репозитории принимает один — потенциальная runtime-ошибка; проверить на deploy-окружении.
|
||||
|
||||
2. **`update_specialist()` / `update_location()`** критичны для hourly sync, но **не в git** — только в production PostgreSQL.
|
||||
|
||||
3. **`app:schedule:clear-cache`** не в cron — старые строки в `schedule` копятся, но не отдаются (TTL проверяется при чтении).
|
||||
|
||||
4. **Cabinet и backend не связаны по слотам** — изменения в backend cache не влияют на cabinet.
|
||||
|
||||
5. **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 |
|
||||
Reference in New Issue
Block a user