feat: migrate to VitePress from monorepo docs, add test-contour section
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: Анонимная запись на приём через MIS (Backend)
|
||||
---
|
||||
|
||||
# Сценарий 3.1: Анонимная запись (**Anonymous Reserve**)
|
||||
|
||||
## Бизнес-цель
|
||||
|
||||
Посетитель сайта может **записаться на приём**, указав контактные данные и выбранный слот, **без обязательной регистрации** в Laravel/Next-сенсах личного кабинета: запись фиксируется в **МИС (Инфоклиника)**.
|
||||
|
||||
## Точки входа
|
||||
|
||||
| Тип | Метод + URL | Класс |
|
||||
| --- | --- | --- |
|
||||
| HTTP | `POST /reservation/anonymous-reserve` | `InfoclinicaController::bookingAnonymous` |
|
||||
|
||||
Внутренне вызывается Messenger-сообщение **`GetAnonymousReserveRequestMessage`** (транспорт `sync` по `messenger.yaml`).
|
||||
|
||||
## Входной контракт (DTO)
|
||||
|
||||
`App\Dto\AnonymousReserveRequestDto` валидирует:
|
||||
|
||||
- ФИО, email, телефон в формате `+7(999)999-99-99`;
|
||||
- `workDate` — 8 цифр `YYYYMMDD`;
|
||||
- `time` — интервал `HH:MM-HH:MM`;
|
||||
- `filial`, `schedident`, `rnum`, `specialist` (dcode), `accept`, `captcha` и т.д.
|
||||
|
||||
Метод `toArray()` формирует тело для MIS:
|
||||
|
||||
- поле `reserve` — **JSON-строка** с деталями слота (`date`, `st`, `en`, `services`, `filial`, `timezone`, `schedident`, `rnum`, `dcode`).
|
||||
|
||||
Пример упрощённой структуры тела:
|
||||
|
||||
```json
|
||||
{
|
||||
"accept": true,
|
||||
"fio": "Иванов Иван",
|
||||
"captcha": "...",
|
||||
"email": "user@example.com",
|
||||
"phone": "+7(903)123-45-67",
|
||||
"reserve": "{\"date\":\"20260520\",\"st\":\"10:00\",\"en\":\"10:30\", ... }"
|
||||
}
|
||||
```
|
||||
|
||||
## Пошаговый алгоритм
|
||||
|
||||
1. `InfoclinicaController::bookingAnonymous` десериализует JSON в `AnonymousReserveRequestDto`, валидирует.
|
||||
2. `SpecialistService::createAnonymousReserve($dto)` создаёт `GetAnonymousReserveRequestMessage` и диспатчит через `MessageBusInterface` (sync).
|
||||
3. `GetAnonymousReserveRequestMessageHandler::__invoke`:
|
||||
- логирует старт;
|
||||
- вызывает `InfoclinicaClientService::anonymousReserve($dto)`;
|
||||
- выполняет `POST` на путь **`/api/reservation/anonymous-reserve`** MIS с `json_encode($dto->toArray())`;
|
||||
- возвращает массив ответа `toArray()` HTTP-клиента Symfony;
|
||||
- при `HttpExceptionInterface` — лог и массив с `status_code`, телом ответа MIS.
|
||||
|
||||
4. Контроллер делает `$this->json($reserve, $reserve['status_code'] ?? 200)` — **если в ответе есть числовой `status_code`, он подставляется как HTTP-код ответа API**.
|
||||
|
||||
## Создание строки `Record` в PostgreSQL
|
||||
|
||||
В текущем дереве `apps/backend/src` **не найдено** кода `persist(new Record(...))` или использования `RecordRepository` в связке с этим эндпоинтом. Сущность `Record` и таблица описаны в модели данных, но **локальное сохранение факта записи вместе с анонимным reserve в этом HTTP-сценарии не реализовано** (или перенесено в другой сервис).
|
||||
|
||||
## Mermaid
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Клиент
|
||||
participant IC as InfoclinicaController
|
||||
participant SS as SpecialistService
|
||||
participant BUS as MessageBus
|
||||
participant H as GetAnonymousReserveRequestMessageHandler
|
||||
participant CL as InfoclinicaClientService
|
||||
participant MIS as Инфоклиника API
|
||||
|
||||
C->>IC: POST /reservation/anonymous-reserve
|
||||
IC->>SS: createAnonymousReserve(dto)
|
||||
SS->>BUS: dispatch(GetAnonymousReserveRequestMessage)
|
||||
BUS->>H: __invoke
|
||||
H->>CL: anonymousReserve(dto)
|
||||
CL->>MIS: POST /api/reservation/anonymous-reserve
|
||||
MIS-->>CL: JSON
|
||||
CL-->>H: массив
|
||||
H-->>IC: результат
|
||||
IC-->>C: HTTP код из status_code или 200
|
||||
```
|
||||
|
||||
## Внешние зависимости
|
||||
|
||||
| Система | Роль |
|
||||
| --- | --- |
|
||||
| Инфоклиника | создание записи |
|
||||
| PostgreSQL | **не задействуется** для `Record` в этом сценарии (по текущему коду) |
|
||||
|
||||
## Обработка ошибок и edge cases
|
||||
|
||||
- **`InvalidArgumentException` в контроллере** — `400` с текстом валидации.
|
||||
- **Ошибки десериализации JSON в DTO** — должны обрабатываться обработчиком исключений приложения (в контроллере явного try/catch на `ExceptionInterface` нет).
|
||||
- **Ответ MIS с ошибкой**: может прийти как `200` с полем `status_code` внутри JSON — клиентский код фронта должен учитывать оба уровня (HTTP и вложенный).
|
||||
|
||||
## Ссылки на классы
|
||||
|
||||
- `apps/backend/src/Controller/InfoclinicaController.php`
|
||||
- `apps/backend/src/Service/Specialist/SpecialistService.php`
|
||||
- `apps/backend/src/Message/GetAnonymousReserveRequestMessage.php`
|
||||
- `apps/backend/src/MessageHandler/GetAnonymousReserveRequestMessageHandler.php`
|
||||
- `apps/backend/src/Service/Client/InfoclinicaClientService.php`
|
||||
- `apps/backend/src/Dto/AnonymousReserveRequestDto.php`
|
||||
|
||||
См. [sms-record.md](./sms-record.md), [backend-ddd.md](../backend-ddd.md).
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: Авторизация по UID и pcode (Backend)
|
||||
---
|
||||
|
||||
# Сценарий 1.2: Авторизация по **UID** и **pcode**
|
||||
|
||||
## Бизнес-цель
|
||||
|
||||
Часть пользователей приходит из медицинской системы (**Инфоклиника / MIS**): у пациента уже есть **числовой идентификатор** в сторонней системе. Сайт должен позволить:
|
||||
|
||||
- связать этот **UID** с учётной записью в API (`App\Entity\User::uid`);
|
||||
- войти **по email+паролю**, если пользователь уже создан с тем же `uid`;
|
||||
- войти **по pcode + дате рождения**, если личный кабинет строится на «медицинском» коде без ввода email на первом шаге.
|
||||
|
||||
## Что такое `uid` и `pcode` в коде
|
||||
|
||||
| Термин в API | Где в коде | Смысл |
|
||||
| --- | --- | --- |
|
||||
| `uid` | поле `User::$uid`, уникальное `int` | Идентификатор пациента/пользователя из внешней системы (в т.ч. MIS). |
|
||||
| `pcode` в `POST /user/auth-by-pcode` | мапится в `UserUidAuthDto::$uid` | Тот же **числовой идентификатор**, что и `uid`; название «pcode» — контракт фронта/legacy. |
|
||||
|
||||
Откуда берутся значения **на практике**: из МИС/личного кабинета после идентификации пациента (конкретный HTTP-запрос к Инфоклинике **в этом приложении** для «получить pcode» в сценарии логина не вызывается — клиент передаёт уже известные `uid`/`pcode` и дату рождения).
|
||||
|
||||
## Точки входа
|
||||
|
||||
| Тип | Метод + URL | Класс |
|
||||
| --- | --- | --- |
|
||||
| HTTP | `POST /user/auth` | `UserController::auth` |
|
||||
| HTTP | `POST /user/auth-by-pcode` | `UserController::authByPcode` |
|
||||
|
||||
## Пошаговый алгоритм — `POST /user/auth`
|
||||
|
||||
1. Тело: `uid`, `regionId`, `email`, `password`, опционально `bdate` → `UserAuthDto`, валидация.
|
||||
2. `AuthenticationService::jwtAuth(dto)`:
|
||||
- `UserRepository::findOneBy(['uid' => $dto->uid])`;
|
||||
- если пользователь есть — проверка пароля **введённого** `password` через `passwordHasher`;
|
||||
- если пользователя **нет** — возвращается `user: null` (флаг пароля не применяется).
|
||||
3. Если пароль не совпал при существующем пользователе — `400`, как при обычном логине.
|
||||
4. Если пользователя не было — `RegistrationService::create(dto)`:
|
||||
- `setEmail(md5($dto->email))` (в БД снова **не** хранится открытый email);
|
||||
- `setUid`, `setRegionId`, роли `ROLE_USER`, `birthDate` из `bdate` (формат `Ymd`), хэш пароля.
|
||||
5. `updateLoggedIn`, `flush`, выдача JWT через `JWTTokenManagerInterface::create`.
|
||||
|
||||
## Пошаговый алгоритм — `POST /user/auth-by-pcode`
|
||||
|
||||
1. Тело: `pcode` (кладётся в `UserUidAuthDto::$uid`), `birthDate` или `bdate`.
|
||||
2. Валидация DTO, разбор даты (`Ymd` или `Y-m-d`).
|
||||
3. `UserRepository::findOneByUidAndBirthDate($uid, $birthDate)` — сравнение даты **по диапазону суток**.
|
||||
4. Если не найден — `RegistrationService::createByUidAndBirthDate`:
|
||||
- `email = md5((string) uid)` — синтетический идентификатор для колонки `email` / JWT `username`;
|
||||
- регион по умолчанию `1` (аргумент сервиса);
|
||||
- пароль по умолчанию: `hash( md5(uid . birthDateRaw) )` где первая часть — конкатенация строк из DTO перед нормализацией даты в сущности.
|
||||
5. `updateLoggedIn`, `flush`, JWT в ответе.
|
||||
|
||||
## Mermaid
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[POST /user/auth или /user/auth-by-pcode] --> V{Валидация DTO}
|
||||
V -->|ошибка| E400[400 errors]
|
||||
V -->|ок| B{Какой маршрут?}
|
||||
B -->|auth| C[jwtAuth по uid]
|
||||
C --> D{User найден?}
|
||||
D -->|да| P[проверка password]
|
||||
P -->|fail| E400b[400 неверный пароль]
|
||||
P -->|ok| T[JWT + loggedIn]
|
||||
D -->|нет| R[RegistrationService.create]
|
||||
R --> T
|
||||
|
||||
B -->|auth-by-pcode| F[findOneByUidAndBirthDate]
|
||||
F -->|есть| T
|
||||
F -->|нет| G[createByUidAndBirthDate]
|
||||
G -->|исключение| E500[500]
|
||||
G -->|ok| T
|
||||
```
|
||||
|
||||
## Внешние зависимости
|
||||
|
||||
| Система | Участие |
|
||||
| --- | --- |
|
||||
| PostgreSQL | `users` |
|
||||
| Инфоклиника | **не вызывается напрямую** в этих методах; идентификатор приходит от клиента |
|
||||
| JWT (Lexik) | выдача токена |
|
||||
|
||||
## Обработка ошибок и edge cases
|
||||
|
||||
- **Неверный формат даты** в `auth-by-pcode` — `400` с подсказкой по форматам.
|
||||
- **Ошибка при создании пользователя** — `500` с текстом исключения (может раскрывать внутренние детали — вопрос hardening).
|
||||
- **Коллизии `uid`**: на уровне сущности `User` есть ограничение уникальности `uid`; при конфликте БД вернёт ошибку при `flush` (в этом контроллере не разобрана отдельно).
|
||||
|
||||
## Ссылки на классы
|
||||
|
||||
- `apps/backend/src/Controller/UserController.php` (`auth`, `authByPcode`)
|
||||
- `apps/backend/src/Service/User/AuthenticationService.php` (`jwtAuth`)
|
||||
- `apps/backend/src/Service/User/RegistrationService.php` (`create`, `createByUidAndBirthDate`)
|
||||
- `apps/backend/src/Dto/UserAuthDto.php`, `UserUidAuthDto.php`
|
||||
- `apps/backend/src/Repository/UserRepository.php` (`findOneByUidAndBirthDate`)
|
||||
|
||||
См. [backend-ddd.md](../backend-ddd.md) (контекст Identity).
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Создание лида Calltouch (Backend)
|
||||
---
|
||||
|
||||
# Сценарий 4.2: Создание лида **Calltouch**
|
||||
|
||||
## Бизнес-цель
|
||||
|
||||
Маркетинг фиксирует обращения пользователей как **лиды** в системе **Calltouch** для сквозной аналитики и колл-центра.
|
||||
|
||||
## Точки входа
|
||||
|
||||
| Тип | Метод + URL | Класс |
|
||||
| --- | --- | --- |
|
||||
| HTTP | `POST /calltouch/create-lead` | `CalltouchController::createLead` |
|
||||
|
||||
**Ограничение доступа:** `#[IsGranted('ROLE_ADMIN')]` на уровне класса контроллера — вызов только для административной роли API.
|
||||
|
||||
## Реализация HTTP vs клиент
|
||||
|
||||
1. Тело (form/json) мапится в `CalltouchCreateRequestDto`, проходит `Validator`.
|
||||
2. **Фактический вызов** `CalltouchClientService::requestCreate($dto)` в коде **закомментирован**; ответ клиента возвращён не будет.
|
||||
3. Контроллер отдаёт `200` с полем `request` (сырые данные из `$request->request->all()`).
|
||||
|
||||
То есть **интеграция подготовлена, но в текущей ветке кода не активна** до раскомментирования строки.
|
||||
|
||||
## Как устроен `CalltouchClientService` (целевое поведение)
|
||||
|
||||
1. `configureHeaders($dto->regionId)` выбирает пару `siteId` + токен из строки ENV, разобранной в конструкторе (`param` формат `region:siteId:token,...`).
|
||||
2. Формируется `POST` на путь **`/lead-service/v1/api/request/create`** заголовками `Access-Token`, `SiteId`.
|
||||
3. Тело: `json_encode(['requests' => $dto->toArray()])`.
|
||||
4. Возвращается `data` из JSON-ответа Calltouch.
|
||||
|
||||
## Mermaid (фактическое поведение в коде)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Админ-клиент
|
||||
participant CC as CalltouchController
|
||||
participant V as Validator
|
||||
A->>CC: POST /calltouch/create-lead
|
||||
CC->>V: validate(CalltouchCreateRequestDto)
|
||||
CC-->>A: 200 JSON (поле request)
|
||||
```
|
||||
|
||||
После раскомментирования `CalltouchClientService::requestCreate` к схеме добавятся шаги HTTP `POST` к API Calltouch с заголовками `Access-Token` и `SiteId`.
|
||||
|
||||
## Внешние зависимости
|
||||
|
||||
| Система | Статус |
|
||||
| --- | --- |
|
||||
| Calltouch API | клиент реализован, HTTP в контроллере отключён |
|
||||
| PostgreSQL | не используется в сценарии лида напрямую |
|
||||
|
||||
## Обработка ошибок и edge cases
|
||||
|
||||
- **Нет конфигурации региона** — `configureHeaders` бросает `InvalidArgumentException` (на будущее при включении клиента).
|
||||
- **Опечатка в конструкторе** `CalltouchClientService`: два присваивания `$this->baseUrl` подряд — стоит перепроверить при включении интеграции.
|
||||
|
||||
## Ссылки на классы
|
||||
|
||||
- `apps/backend/src/Controller/CalltouchController.php`
|
||||
- `apps/backend/src/Service/Client/CalltouchClientService.php`
|
||||
- `apps/backend/src/Dto/CalltouchCreateRequestDto.php`
|
||||
- `apps/backend/src/Service/Client/Interfaces/CalltouchClientServiceInterface.php`
|
||||
|
||||
См. [backend-ddd.md](../backend-ddd.md).
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Смена региона пользователя (Backend)
|
||||
---
|
||||
|
||||
# Сценарий 1.3: Смена региона пользователя
|
||||
|
||||
## Бизнес-цель
|
||||
|
||||
Пользователь может переключать **регион** (филиал сети / географическая зона), чтобы видеть контент и услуги, релевантные выбранной территории. В API регион хранится в **`User::$regionId`** и участвует в выдаче данных на фронте.
|
||||
|
||||
## Точки входа
|
||||
|
||||
| Тип | Метод + URL | Класс |
|
||||
| --- | --- | --- |
|
||||
| HTTP | `PUT /user/change-region` | `App\Controller\UserController::changeRegion` |
|
||||
|
||||
Требуется аутентификация: `#[IsGranted('ROLE_USER')]` — клиент передаёт **JWT** в заголовке.
|
||||
|
||||
CLI и Messenger **не используются**.
|
||||
|
||||
## Пошаговый алгоритм (flow)
|
||||
|
||||
1. Клиент отправляет JSON с полем `regionId`.
|
||||
2. Данные попадают в `App\Dto\RegionDto`; Symfony Validator проверяет ограничения DTO.
|
||||
3. Вызывается `App\Service\User\UserProfileService::updateRegion($dto)`:
|
||||
- `JWTDecoderService::getUser()` — из текущего токена извлекается **username** и по нему загружается `User` из БД;
|
||||
- на найденного пользователя выставляется `setRegionId($dto->regionId)`;
|
||||
- `EntityManager::persist` + `flush`.
|
||||
4. Контроллер сериализует **тот же** `User::toArray()` и возвращает `200` с `user` и `successful: true`.
|
||||
|
||||
## Mermaid
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Клиент (JWT)
|
||||
participant UC as UserController
|
||||
participant V as Validator
|
||||
participant UPS as UserProfileService
|
||||
participant JWT as JWTDecoderService
|
||||
participant UR as UserRepository
|
||||
participant EM as EntityManager
|
||||
|
||||
C->>UC: PUT /user/change-region {regionId}
|
||||
UC->>V: validate(RegionDto)
|
||||
UC->>UPS: updateRegion(dto)
|
||||
UPS->>JWT: getUser()
|
||||
JWT->>UR: find by token username
|
||||
UR-->>JWT: User
|
||||
UPS->>EM: setRegionId + flush
|
||||
UPS-->>UC: User
|
||||
UC-->>C: 200 { user, successful }
|
||||
```
|
||||
|
||||
## Внешние зависимости
|
||||
|
||||
| Система | Участие |
|
||||
| --- | --- |
|
||||
| PostgreSQL | обновление строки в `users` |
|
||||
| JWT | идентификация текущего пользователя |
|
||||
|
||||
## Обработка ошибок и edge cases
|
||||
|
||||
- **Невалидный `regionId`** — `400` с телом `errors` из Validator.
|
||||
- **`getUser()` вернул null** — в текущей реализации сервиса нет явной проверки; при `ROLE_USER` обычно токен уже валиден, но при рассинхроне payload/БД возможна ошибка уровня type error или `flush` — сценарий стоит учитывать при доработках.
|
||||
- **Справочник регионов**: этот эндпоинт **не проверяет**, существует ли `regionId` в таблице регионов — допустим любой int, прошедший валидацию DTO.
|
||||
|
||||
## Ссылки на классы
|
||||
|
||||
- `apps/backend/src/Controller/UserController.php` (`changeRegion`)
|
||||
- `apps/backend/src/Service/User/UserProfileService.php`
|
||||
- `apps/backend/src/Dto/RegionDto.php`
|
||||
- `apps/backend/src/Service/DecoderJWT/JWTDecoderService.php`
|
||||
|
||||
См. [login-jwt.md](./login-jwt.md) и [backend-ddd.md](../backend-ddd.md).
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: Бизнес-сценарии Backend API — оглавление
|
||||
---
|
||||
|
||||
# Бизнес-сценарии Backend API
|
||||
|
||||
Подробные потоки данных (**data flow**) от HTTP/CLI/Messenger до БД и внешних систем. Формат согласован с [архитектурой модулей](./../backend-architecture.md), [DDD-картой](./../backend-ddd.md) и [CRUD контента](./../backend-content-crud.md).
|
||||
|
||||
## Блок 1. Идентичность и профиль
|
||||
|
||||
| № | Сценарий | Файл |
|
||||
| --- | --- | --- |
|
||||
| 1.1 | Логин и JWT | [login-jwt.md](./login-jwt.md) |
|
||||
| 1.2 | UID / pcode, привязка к `User` | [auth-uid-pcode.md](./auth-uid-pcode.md) |
|
||||
| 1.3 | Смена региона | [change-region.md](./change-region.md) |
|
||||
|
||||
## Блок 2. Врачи, расписание, локации
|
||||
|
||||
| № | Сценарий | Файл |
|
||||
| --- | --- | --- |
|
||||
| 2.1 | Карточка врача и локации | [specialist-card-locations.md](./specialist-card-locations.md) |
|
||||
| 2.2 | Расписание и кеш (таблица `schedule`) | [schedule-cache.md](./schedule-cache.md) |
|
||||
| 2.3 | `GetScheduleMessage` и обработчик | [schedule-messenger.md](./schedule-messenger.md) |
|
||||
| 2.4 | **Полный мануал: расписание Backend + Cabinet** | [doctor-schedule-sync.md](../doctor-schedule-sync.md) |
|
||||
|
||||
## Блок 3. Запись на приём
|
||||
|
||||
| № | Сценарий | Файл |
|
||||
| --- | --- | --- |
|
||||
| 3.1 | Анонимная запись (MIS) | [anonymous-reserve.md](./anonymous-reserve.md) |
|
||||
| 3.2 | SMS, `Record`, `AlertSms` | [sms-record.md](./sms-record.md) |
|
||||
| 3.3 | Киоск `clvisitsovacheckpass` | [kiosk-checkpass.md](./kiosk-checkpass.md) |
|
||||
|
||||
## Блок 4. Синхронизация и интеграции
|
||||
|
||||
| № | Сценарий | Файл |
|
||||
| --- | --- | --- |
|
||||
| 4.1 | Врачи (Infoclinica → `Idoctor`), Bitrix, отзывы | [sync-doctors-reviews.md](./sync-doctors-reviews.md) |
|
||||
| 4.2 | Лид Calltouch | [calltouch-lead.md](./calltouch-lead.md) |
|
||||
| 4.3 | XML-фид Яндекса | [xml-yandex-feed.md](./xml-yandex-feed.md) |
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
title: Отметка киоска clvisitsovacheckpass (Backend)
|
||||
---
|
||||
|
||||
# Сценарий 3.3: Проверка / отметка киоска (`clvisitsovacheckpass`)
|
||||
|
||||
## Бизнес-цель
|
||||
|
||||
В филиале может стоять **киоск** самообслуживания. Когда пациент авторизован в приложении, backend фиксирует факт «проверки прохода» для пары **пациент (`pcode`/`uid`) + филиал**, чтобы киоск знал, можно ли продолжить сценарий (метод возвращает булев признак `isResult()` у сущности `MarkKiosk`).
|
||||
|
||||
## Точки входа
|
||||
|
||||
| Тип | Метод + URL | Класс |
|
||||
| --- | --- | --- |
|
||||
| HTTP | `GET /infoclinica/clvisitsovacheckpass/{filial}` | `InfoclinicaController::clvisitsovacheckpass` |
|
||||
|
||||
Доступ: `#[IsGranted('ROLE_USER')]` — нужен JWT. `filial` — целочисленный идентификатор из URL.
|
||||
|
||||
## Что такое `pcode` здесь
|
||||
|
||||
Внутри метода берётся **текущий пользователь** через `JWTDecoderService::getUser()`, далее `$user->getUid()` трактуется как **`pcode` пациента** для записи в `MarkKiosk`. То есть это **тот же числовой uid**, что хранится в `users.uid` после сценариев [auth-uid-pcode.md](./auth-uid-pcode.md).
|
||||
|
||||
## Пошаговый алгоритм
|
||||
|
||||
1. Проверка JWT; если пользователь не найден — `401` с телом `{"error":"Пользователь не найден"}`.
|
||||
2. `$pcode = $user->getUid()`.
|
||||
3. Репозиторий `MarkKiosk` ищет запись по паре `['pcode' => $pcode, 'filial' => $filial]`.
|
||||
4. Если записи нет — создаётся `new MarkKiosk()` с `pcode`, `filial`, `createdAt`/`modifyAt` (текущее время), `persist` + `flush`.
|
||||
5. Повторная выборка той же сущности (как в коде после создания).
|
||||
6. Ответ API: JSON c булевым полем из `MarkKiosk::isResult()` — для **новой** строки поле `result` в сущности по умолчанию **`null`** (в setter при создании в контроллере `result` не выставляется).
|
||||
|
||||
## Mermaid
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant K as Киоск / клиент
|
||||
participant IC as InfoclinicaController
|
||||
participant JWT as JWTDecoderService
|
||||
participant EM as EntityManager
|
||||
participant MK as MarkKiosk
|
||||
|
||||
K->>IC: GET .../clvisitsovacheckpass/{filial} + JWT
|
||||
IC->>JWT: getUser()
|
||||
alt нет пользователя
|
||||
IC-->>K: 401
|
||||
else ok
|
||||
IC->>EM: find MarkKiosk pcode+filial
|
||||
alt нет строки
|
||||
IC->>EM: persist новый MarkKiosk
|
||||
end
|
||||
EM-->>IC: MarkKiosk
|
||||
IC-->>K: {result: isResult()}
|
||||
end
|
||||
```
|
||||
|
||||
## Внешние зависимости
|
||||
|
||||
| Система | Роль |
|
||||
| --- | --- |
|
||||
| PostgreSQL | таблица `mark_kiosk` (имя по маппингу Doctrine) |
|
||||
| Инфоклиника | **не вызывается** в этом методе; имя маршрута исторически связано с MIS |
|
||||
|
||||
## Обработка ошибок и edge cases
|
||||
|
||||
- **Нет JWT / неверный токен** — ответ средства Symfony Security (не разобран в контроллере).
|
||||
- **Повторный вызов** — запись уже есть; логика `isResult()` определяет, что отдать киоску при повторе (см. `Entity/MarkKiosk.php`).
|
||||
- **У пользователя нет `uid`** — маловероятно по модели, но при `0` возможны коллизии — вопрос целостности данных.
|
||||
|
||||
## Ссылки на классы
|
||||
|
||||
- `apps/backend/src/Controller/InfoclinicaController.php`
|
||||
- `apps/backend/src/Entity/MarkKiosk.php`
|
||||
- `apps/backend/src/Service/DecoderJWT/JWTDecoderService.php`
|
||||
|
||||
Карта домена: [backend-ddd.md](../backend-ddd.md).
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: Логин по email и выдача JWT (Backend)
|
||||
---
|
||||
|
||||
# Сценарий 1.1: Логин и JWT
|
||||
|
||||
## Бизнес-цель
|
||||
|
||||
Пользователь сайта или приложения входит по **email и паролю**. Система проверяет учётные данные, обновляет отметку последнего входа и выдаёт **JWT**, чтобы дальнейшие запросы к API выполнялись от имени этого пользователя без хранения сессии на сервере (stateless API).
|
||||
|
||||
## Точки входа
|
||||
|
||||
| Тип | Метод + URL | Класс |
|
||||
| --- | --- | --- |
|
||||
| HTTP | `POST /user/login` | `App\Controller\UserController::login` |
|
||||
| HTTP | `GET /user/logout` | `App\Controller\UserController::logout` (ответ-заглушка; инвалидация токена на сервере не показана в коде) |
|
||||
| HTTP | `GET /user/` | `App\Controller\UserController::index` — текущий пользователь по JWT (`#[IsGranted('ROLE_USER')]`) |
|
||||
|
||||
Асинхронные сообщения и CLI для этого сценария **не используются**.
|
||||
|
||||
## Пошаговый алгоритм (flow)
|
||||
|
||||
1. Клиент отправляет JSON с полями `username` (фактически email) и `password` (см. OpenAPI в контроллере).
|
||||
2. `UserController::login` вручную парсит тело; при отсутствии полей отвечает `400` с текстом `Missing credentials`.
|
||||
3. Значения кладутся в `App\Dto\UserLoginDto`; срабатывает Symfony Validator по атрибутам DTO.
|
||||
4. Вызывается `App\Service\User\AuthenticationService::jsonAuth($dto)`:
|
||||
- в `App\Repository\UserRepository` выполняется поиск `User` по **`email = md5(введённый email)`** (в БД в колонке `users.email` хранится **хэш**, а не открытый email);
|
||||
- пароль проверяется через `UserPasswordHasherInterface::isPasswordValid`.
|
||||
5. При неверной паре или отсутствии пользователя контроллер возвращает `400` с сообщением о неверных учётных данных.
|
||||
6. При успехе вызывается `$user->updateLoggedIn()`, `EntityManager::flush()` — в БД обновляется поле `loggedIn`.
|
||||
7. Lexik JWT: `JWTTokenManagerInterface::create($user)` формирует токен; в ответ уходит JSON: `successful`, `token`, `user` (массив из `User::toArray()` — `uid`, `bdate`, `roles`, `regionId`, `loggedIn`).
|
||||
|
||||
## Mermaid
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Клиент
|
||||
participant UC as UserController
|
||||
participant V as Validator
|
||||
participant AS as AuthenticationService
|
||||
participant UR as UserRepository
|
||||
participant EM as EntityManager
|
||||
participant JWT as JWTTokenManagerInterface
|
||||
|
||||
C->>UC: POST /user/login {username, password}
|
||||
UC->>V: validate(UserLoginDto)
|
||||
V-->>UC: ок / ошибки
|
||||
UC->>AS: jsonAuth(dto)
|
||||
AS->>UR: findOneBy email = md5(dto.email)
|
||||
UR-->>AS: User | null
|
||||
AS-->>UC: {user, isPasswordValid}
|
||||
alt неверный пароль или нет пользователя
|
||||
UC-->>C: 400
|
||||
else успех
|
||||
UC->>EM: flush (updateLoggedIn)
|
||||
UC->>JWT: create(User)
|
||||
UC-->>C: 200 {token, user}
|
||||
end
|
||||
```
|
||||
|
||||
## Внешние зависимости
|
||||
|
||||
| Система | Участие |
|
||||
| --- | --- |
|
||||
| PostgreSQL | таблица `users` (`App\Entity\User`) |
|
||||
| Lexik JWT | генерация и последующая валидация токена |
|
||||
| Bitrix / Инфоклиника / Redis / SMS | **не задействованы** в этом сценарии |
|
||||
|
||||
## Обработка ошибок и edge cases
|
||||
|
||||
- **Нет полей credentials** — `400`, `Missing credentials`.
|
||||
- **Ошибки валидации DTO** — `400`, `successful: false`, `errors` (строка нарушений).
|
||||
- **Неверный email/пароль** — единый ответ `400` с текстом «Не правильное имя пользователя или пароль».
|
||||
- **Согласованность с JWT**: `App\Service\DecoderJWT\JWTDecoderService::getUser()` восстанавливает пользователя по `username` из payload и полю `email` в БД — это тот же «логин», что хранится в `User` после регистрационных сценариев (часто `md5(...)`).
|
||||
|
||||
## Ссылки на классы
|
||||
|
||||
- `apps/backend/src/Controller/UserController.php`
|
||||
- `apps/backend/src/Service/User/AuthenticationService.php`
|
||||
- `apps/backend/src/Dto/UserLoginDto.php`
|
||||
- `apps/backend/src/Entity/User.php`
|
||||
- `apps/backend/src/Repository/UserRepository.php`
|
||||
- Конфигурация security/JWT: `apps/backend/config/packages/security.yaml`, `lexik_jwt_authentication.yaml`
|
||||
|
||||
См. также [backend-ddd.md](../backend-ddd.md).
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
title: Расписание врача и кеш слотов (Backend)
|
||||
---
|
||||
|
||||
# Сценарий 2.2: Расписание и «кеш» слотов
|
||||
|
||||
## Бизнес-цель
|
||||
|
||||
Пациенту показывают **свободные интервалы** записи к врачу в конкретном филиале и режиме (очный / онлайн). Чтобы не дёргать MIS на каждый запрос, backend хранит **недавно полученное расписание** и отдаёт его при повторных обращениях с теми же параметрами.
|
||||
|
||||
**Важно для архитектуры:** несмотря на название сервиса, **данные кеша живут в PostgreSQL** (таблица сущности `Schedule`), а не в Redis. Redis в этом сценарии **не используется** (см. `ScheduleCacheService`).
|
||||
|
||||
## Точки входа
|
||||
|
||||
| Тип | Метод + URL | Класс |
|
||||
| --- | --- | --- |
|
||||
| HTTP | `GET /specialist/schedule?...` | `SpecialistController::specialistSchedule` |
|
||||
| CLI | `app:schedule:clear-cache` | `ClearScheduleCacheCommand` (очистка старых строк) |
|
||||
|
||||
Фактический вызов MIS выполняется внутри `GetScheduleMessageHandler` (см. [schedule-messenger.md](./schedule-messenger.md)).
|
||||
|
||||
## Параметры запроса расписания
|
||||
|
||||
Используется `App\Dto\ScheduleDto`:
|
||||
|
||||
- `st`, `en` — границы времени (целые);
|
||||
- `dcode` — код врача;
|
||||
- `filial` — филиал (в query строка строится как `filialId`);
|
||||
- `onlineMode` — признак онлайн-расписания.
|
||||
|
||||
`ScheduleDto::toQueryString()` формирует строку для поиска в кеше и запроса к MIS (HTTP query).
|
||||
|
||||
## Пошаговый алгоритм HTTP
|
||||
|
||||
1. Контроллер наполняет DTO из query, валидирует.
|
||||
2. `SpecialistService::getSchedule($dto)` диспатчит `GetScheduleMessage` (см. отдельную статью).
|
||||
3. Хендлер сначала зовёт `ScheduleCacheService::getCachedSchedule($queryString, $isOnlineMode)`.
|
||||
|
||||
## Логика `ScheduleCacheService`
|
||||
|
||||
### TTL («как долго живёт кеш»)
|
||||
|
||||
Константа **`CACHE_TTL_MINUTES = 5`**.
|
||||
|
||||
`getCachedSchedule`:
|
||||
|
||||
- вычисляет порог `createdAt >= now - 5 minutes`;
|
||||
- `ScheduleRepository::findByQueryModeAndTime($queryString, $isOnlineMode, $createdAfter)`;
|
||||
- если строк нет — `null` (промах кеша).
|
||||
|
||||
То есть **инвалидация по времени**: записи старше 5 минут не считаются валидными для ответа.
|
||||
|
||||
### Запись и перезапись
|
||||
|
||||
`saveSchedule` перед вставкой новых слотов вызывает **`removeByQueryStringAndMode`**: удаляет все записи кеша с тем же `queryString` и `onlineMode`. Это **жёсткое обновление** среза расписания под ключ запроса.
|
||||
|
||||
Каждый слот — строка `Schedule` с полями врача, отделения, даты, интервала, `queryString`, `onlineMode`, `createdAt` и др.
|
||||
|
||||
### Ошибки чтения из БД
|
||||
|
||||
В `getCachedSchedule` перехват `Exception` → лог `Error reading from cache` → возврат `null` (как промах кеша, дальше пойдут в MIS).
|
||||
|
||||
## Инвалидация / уборка
|
||||
|
||||
| Механизм | Описание |
|
||||
| --- | --- |
|
||||
| По TTL при чтении | старше 5 минут не отдаются |
|
||||
| `saveSchedule` | удаление предыдущих строк с тем же ключом перед insert |
|
||||
| `app:schedule:clear-cache --hours=N` | массовое удаление по `createdAt < now - N hours` через `clearOldCache` |
|
||||
| Опция `--stats` | статистика по таблице без удаления |
|
||||
|
||||
## `ScheduleErrorHandlerService`
|
||||
|
||||
Не часть кеша; вызывается из хендлера при ошибках HTTP-клиента или непойманных исключений: логирование и возврат массива с `status_code`, телом ответа MIS, длительностью и т.д. (см. [schedule-messenger.md](./schedule-messenger.md)).
|
||||
|
||||
## Mermaid
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[GET /specialist/schedule] --> B[ScheduleDto + validate]
|
||||
B --> C[SpecialistService.getSchedule]
|
||||
C --> H[GetScheduleMessageHandler]
|
||||
H --> D{Есть свежие Schedule строки?}
|
||||
D -->|да ≤5 мин| R[reconstructFromDatabase → ответ cached]
|
||||
D -->|нет| E[HTTP MIS intervals]
|
||||
E --> S[saveSchedule: delete old + insert Schedule rows]
|
||||
S --> R2[ответ api + _meta]
|
||||
```
|
||||
|
||||
## Внешние зависимости
|
||||
|
||||
| Система | Роль |
|
||||
| --- | --- |
|
||||
| PostgreSQL | хранение «кеша» `Schedule` |
|
||||
| Инфоклиника (MIS) | источник расписания при промахе |
|
||||
| Redis | **не используется** в этом сценарии по текущему коду |
|
||||
|
||||
## Обработка ошибок и edge cases
|
||||
|
||||
- **БД недоступна при чтении кеша** — лог, ответ пойдёт в MIS; если и MIS недоступна — ошибка из хендлера.
|
||||
- **Пустой ответ MIS** — `normalize` / пустые массивы зависят от клиента (см. `InfoclinicaClientService`).
|
||||
- **Несогласованность `onlineMode` в DTO**: в контроллере в поле может попадать `0|1` из query — при строгой типизации возможны проблемы валидации (наблюдение для джуна при отладке).
|
||||
|
||||
## Ссылки на классы
|
||||
|
||||
- `apps/backend/src/Controller/SpecialistController.php` (`specialistSchedule`)
|
||||
- `apps/backend/src/Dto/ScheduleDto.php`
|
||||
- `apps/backend/src/Service/ScheduleCache/ScheduleCacheService.php`
|
||||
- `apps/backend/src/Repository/ScheduleRepository.php`
|
||||
- `apps/backend/src/Command/ClearScheduleCacheCommand.php`
|
||||
- `apps/backend/src/Service/ErrorHandler/ScheduleErrorHandlerService.php`
|
||||
|
||||
Связанный сценарий Messenger: [schedule-messenger.md](./schedule-messenger.md).
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
title: Асинхронное сообщение GetScheduleMessage (Backend)
|
||||
---
|
||||
|
||||
# Сценарий 2.3: `GetScheduleMessage` и `GetScheduleMessageHandler`
|
||||
|
||||
## Бизнес-цель
|
||||
|
||||
Получение расписания спроектировано через **Symfony Messenger**, чтобы:
|
||||
|
||||
- отделить HTTP-слой от интеграции с MIS и работы с кешем;
|
||||
- унифицировать вызовы (тот же message может диспатчиться из других мест);
|
||||
- заложить возможность смены транспорта с `sync` на очередь без переписывания контроллера.
|
||||
|
||||
По факту конфигурации в `config/packages/messenger.yaml` маршрут для `App\Message\GetScheduleMessage` указывает на транспорт **`sync://`**, то есть обработка **синхронная в том же PHP-процессе**, а не отложенная очередь.
|
||||
|
||||
## Точки входа
|
||||
|
||||
| Тип | Где создаётся сообщение | Класс |
|
||||
| --- | --- | --- |
|
||||
| HTTP | `GET /specialist/schedule` → `SpecialistService::getSchedule` | `App\Message\GetScheduleMessage` |
|
||||
| Messenger | обработчик | `App\MessageHandler\GetScheduleMessageHandler` |
|
||||
|
||||
CLI для этого сообщения отдельно не зарегистрирован в коде репозитория.
|
||||
|
||||
## Пошаговый алгоритм
|
||||
|
||||
1. `SpecialistService::getSchedule` создаёт `new GetScheduleMessage($dto->toQueryString(), $dto->onlineMode)` и диспатчит через `MessageBusInterface`.
|
||||
2. `GetScheduleMessageHandler::__invoke`:
|
||||
- стартует `PerformanceTrackerService`;
|
||||
- **кеш-hit**: `ScheduleCacheService::getCachedSchedule` — при успехе возвращает данные + `_meta.source = cached`;
|
||||
- **кеш-miss**: `InfoclinicaClientService::getSchedule($queryString, $isOnlineMode)` (второй аргумент — в коде хендлера; фактическая сигнатура клиента может отличаться — см. раздел Edge cases);
|
||||
- при успешном ответе MIS — `ScheduleCacheService::saveSchedule` и ответ с `_meta.source = api`;
|
||||
- при `HttpExceptionInterface` — `ScheduleErrorHandlerService::handleHttpException` возвращает массив диагностики;
|
||||
- при прочих исключениях — `handleGeneralException`.
|
||||
|
||||
## Mermaid
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant SC as SpecialistController
|
||||
participant SS as SpecialistService
|
||||
participant BUS as MessageBus
|
||||
participant H as GetScheduleMessageHandler
|
||||
participant CACHE as ScheduleCacheService
|
||||
participant MIS as InfoclinicaClientService
|
||||
participant ERR as ScheduleErrorHandlerService
|
||||
|
||||
SC->>SS: getSchedule(ScheduleDto)
|
||||
SS->>BUS: dispatch(GetScheduleMessage)
|
||||
BUS->>H: __invoke
|
||||
H->>CACHE: getCachedSchedule
|
||||
alt hit
|
||||
CACHE-->>H: данные слотов
|
||||
else miss
|
||||
H->>MIS: getSchedule (HTTP)
|
||||
alt MIS OK
|
||||
MIS-->>H: JSON нормализован
|
||||
H->>CACHE: saveSchedule
|
||||
else HTTP error
|
||||
MIS-->>ERR: HttpException
|
||||
ERR-->>H: error payload
|
||||
end
|
||||
end
|
||||
H-->>SS: результат массива
|
||||
SS-->>SC: JsonResponse
|
||||
```
|
||||
|
||||
## Внешние зависимости
|
||||
|
||||
| Система | Роль |
|
||||
| --- | --- |
|
||||
| PostgreSQL (`Schedule`) | кеш слотов |
|
||||
| Инфоклиника | `GET /api/reservation/intervals?{query}` (см. `InfoclinicaClientService`) |
|
||||
| Логирование | канал `infoclinica-cache`, `infoclinica-error` (через `withName`) |
|
||||
|
||||
## Обработка ошибок и edge cases
|
||||
|
||||
- **Ошибка HTTP MIS** — не исключение наружу; возвращается массив с `status_code`, `response` и др. Контроллер отдаёт его как JSON `200` с этим телом (поведение «мягкой ошибки» — важно для фронта).
|
||||
- **Рассинхрон сигнатур**: в `GetScheduleMessageHandler` вызов `getSchedule` с двумя аргументами должен соответствовать PHP-интерфейсу клиента; при несоответствии будет `ArgumentCountError` на старте (сигнал провести рефакторинг интерфейса `InfoclinicaClientServiceInterface` и реализации).
|
||||
- **sync-транспорт**: нет повторного выполнения из failed queue для этого message в штатной конфигурации (в отличие от реальной async-очереди).
|
||||
|
||||
## Ссылки на классы
|
||||
|
||||
- `apps/backend/src/Message/GetScheduleMessage.php`
|
||||
- `apps/backend/src/MessageHandler/GetScheduleMessageHandler.php`
|
||||
- `apps/backend/src/Service/Specialist/SpecialistService.php`
|
||||
- `apps/backend/config/packages/messenger.yaml` (`routing` для `GetScheduleMessage`)
|
||||
|
||||
Дополнительно: кеш в БД — [schedule-cache.md](./schedule-cache.md).
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
title: SMS-уведомления и сущности Record / AlertSms (Backend)
|
||||
---
|
||||
|
||||
# Сценарий 3.2: Уведомления (SMS), связь `Record` ↔ `AlertSms`
|
||||
|
||||
## Бизнес-цель
|
||||
|
||||
После записи к врачу пациент может получить **SMS** (напоминание, код подтверждения и т.п.). В модели данных предусмотрено локальное хранение **факта записи** (`Record`) и **ответа SMS-провайдера** (`AlertSms`) в связке один-к-одному.
|
||||
|
||||
## Точки входа (фактический код)
|
||||
|
||||
| Компонент | Назначение |
|
||||
| --- | --- |
|
||||
| `App\Entity\Record` | Телефон, `specialistId`, `hash`, JSON-блоб `reserve`, время создания. |
|
||||
| `App\Entity\AlertSms` | Ссылка на `Record`, время, текст/ответ провайдера. |
|
||||
| `Sms4bClientService`, `SmsruClientService` | Реализации `SmsClientServiceInterface` (HTTP-клиенты). |
|
||||
|
||||
**Поиск по дереву `apps/backend/src`:** вызовов `Sms4bClientService` / `SmsruClientService` или `RecordRepository` из контроллеров и обработчиков **не обнаружено**. То есть **интеграция SMS заложена на уровне инфраструктуры/сервисов, но не подключена к HTTP-сценарию анонимной записи** в этом репозитории на момент документирования.
|
||||
|
||||
## Как бы выглядел целевой flow (рекомендуемая логика)
|
||||
|
||||
1. После успешного ответа MIS о записи backend создаёт `Record` с телефоном и сериализованным `reserve`.
|
||||
2. Асинхронно или синхронно вызывается SMS-клиент с текстом шаблона.
|
||||
3. Ответ провайдера сохраняется в `AlertSms`, линкуется через `Record::setAlertSms` (Doctrine `OneToOne`).
|
||||
|
||||
## Mermaid (целевая схема — не полностью реализована в коде)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant API as Backend API
|
||||
participant DB as PostgreSQL
|
||||
participant SMS as SMS шлюз (Sms4b / sms.ru)
|
||||
|
||||
API->>DB: persist Record
|
||||
API->>SMS: отправить SMS
|
||||
SMS-->>API: ответ / статус
|
||||
API->>DB: persist AlertSms ↔ Record
|
||||
```
|
||||
|
||||
## Внешние зависимости
|
||||
|
||||
| Система | Статус в коде |
|
||||
| --- | --- |
|
||||
| Sms4b / sms.ru | Классы-клиенты есть |
|
||||
| PostgreSQL | Таблицы под `Record` / `AlertSms` предполагаются миграциями |
|
||||
|
||||
## Обработка ошибок и edge cases
|
||||
|
||||
- **Нет вызова SMS** — риск «тихого» пропуска уведомления; фронт не может отличить по API, если нет явного шага.
|
||||
- **Повторная отправка** — нужна идемпотентность по `hash` записи (поле в `Record`) — в коде не просматривалось.
|
||||
|
||||
## Ссылки на классы
|
||||
|
||||
- `apps/backend/src/Entity/Record.php`
|
||||
- `apps/backend/src/Entity/AlertSms.php`
|
||||
- `apps/backend/src/Repository/RecordRepository.php`, `AlertSmsRepository.php`
|
||||
- `apps/backend/src/Service/Client/Sms4bClientService.php`
|
||||
- `apps/backend/src/Service/Client/SmsruClientService.php`
|
||||
|
||||
Модель данных: [data-model.md](../../data-model.md). Сценарий записи в MIS: [anonymous-reserve.md](./anonymous-reserve.md).
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
title: Карточка врача и локации приёма (Backend)
|
||||
---
|
||||
|
||||
# Сценарий 2.1: Карточка врача и локации (`Specialist` + `Location`)
|
||||
|
||||
## Бизнес-цель
|
||||
|
||||
Пользователю нужна **карточка врача** (ФИО, медиа, признаки активности, регион и т.д.) вместе с **локациями приёма**: привязка к отделению, филиалу, режиму online и пр., чтобы выбрать место и сценарий записи.
|
||||
|
||||
## Точки входа
|
||||
|
||||
| Тип | Метод + URL | Класс |
|
||||
| --- | --- | --- |
|
||||
| HTTP | `GET /specialist/{id}` | `SpecialistController::show` (`id` — целое) |
|
||||
| HTTP | `GET /specialist/by/{identifier}?regionId=` | `SpecialistController::showBy` |
|
||||
|
||||
Список врачей: `GET /specialist/list` — отдельный сценарий фильтрации, тот же репозиторий.
|
||||
|
||||
CLI / Messenger для **чтения карточки** не используются.
|
||||
|
||||
## Как собирается «агрегат» в рамках Symfony / ORM
|
||||
|
||||
Doctrine-сущность `App\Entity\Specialist` содержит `OneToMany` на `App\Entity\Location` (`mappedBy: 'specialist'`, каскады `persist`/`remove`). Для API **отдельного сервиса-сборщика нет**: при `GET /specialist/{id}` используется param converter и сериализация.
|
||||
|
||||
Группы Serializer **`specialist:detail`** и **`from.specialist:read`** включают в ответ связанные локации (см. атрибуты `Groups` на поле коллекции локаций в `Specialist`).
|
||||
|
||||
Маршрут **`showBy`**:
|
||||
|
||||
1. `SpecialistService::getSpecialist($identifier, $regionId)`;
|
||||
2. если `identifier` числовой — выборка по `id`;
|
||||
3. иначе — по `alias` и опциональному `regionId` через `SpecialistRepository::createFilteredQueryBuilder`;
|
||||
4. при `null` — **404** `{"error":"not found"}`.
|
||||
|
||||
## Пошаговый алгоритм — `GET /specialist/{id}`
|
||||
|
||||
1. Загрузка `Specialist` из БД по `id` (или 404 на уровне фреймворка, если не найден).
|
||||
2. `JsonResponse` с группами `specialist:detail`, `from.specialist:read`.
|
||||
3. При обходе графа сериализатором Doctrine догружает `locations`.
|
||||
|
||||
## Mermaid
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Клиент
|
||||
participant SC as SpecialistController
|
||||
participant SS as SpecialistService
|
||||
participant SR as SpecialistRepository
|
||||
participant SER as Serializer / ORM
|
||||
|
||||
alt По числовому id
|
||||
C->>SC: GET /specialist/{id}
|
||||
SC->>SER: serialize(Specialist)
|
||||
SER->>SER: подтягивание Location
|
||||
SC-->>C: JSON
|
||||
else По alias
|
||||
C->>SC: GET /specialist/by/{identifier}
|
||||
SC->>SS: getSpecialist
|
||||
SS->>SR: QueryBuilder
|
||||
SR-->>SS: Specialist | null
|
||||
SS-->>SC: entity
|
||||
SC->>SER: serialize
|
||||
SC-->>C: 200 или 404
|
||||
end
|
||||
```
|
||||
|
||||
## Внешние зависимости
|
||||
|
||||
| Система | Участие |
|
||||
| --- | --- |
|
||||
| PostgreSQL | данные `specialist` и `location` |
|
||||
| Инфоклиника / Bitrix | **не вызываются** при чтении карточки |
|
||||
|
||||
## Обработка ошибок и edge cases
|
||||
|
||||
- **Не найден по alias** — `404` с простым телом.
|
||||
- **Фото врача**: отдельные эндпоинты загрузки картинки (`specialistPicture`, upload) — не часть сценария «только чтение».
|
||||
|
||||
## Ссылки на классы
|
||||
|
||||
- `apps/backend/src/Controller/SpecialistController.php`
|
||||
- `apps/backend/src/Service/Specialist/SpecialistService.php`
|
||||
- `apps/backend/src/Entity/Specialist.php`, `Entity/Location.php`
|
||||
- `apps/backend/src/Repository/SpecialistRepository.php`
|
||||
|
||||
Админские CRUD по локациям: `LocationController`. Карта домена: [backend-ddd.md](../backend-ddd.md).
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
title: Синхронизация врачей и отзывов (Backend)
|
||||
---
|
||||
|
||||
# Сценарий 4.1: Синхронизация врачей и отзывов (Infoclinica + Bitrix)
|
||||
|
||||
## Бизнес-цель
|
||||
|
||||
Актуализировать справочники backend из внешних систем:
|
||||
|
||||
- **Список врачей из Инфоклиники** загружается и складывается в PostgreSQL (сущность **`Idoctor`** — staging/интеграционная модель).
|
||||
- **Нормализация признаков `Specialist`** из Bitrix-related данных (команда **`bitrix-update-doctors`** — очистка `dcodes`).
|
||||
- **Отзывы** подтягиваются **из MySQL Bitrix** через `BitrixService` и сохраняются как `Review`, связанные с `Specialist`.
|
||||
|
||||
## Точки входа
|
||||
|
||||
| Тип | Имя команды Symfony | Класс |
|
||||
| --- | --- | --- |
|
||||
| CLI | `upload:doctors` | `UploadDoctorsCommand` |
|
||||
| CLI | `bitrix-update-doctors` | `BitrixUpdateDoctorsCommand` |
|
||||
| CLI | `bitrix-update-reviews` | `BitrixUpdateReviewsCommand` |
|
||||
|
||||
Все три предназначены для **cron** или ручного запуска в контейнере `php84`.
|
||||
|
||||
## Сценарий A — `upload:doctors` (Инфоклиника → `Idoctor`)
|
||||
|
||||
### Алгоритм
|
||||
|
||||
1. Читает активные отделения из PostgreSQL (`Department`, опция `--department` для одного `did`).
|
||||
2. Для каждого отделения в цикле вызывает HTTP **через клиент Инфоклиники**: `GET /specialists/doctors?departments={did}&onlineMode={0|1}&firstrow=&lastrow=` чанками (`CHUNK_SIZE` 300).
|
||||
3. Для каждого врача определяется ключ `"{dcode}_{departmentId}_{onlineMode}"`, подгружаются существующие `Idoctor` тем же ключом.
|
||||
4. `updateDoctorEntity` обновляет поля (`dcode`, `name`, `department`, `filial`, `nearestDate`, `onlineMode`), `persist`, пакетный `flush` каждые `BATCH_SIZE` (150).
|
||||
5. Между отделениями — `sleep(1)`; между чанками — `usleep(200000)`.
|
||||
|
||||
### «Конфликты»
|
||||
|
||||
Явного SQL `ON CONFLICT` нет: используется **ORM upsert-паттерн** — найти сущность по составному ключу в PHP или создать `new Idoctor()`, затем `persist`.
|
||||
|
||||
## Сценарий B — `bitrix-update-doctors` (PostgreSQL `Specialist`)
|
||||
|
||||
### Алгоритм
|
||||
|
||||
1. Загружает **все** `Specialist` из БД.
|
||||
2. Нормализует строку `dcodes`: для каждого врача фильтрует коды длиной ≥ 7, отбрасывает `'0'`, пустые наборы превращает в `null`.
|
||||
3. `flush` один раз в конце.
|
||||
4. Обращение к `BitrixService` для `kodoper` **закомментировано** в текущей версии файла.
|
||||
|
||||
**Это не загрузка врачей из Bitrix**, а офлайн-очистка данных в уже существующей таблице `specialist`.
|
||||
|
||||
## Сценарий C — `bitrix-update-reviews` (MySQL Bitrix → PostgreSQL `Review`)
|
||||
|
||||
### Алгоритм
|
||||
|
||||
1. Постранично обходит `Specialist` батчами по 5 записей.
|
||||
2. Для каждого врача `BitrixService::getReviews($specialist->getId())`:
|
||||
- читает связанные элементы инфоблоков в **MySQL** (`doctrine.dbal.mysql_connection`);
|
||||
- для каждого отзыва известен `REVIEW_ID`.
|
||||
3. В PostgreSQL ищется `Review` с тем же `externalId`; если нет — `new Review()` + `setExternalId`.
|
||||
4. Поля текста, рейтинга, автора, даты, активности заполняются из структуры Bitrix (включая «распаковку» сериализованных полей в `getReviews`).
|
||||
5. Неактивные или без текста — пропуск.
|
||||
6. `$specialist->addReview($review)`, `flush`; при ошибке драйвера — логирование проблемных UTF-8 последовательностей.
|
||||
|
||||
### «Конфликты»
|
||||
|
||||
Снова **без `ON CONFLICT`**: идемпотентность за счёт поиска по `externalId` перед вставкой.
|
||||
|
||||
## Mermaid
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph MIS["Инфоклиника"]
|
||||
API["GET /specialists/doctors"]
|
||||
end
|
||||
subgraph PG["PostgreSQL"]
|
||||
ID["Idoctor"]
|
||||
SP["Specialist"]
|
||||
RV["Review"]
|
||||
end
|
||||
subgraph BX["Bitrix MySQL"]
|
||||
IB["Инфоблоки отзывов"]
|
||||
end
|
||||
|
||||
UC["upload:doctors"] --> API
|
||||
API --> ID
|
||||
BD["bitrix-update-doctors"] --> SP
|
||||
BR["bitrix-update-reviews"] --> IB
|
||||
BR --> RV
|
||||
RV --> SP
|
||||
```
|
||||
|
||||
Узлы `UC` / `BD` / `BR` — это команды `upload:doctors`, `bitrix-update-doctors`, `bitrix-update-reviews`.
|
||||
|
||||
## Внешние зависимости
|
||||
|
||||
| Система | Сценарий |
|
||||
| --- | --- |
|
||||
| Инфоклиника HTTP | `upload:doctors` |
|
||||
| PostgreSQL | все три команды |
|
||||
| Bitrix MySQL | `bitrix-update-reviews` (и потенциально расширения `BitrixService`) |
|
||||
|
||||
## Обработка ошибок и edge cases
|
||||
|
||||
- **Сетевые ошибки загрузки врачей** — warning в консоли, переход к следующему чанку/отделению.
|
||||
- **Проблемные отзывы** — `DriverException` логируется с дампом полей.
|
||||
- **`BitrixService` зависит от `regionId` в некоторых методах** — убедитесь, что выставление региона покрыто в вашем окружении при расширении команды.
|
||||
|
||||
## Ссылки на классы
|
||||
|
||||
- `apps/backend/src/Command/UploadDoctorsCommand.php`
|
||||
- `apps/backend/src/Command/BitrixUpdateDoctorsCommand.php`
|
||||
- `apps/backend/src/Command/BitrixUpdateReviewsCommand.php`
|
||||
- `apps/backend/src/Service/Bitrix/BitrixService.php`
|
||||
- `apps/backend/src/Entity/Idoctor.php`, `Specialist.php`, `Review.php`
|
||||
|
||||
См. [backend-ddd.md](../backend-ddd.md) и [data-model.md](../../data-model.md).
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Генерация XML-фида для Яндекса (Backend)
|
||||
---
|
||||
|
||||
# Сценарий 4.3: XML-фиды (Яндекс)
|
||||
|
||||
## Бизнес-цель
|
||||
|
||||
Сгенерировать **YML/XML фид** для рекламных / информационных площадок (исторически Яндекс): в фид попадают **врачи**, **клиники/услуги**, **цены**, тексты и URL с UTM-метками.
|
||||
|
||||
## Точки входа
|
||||
|
||||
| Тип | Метод + URL | Класс |
|
||||
| --- | --- | --- |
|
||||
| HTTP | `GET /xml/feed` | `XmlFeedController::generateFeed` |
|
||||
| HTTP | `GET /xml/feed/v1` | `XmlFeedController::generateFeedV1` — выбор филиалов по `filials` (csv `fid`) или по `regionId` |
|
||||
|
||||
Для `/xml/feed`: query-параметр `filial` — числовой `fid`; поддерживаются UTM-поля.
|
||||
|
||||
## Пошаговый алгоритм (`generateFeed`)
|
||||
|
||||
1. `FilialRepository::findOneBy(['fid' => filialId])` — если филиал **не найден**, контроллер возвращает **пустой `Response` (HTTP 200 без тела)** — в коде нет явного статуса ошибки.
|
||||
2. Собираются UTM-параметры в массив (utm_source, utm_medium, utm_campaign, ...).
|
||||
3. `XmlFeedGeneratorService::generateFeed($filial, $utmParams)` строит `DOMDocument`:
|
||||
- `addShopInfo` — метаданные магазина/сети;
|
||||
- `addDoctors` — врачи филиала (`SpecialistService::getList` с фильтром `active=true`, `filial=fid`);
|
||||
- `addClinics`, `addServices`, `addOffers` — прайс и структура услуг через `PriceListService`, отделения через `DepartmentService`, локации через `LocationService`, филиалы через `FilialService`, тексты dcode через `SpecialistDcodeDescriptionRepository`.
|
||||
4. Контроллер возвращает `Response` с `Content-Type: application/xml` и телом `saveXML()`.
|
||||
|
||||
Версия **V1** использует `XmlFeedGeneratorV1Service` — та же идея, другой шаблон дерева XML (см. класс).
|
||||
|
||||
## Mermaid
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
C[GET /xml/feed?filial=...] --> F{Филиал найден?}
|
||||
F -->|нет| E404[Пустой Response 200]
|
||||
F -->|да| X[XmlFeedGeneratorService.generateFeed]
|
||||
X --> D[SpecialistService.getList]
|
||||
X --> P[PriceListService.getList]
|
||||
X --> L[LocationService / DepartmentService]
|
||||
X --> DOM[DOMDocument saveXML]
|
||||
DOM --> R[Response XML]
|
||||
```
|
||||
|
||||
## Внешние зависимости
|
||||
|
||||
| Система | Роль |
|
||||
| --- | --- |
|
||||
| PostgreSQL | все справочники и врачи |
|
||||
| Инфоклиника / Bitrix | **не вызываются** при генерации |
|
||||
| Redis | **не используется** |
|
||||
|
||||
## Обработка ошибок и edge cases
|
||||
|
||||
- **Пустой/неверный `filial`** — пустой ответ `200` без XML (так задумано в текущем контроллере).
|
||||
- **Большой XML** — генерация синхронна; долгие запросы на проде стоит кешировать на уровне nginx/CDN или вынести в задачу.
|
||||
- **Пустые списки врачей/цен** — фид всё равно строится, но может не пройти требования площадки — нужна валидация бизнес-правил.
|
||||
|
||||
## Ссылки на классы
|
||||
|
||||
- `apps/backend/src/Controller/XmlFeedController.php`
|
||||
- `apps/backend/src/Service/XmlFeedGenerator/XmlFeedGeneratorService.php`
|
||||
- `apps/backend/src/Service/XmlFeedGenerator/XmlFeedGeneratorV1Service.php`
|
||||
- `apps/backend/src/Service/Specialist/SpecialistService.php`
|
||||
- `apps/backend/src/Service/PriceList/PriceListService.php`
|
||||
|
||||
См. [backend-architecture.md](../backend-architecture.md) и [backend-ddd.md](../backend-ddd.md).
|
||||
Reference in New Issue
Block a user