Files
docs/infrastructure/backend-external-services.md
T

528 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Backend — внешние сервисы и стратегия для test/stage/prod
---
# Backend: внешние сервисы, БД и интеграции
> Разбор всех **сторонних зависимостей** `apps/backend`: что они делают, где используются в коде, и **что делать в тестовом контуре** (K8s) — поднять свой инстанс, поставить заглушку или эмулятор.
>
> Связанные документы: [K8s + Terraform + ArgoCD + Gitea](./k8s-cicd-platform-plan.md), локальный контур (`local/`, см. корень репозитория), [бизнес-сценарии](../apps/backend-scenarios/index.md).
---
## 0. Сводная матрица решений (test-контур)
| # | Зависимость | Тип | Где в коде | Test-контур | Stage | Prod |
|---|-------------|-----|------------|-------------|-------|------|
| 1 | PostgreSQL (основная) | БД | `DATABASE_URL`, Doctrine ORM | **Новый инстанс** (1×) | 1× на stage-сервере | **HA: primary + sync replica** (§7.6 плана) |
| 2 | PostgreSQL (cabinet) | БД | `DATABASE_CABINET_URL`, `UsrlogController` | **Новый инстанс** + seed | Отдельный | Prod |
| 3 | MySQL (Bitrix CMS) | БД | `DATABASE_BITRIX_URL`, `BitrixService`, SQL views | **Новый инстанс** + seed из `local/mysql-bitrix` | Отдельный | Prod Bitrix |
| 4 | Redis | Кеш/сессии | `REDIS_URL` | **Новый инстанс** в K8s | Отдельный | Prod Redis |
| 5 | MIS / Инфоклиника (HTTP) | API | `MIS_URL`, `InfoclinicaClientService` | **Mock-сервис** (WireMock / свой stub) | Test MIS или read-only prod | Prod MIS |
| 6 | Widget API (`widget.sovamed.ru`) | API | Hardcoded в Upload*Command | **Mock** или `MIS_URL` после рефакторинга | Stage widget | Prod |
| 7 | Bitrix site (HTTP, картинки) | API | `BITRIX_URL`, `BitrixClientService` | **Mock** (static files) или seed URLs | Stage | Prod |
| 8 | SMS (sms.ru, sms4b) | API | `SMSRU_*`, `SMS4B_*` | **Заглушка** (noop) | Заглушка | **Реальный** провайдер |
| 9 | Почта | API | `MAILER_DSN`, `SendMailService` | **Mailpit / null://null** | Mailpit или sandbox SMTP | Prod SMTP |
| 10 | Yandex SmartCaptcha | API | `SMARTCAPTCHA_*`, `ServiceController` | **Заглушка** (always OK) | Заглушка | **Реальный** ключ |
| 11 | Calltouch | API | `CT_*`, `CalltouchController` | **Заглушка** (noop, уже частично) | Заглушка | **Реальный** API |
| 12 | Bitrix24 | — | `BITRIX24_URL` в `.env` | **Не используется в коде** — игнор | — | При появлении интеграции |
| 13 | JWT / AES / Lock | Локально | env + файлы ключей | **Свои ключи** test | Свои | Prod secrets |
| 14 | Symfony Messenger | Очередь | `MESSENGER_TRANSPORT_DSN` | `doctrine://` или `sync://` | doctrine/redis | prod queue |
```mermaid
flowchart TB
subgraph backend [backend pod]
API[Symfony API]
CMD[Console / CronJobs]
end
subgraph own_test [Поднимаем в test — свои инстансы]
PG[(PostgreSQL main)]
PGC[(PostgreSQL cabinet)]
MY[(MySQL Bitrix)]
RD[(Redis)]
end
subgraph stubs [Заглушки в test]
MIS_M[Mock MIS / Widget API]
SMS_M[Noop SMS]
MAIL_M[Mailpit]
CAP_M[Mock SmartCaptcha]
CT_M[Noop Calltouch]
BTX_M[Mock Bitrix HTTP images]
end
subgraph prod_only [Только prod / stage по решению]
MIS_P[Реальная Инфоклиника]
SMS_P[sms.ru / sms4b]
CAP_P[Yandex SmartCaptcha]
CT_P[Calltouch]
end
API --> PG
API --> PGC
API --> MY
API --> RD
API --> MIS_M
API --> SMS_M
API --> MAIL_M
API --> CAP_M
API --> CT_M
API --> BTX_M
CMD --> PG
CMD --> MY
CMD --> MIS_M
MIS_P -.->|stage/prod| API
SMS_P -.->|prod| API
```
---
## 1. Базы данных (с БД — новый инстанс)
### 1.1. PostgreSQL — основная (`DATABASE_URL`)
**Назначение:** все Doctrine-сущности backend (`User`, `Specialist`, `News`, `Schedule`, …).
**Где используется:**
- `config/packages/doctrine.yaml` → connection `default`
- все Repository, миграции `apps/backend/migrations`
- кеш расписания (`Schedule` в таблице, не Redis)
- Symfony Messenger при `MESSENGER_TRANSPORT_DSN=doctrine://default`
**Test:** Helm Bitnami PostgreSQL / CNPG `instances: 1` в `sova-data-test`.
**Stage:** один PG на **отдельном stage-сервере**.
**Prod:** Patroni (2 db-VM) + **witness etcd на отдельной VM** (не prod-app). Подключение: `DATABASE_URL` → PgBouncer **:6432** (transaction, API); migrate Job и Messenger → **:5432** (session). [§7.6 плана](./k8s-cicd-platform-plan.md).
**Stage/Prod:** отдельные credentials; **никогда** не указывать prod URL в test.
---
### 1.2. PostgreSQL — cabinet (`DATABASE_CABINET_URL`)
**Назначение:** read-only доступ к legacy-таблицам cabinet (не Doctrine entities backend).
**Где используется:**
- `config/packages/doctrine.yaml` → connection `cabinet`
- `UsrlogController``SELECT` из `public.usrlog` через `doctrine.dbal.cabinet_connection`
**Test:** отдельный PostgreSQL (можно второй database на том же chart), seed из `local/postgres/init/03-cabinet-schema-and-seed.sql`.
**Если usrlog не нужен в test:** connection можно оставить на пустую БД; эндпоинт `/usrlog/list` вернёт пустой список.
---
### 1.3. MySQL — Bitrix CMS (`DATABASE_BITRIX_URL`)
**Назначение:** чтение таблиц Bitrix (`b_iblock_*`, `b_file`) для синхронизации контента и отзывов.
**Где используется:**
| Компонент | Как |
|-----------|-----|
| `BitrixService` | прямые SQL через `doctrine.dbal.mysql_connection` |
| `BitrixUpdateReviewsCommand` | `getReviews()` — отзывы по `specialist.id` |
| `BitrixUpdateDoctorsCommand` | нормализация `dcodes` (Bitrix HTTP закомментирован) |
| `*CrudService::syncFromView*` | `INSERT … SELECT FROM public.view_*` |
| Миграции | views `view_article`, `view_news`, … с JOIN на `b_*` (через **mysql_fdw** на prod или упрощённые views в test) |
**Test:** **новый MySQL** в K8s (Bitnami MySQL chart), init SQL из `local/mysql-bitrix/init/` в корне репозитория. Дополнительно — создать упрощённые `view_news`, `view_promo`, … или наполнять таблицы `news`/`promo` через CRUD API без sync-команд.
**Важно:** на prod PG использует **mysql_fdw** (`infrastructure/pgsql` + extensions). В test можно:
- **Вариант A (рекомендуется):** mysql_fdw в test-PG → test-MySQL (повторяет prod);
- **Вариант B:** отключить sync-команды в test CronJobs, контент только через adminPanel CRUD;
- **Вариант C:** materialized views только на PG без FDW (дублировать seed в PG).
---
### 1.4. Redis (`REDIS_URL`)
**Назначение:** Symfony cache, sessions (prod php.ini), потенциально Messenger.
**Test:** Bitnami Redis в `sova-data-test`, свой пароль в SealedSecret.
---
## 2. HTTP-интеграции
### 2.1. MIS / Инфоклиника (`MIS_URL` → `InfoclinicaClientService`)
**Prod URL (пример):** `https://widget.sovamed.ru`
**Эндпоинты клиента:**
| Метод | Путь | Сценарий |
|-------|------|----------|
| GET | `/api/reservation/intervals?{query}` | Расписание врача |
| GET | `/filials/list` | Список филиалов (через client) |
| POST | `/api/reservation/anonymous-reserve` | Анонимная запись |
**Где вызывается:**
- `GetScheduleMessageHandler``getSchedule()`
- `GetAnonymousReserveRequestMessageHandler``anonymousReserve()`
- `UploadDoctorsCommand`, `UploadDepartmentsCommand``$client->request('GET', …)` через `InfoclinicaClientServiceInterface`
**Test — рекомендация: Mock-сервис в K8s**
Развернуть **WireMock** или лёгкий **Node/Go stub** в namespace `sova-mocks`:
```text
MIS_URL=http://mis-mock.sova-mocks.svc.cluster.local
```
Stub должен отдавать JSON в формате, который ждёт `InfoclinicaClientService::normalizeSchedule()` (см. [schedule-cache.md](../apps/backend-scenarios/schedule-cache.md)).
**Альтернативы:**
| Вариант | Когда |
|---------|-------|
| WireMock + JSON fixtures | Полный контроль, воспроизводимые тесты |
| Отдельный «test MIS» у вендора | Если Infoclinica даёт sandbox (уточнить у владельца интеграции) |
| Read-only прокси на prod | **Не рекомендуется** — риск записи/нагрузки |
**Stage:** sandbox MIS или изолированный контур клиники.
---
### 2.2. Widget API (hardcoded `https://widget.sovamed.ru`)
**Проблема:** часть команд **не читает `MIS_URL`**, а хардкодит URL:
| Команда | Путь |
|---------|------|
| `UploadFilialsCommand` | `GET /filials/list` |
| `UploadPriceCommand` | `GET /pricelist/list` |
| `UploadPriceDepCommand` | `GET /pricelist/departments` |
**Test:**
1. **Краткосрочно:** тот же mock-сервис, что для MIS, + **рефакторинг команд** — вынести `base_uri` в env `WIDGET_API_URL` или переиспользовать `MIS_URL`.
2. **До рефакторинга:** mock должен слушать на URL, который прописан в коде (или патч через `/etc/hosts` + Ingress) — **технический долг**.
---
### 2.3. Bitrix site HTTP (`BITRIX_URL` → `BitrixClientService`)
**Prod URL (пример):** `https://sovamed.ru`
**Назначение:** скачивание файлов врачей с путей вида `/upload/iblock/...`.
**Где используется:**
- `GetSpecialistPictureMessageHandler``getSpecialistImage($path)`
**Test:**
| Вариант | Описание |
|---------|----------|
| **Mock HTTP** | nginx/static с несколькими `.jpg` в `/upload/iblock/` |
| **Отключить handler** | Если в test seed картинки уже локальные (`specialist/xxx.jpg`) |
| **Прокси на stage Bitrix** | Только read, без записи |
`BITRIX_URL=http://bitrix-mock.sova-mocks.svc.cluster.local`
---
### 2.4. SMS — sms.ru и sms4b (`SMSRU_*`, `SMS4B_*`)
**Клиенты:** `SmsruClientService`, `Sms4bClientService` (реализуют `SmsClientServiceInterface`).
**Статус в коде:** сервисы **зарегистрированы** в `config/services.yaml`, но **ни один контроллер/команда их пока не inject'ит**. Сущности `Record` / `AlertSms` есть, логика отправки SMS **не подключена** к HTTP-сценариям.
**Test (по вашему решению): заглушка**
```yaml
# Symfony: test env — noop implementation
App\Service\Client\Interfaces\SmsClientServiceInterface:
class: App\Service\Client\Stub\NoopSmsClientService
```
`NoopSmsClientService` пишет в log «SMS suppressed» и возвращает `{ "status": "ok", "stub": true }`.
**Prod:** реальные `SMSRU_URL` / `SMS4B_URL` и токены из SealedSecret.
**Stage:** заглушка или отдельный «sandbox»-аккаунт провайдера с whitelist номеров.
---
### 2.5. Почта (`MAILER_DSN`, `SendMailService`)
**Где используется:**
- `ServiceController::sendmail` — отправка по query-параметрам, защита `MAILER_ACCESS_TOKEN`
- Symfony `MailerInterface``SendMailService`
**Prod `.env`:** `MAILER_DSN=null://null` (письма фактически не уходят, но endpoint есть).
**Test — рекомендация:**
| DSN | Зачем |
|-----|-------|
| `smtp://mailpit.sova-mocks:1025` | UI на :8025, видно письма в браузере |
| `null://null` | Если endpoint `/service/sendmail` не тестируете |
**Безопасность Mailpit Web UI:**
Mailpit UI (`:8025`) **не выставлять в интернет без защиты**. В тестовых письмах могут быть ФИО, mock-пароли, токены сброса (иногда валидные и на stage).
```yaml
# Ingress mailpit.test.sova.dev — только Basic Auth
metadata:
annotations:
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: mailpit-basic-auth
nginx.ingress.kubernetes.io/auth-realm: "Mailpit test only"
```
Альтернатива: UI **только cluster-internal** (`kubectl port-forward`), SMTP `:1025` — из `sova-test` по Cluster DNS без Ingress.
**Stage:** Mailpit или корпоративный sandbox SMTP.
**Prod:** реальный SMTP / API (SendGrid, Yandex 360, …).
---
### 2.6. Yandex SmartCaptcha (`SMARTCAPTCHA_URL`, `SMARTCAPTCHA_KEY`)
**Где используется:**
- `ServiceController::smartCaptcha``POST /smart-captcha`
- `AnonymousReserveRequestDto::$captcha` — поле обязательно при записи (валидация на backend)
**Test — заглушка:**
Mock возвращает `{ "status": "ok", "message": "" }` для любого token (как Yandex при успехе).
Или Symfony binding:
```yaml
# when@test
App\Service\Client\Interfaces\SmartCaptchaClientServiceInterface:
class: App\Service\Client\Stub\AlwaysValidSmartCaptchaClientService
```
**Prod:** реальный `https://smartcaptcha.yandexcloud.net` + prod key.
**Связь с MIS mock:** при `anonymous-reserve` captcha проверяется на backend **до** вызова MIS; mock captcha достаточен для E2E test.
---
### 2.7. Calltouch (`CT_URL`, `CT_PARAMS`)
**Где используется:**
- `CalltouchController::createLead` — вызов **`CalltouchClientService::requestCreate` закомментирован**
- Клиент готов: `POST /lead-service/v1/api/request/create`
**Test — заглушка:**
- Оставить как сейчас (echo request) **или**
- `NoopCalltouchClientService` возвращает `{ "data": { "leadId": "test-123" } }`
**Prod/Stage (маркетинг):** реальные `CT_URL` + region tokens из `CT_PARAMS`.
---
### 2.8. Bitrix24 (`BITRIX24_URL`)
**Статус:** переменная есть в `.env`, **использований в PHP-коде backend не найдено**. Можно не задавать в test до появления интеграции.
---
## 3. Локальная инфраструктура (не внешние SaaS)
| Переменная | Назначение | Test |
|------------|------------|------|
| `JWT_*` | Lexik JWT auth | Сгенерировать отдельную пару ключей для test |
| `AES_SECRET_KEY` | `AESCryptService` (сущность `Record`) | Случайный ключ 32 байта в Secret |
| `LOCK_DSN` | Symfony Lock | `flock://` или `postgresql://…` advisory lock |
| `MESSENGER_TRANSPORT_DSN` | Очередь | `doctrine://default` (таблица messenger_messages) |
| `CORS_ALLOW_ORIGIN` | adminPanel origins | `https://admin.test.sova.dev` |
| `API_BASE_URL` | Self-reference / XML feeds | `https://api.test.sova.dev` |
| `MAILER_ACCESS_TOKEN` | Защита `/service/sendmail` | Случайный UUID в Secret |
---
## 4. Эталон: что уже сделано локально (`local/.env.local`)
В `apps/backend/.env.local` для `sova-local` уже задан паттерн **mock URL**:
```dotenv
MIS_URL=http://mock-mis.local
BITRIX_URL=http://mock-bitrix.local
SMS4B_URL=http://mock-sms4b.local
MAILER_DSN=null://null
SMARTCAPTCHA_URL=http://mock-smartcaptcha.local
```
**Для K8s test** нужно не просто «мёртвые» hostname, а **реально поднятые mock-поды** + DNS внутри кластера (Service names).
---
## 5. Архитектура mock-слоя в Kubernetes (test)
```mermaid
flowchart LR
subgraph sova_test [namespace sova-test]
BE[backend]
end
subgraph sova_mocks [namespace sova-mocks]
MIS[mis-mock :8080]
BTX[bitrix-http-mock :8080]
CAP[captcha-mock :8080]
CT[calltouch-mock :8080]
MP[mailpit :8025]
end
subgraph sova_data [namespace sova-data-test]
PG[(PostgreSQL)]
MY[(MySQL Bitrix)]
RD[(Redis)]
end
BE --> PG
BE --> MY
BE --> RD
BE --> MIS
BE --> BTX
BE --> CAP
BE --> CT
BE --> MP
```
### 5.1. Рекомендуемые Helm releases (test)
| Release | Chart / образ | Назначение |
|---------|---------------|------------|
| `mis-mock` | WireMock / custom | расписание, запись, filials, pricelist |
| `bitrix-http-mock` | nginx + static | `/upload/iblock/*.jpg` |
| `captcha-mock` | tiny HTTP server | `POST /validate` → ok |
| `calltouch-mock` | WireMock | lead create |
| `mailpit` | axllent/mailpit | SMTP + Web UI (**Basic Auth** на Ingress) |
| `postgresql-test` | bitnami/postgresql | main + cabinet DBs |
| `mysql-bitrix-test` | bitnami/mysql | Bitrix tables seed |
| `redis-test` | bitnami/redis | cache |
Namespace **`sova-mocks`** общий для всех контуров test (не prod).
### 5.2. Env backend test (фрагмент SealedSecret)
```yaml
DATABASE_URL: postgresql://sova_test:***@postgresql-test.sova-data-test:5432/sova_backend_test
DATABASE_CABINET_URL: postgresql://sova_test:***@postgresql-test.sova-data-test:5432/sova_cabinet_test
DATABASE_BITRIX_URL: mysql://bitrix_test:***@mysql-bitrix-test.sova-data-test:3306/sova_bitrix_test
REDIS_URL: redis://:***@redis-test.sova-data-test:6379/0
MIS_URL: http://mis-mock.sova-mocks.svc.cluster.local:8080
BITRIX_URL: http://bitrix-http-mock.sova-mocks.svc.cluster.local:8080
SMARTCAPTCHA_URL: http://captcha-mock.sova-mocks.svc.cluster.local:8080
SMARTCAPTCHA_KEY: test-key-not-used-by-mock
SMSRU_URL: http://noop.invalid
SMS4B_URL: http://noop.invalid
CT_URL: http://calltouch-mock.sova-mocks.svc.cluster.local:8080
MAILER_DSN: smtp://mailpit.sova-mocks.svc.cluster.local:1025
MAILER_ACCESS_TOKEN: "<uuid>"
CORS_ALLOW_ORIGIN: https://admin.test.sova.dev
```
При внедрении **NoopSmsClientService** URL sms можно не резолвить.
---
## 6. CronJobs и внешние вызовы
| CronJob (из `scripts/cron.*`) | Внешние зависимости | Test |
|-------------------------------|---------------------|------|
| `upload:deps`, `upload:doctors` | MIS HTTP | mock MIS или отключить |
| `upload:filials`, `upload:price*` | widget HTTP (hardcoded) | mock + рефакторинг URL |
| `bitrix-update-reviews` | MySQL Bitrix | test MySQL + seed |
| `bitrix-update-doctors` | только PG | OK |
| `upload:news`, `upload:promo`, … | PG views → MySQL | test MySQL + views или CRUD only |
| `ClearScheduleCacheCommand` | только PG | OK |
**Рекомендация для первого выката test:** включить CronJobs только для **очистки кеша расписания**; sync-команды — после готовности mock MIS и Bitrix MySQL.
**concurrencyPolicy в K8s:**
Для всех sync CronJobs обязательно:
```yaml
spec:
concurrencyPolicy: Forbid # не запускать вторую копию, пока первая не завершилась
jobTemplate:
spec:
activeDeadlineSeconds: 3600
```
Без `Forbid` при зависшей БД `bitrix-update-reviews` через час стартует дубликат — параллельные job'ы начнут мешать друг другу.
---
## 7. Stage vs Prod — отличия от test
| Сервис | Test | Stage | Prod |
|--------|------|-------|------|
| PostgreSQL / Redis / Bitrix MySQL | Изолированные test DB | Изолированные stage DB | Production DB |
| MIS | Mock | Sandbox MIS **или** mock | Production MIS |
| SMS | Noop | Noop или sandbox | Live |
| Mail | Mailpit | Sandbox SMTP | Live |
| SmartCaptcha | Mock | Mock | Live |
| Calltouch | Noop | Noop | Live |
| Bitrix HTTP (images) | Mock | Stage Bitrix site | Prod |
---
## 8. Технический долг (сделать до/во время миграции)
1. **Hardcoded `widget.sovamed.ru`** в `UploadFilialsCommand`, `UploadPriceCommand`, `UploadPriceDepCommand` → env `WIDGET_API_URL` или `MIS_URL`.
2. **Hardcoded `https://api.sovamed.ru`** в `XmlFeedGenerator*` → env `API_PUBLIC_URL`.
3. **Реализовать или явно отключить** SMS-клиенты (`NoopSmsClientService` + bind по env).
4. **Calltouch** — раскомментировать с env-флагом `CALLTOUCH_ENABLED=false` в test.
5. **BITRIX24_URL** — удалить из `.env` или документировать при появлении кода.
6. **Mock-сервисы** — вынести JSON fixtures в репозиторий `sova-mocks` (рядом с `sova-deploy`).
---
## 9. Чек-лист готовности интеграций test-контура
- [ ] PostgreSQL test: миграции backend применены
- [ ] MySQL Bitrix test: seed + (опционально) mysql_fdw views
- [ ] Redis test: ping из backend pod
- [ ] MIS mock: `GET /api/reservation/intervals` возвращает валидный JSON
- [ ] MIS mock: `POST /api/reservation/anonymous-reserve` → 200
- [ ] Captcha mock: `POST /validate` → ok
- [ ] Mailpit: письмо из `/service/sendmail` видно в UI (Basic Auth или port-forward)
- [ ] SMS: noop binding, приложение стартует без sms.ru
- [ ] Calltouch: lead endpoint не бьёт prod
- [ ] CORS: admin.test.sova.dev → api.test.sova.dev
- [ ] JWT keys: login работает
- [ ] CronJobs sync: `concurrencyPolicy: Forbid`
- [ ] CronJobs: только безопасные включены на первом выкате
---
## 10. Связанные файлы в репозитории
| Путь | Содержимое |
|------|------------|
| `apps/backend/.env` | prod-like переменные (образец) |
| `apps/backend/.env.local` | local mock URLs |
| `apps/backend/config/services.yaml` | DI клиентов |
| `apps/backend/config/packages/doctrine.yaml` | 3 DB connection |
| `apps/backend/src/Service/Client/*` | HTTP-клиенты |
| `apps/backend/src/Service/Bitrix/BitrixService.php` | MySQL Bitrix |
| `local/mysql-bitrix/init/` | seed Bitrix tables |
| `local/postgres/init/` | seed PG backend + cabinet |
| `monitoring/prometheus/prometheus.yml` | scrape Bitrix server `192.168.2.11` (prod-only) |
---
*Версия 1.0. При изменении интеграций обновлять матрицу в §0 и env-фрагмент в §5.2.*