468 lines
10 KiB
Markdown
468 lines
10 KiB
Markdown
# SVG Service
|
||
|
||
Backend-сервис для загрузки, sanitization, normalization, хранения и базовой эксплуатации SVG-схем с местами, секторами, группами и pricing.
|
||
|
||
## Текущее состояние
|
||
|
||
Реализовано:
|
||
|
||
- загрузка SVG-файла
|
||
- безопасная первичная обработка SVG
|
||
- sanitization запрещённых элементов и атрибутов
|
||
- normalization в машиночитаемый JSON snapshot
|
||
- хранение original / sanitized / normalized файлов
|
||
- PostgreSQL persistence
|
||
- registry uploads / schemes / versions
|
||
- lifecycle схем:
|
||
- draft
|
||
- publish
|
||
- unpublish
|
||
- rollback
|
||
- create next draft version
|
||
- persistence доменных сущностей:
|
||
- sectors
|
||
- groups
|
||
- seats
|
||
- pricing v1:
|
||
- pricing categories
|
||
- price rules
|
||
- effective price resolution
|
||
- test mode preview API
|
||
- audit/history API
|
||
|
||
---
|
||
|
||
## Стек
|
||
|
||
- Python 3.11+
|
||
- FastAPI
|
||
- Pydantic
|
||
- SQLAlchemy Async
|
||
- PostgreSQL
|
||
- Alembic
|
||
- Docker Compose
|
||
|
||
---
|
||
|
||
## Запуск
|
||
|
||
### 1. Сборка и запуск
|
||
|
||
docker compose build --no-cache
|
||
docker compose up -d
|
||
|
||
### 2. Применение миграций
|
||
|
||
docker compose run --rm svg-service alembic -c /app/alembic.ini upgrade head
|
||
|
||
### 3. Проверка
|
||
|
||
curl -s http://127.0.0.1:9020/healthz
|
||
|
||
Ожидаемый ответ:
|
||
|
||
{"status":"ok"}
|
||
|
||
---
|
||
|
||
## Аутентификация
|
||
|
||
Во все защищённые эндпойнты передаётся заголовок:
|
||
|
||
X-API-Key: <key>
|
||
|
||
Локально использовались ключи из `.env`.
|
||
|
||
---
|
||
|
||
## Основные env-переменные
|
||
|
||
Примерно используются такие параметры:
|
||
|
||
- `APP_NAME`
|
||
- `APP_ENV`
|
||
- `APP_PORT`
|
||
- `API_V1_PREFIX`
|
||
- `AUTH_HEADER_NAME`
|
||
- `ADMIN_API_KEY`
|
||
- `VIEWER_API_KEY`
|
||
- `SVG_MAX_FILE_SIZE_BYTES`
|
||
- `SVG_MAX_ELEMENTS`
|
||
- `SVG_ALLOW_INTERNAL_USE_REFERENCES_ONLY`
|
||
- `SVG_FORBID_FOREIGN_OBJECT_V1`
|
||
- `SVG_FORBID_STYLE_V1`
|
||
- `SVG_FORBID_IMAGE_V1`
|
||
- `POSTGRES_HOST`
|
||
- `POSTGRES_PORT`
|
||
- `POSTGRES_DB`
|
||
- `POSTGRES_USER`
|
||
- `POSTGRES_PASSWORD`
|
||
- `DATABASE_URL`
|
||
|
||
---
|
||
|
||
## Storage layout
|
||
|
||
На файловой системе используются каталоги:
|
||
|
||
- `storage/original`
|
||
- `storage/sanitized`
|
||
- `storage/normalized`
|
||
|
||
Каждая загрузка сохраняется в свой каталог по `upload_id`.
|
||
|
||
---
|
||
|
||
# API
|
||
|
||
## 1. Service / auth / diagnostics
|
||
|
||
### GET `/`
|
||
Базовая информация о сервисе.
|
||
|
||
### GET `/healthz`
|
||
Healthcheck.
|
||
|
||
### GET `/api/v1/ping`
|
||
Проверка API и auth.
|
||
|
||
### GET `/api/v1/auth/me`
|
||
Возвращает текущую роль по API key.
|
||
|
||
### GET `/api/v1/db/ping`
|
||
Проверка доступности PostgreSQL.
|
||
|
||
### GET `/api/v1/manifest`
|
||
Возвращает manifest сервиса:
|
||
- лимиты SVG
|
||
- правила sanitization
|
||
- extraction contract
|
||
|
||
---
|
||
|
||
## 2. Uploads
|
||
|
||
### POST `/api/v1/schemes/upload`
|
||
Загружает SVG, выполняет:
|
||
- validate
|
||
- inspect
|
||
- sanitize
|
||
- normalize
|
||
- storage save
|
||
- create upload record
|
||
- create scheme
|
||
- create version 1
|
||
- persist sectors/groups/seats
|
||
- audit event
|
||
|
||
#### Form-data
|
||
- `file`: SVG-файл
|
||
|
||
#### Ответ
|
||
Возвращает:
|
||
- `upload_id`
|
||
- имя файла
|
||
- размеры
|
||
- counts
|
||
- storage paths
|
||
- accepted flag
|
||
|
||
---
|
||
|
||
### GET `/api/v1/uploads`
|
||
Список upload records.
|
||
|
||
### GET `/api/v1/uploads/{upload_id}`
|
||
Детали upload record.
|
||
|
||
### GET `/api/v1/uploads/{upload_id}/normalized`
|
||
Чтение normalized JSON snapshot.
|
||
|
||
---
|
||
|
||
## 3. Schemes
|
||
|
||
### GET `/api/v1/schemes`
|
||
Список схем.
|
||
|
||
### GET `/api/v1/schemes/{scheme_id}`
|
||
Детали схемы.
|
||
|
||
### GET `/api/v1/schemes/{scheme_id}/current`
|
||
Текущая version схемы.
|
||
|
||
### GET `/api/v1/schemes/{scheme_id}/versions`
|
||
Список всех version схемы.
|
||
|
||
### POST `/api/v1/schemes/{scheme_id}/versions`
|
||
Создаёт новую draft version из current version.
|
||
|
||
Что делает:
|
||
- создаёт `version_number = current + 1`
|
||
- копирует metadata snapshot
|
||
- копирует sectors/groups/seats
|
||
- переключает `current_version_number`
|
||
- сбрасывает схему в `draft`
|
||
- пишет audit event
|
||
|
||
---
|
||
|
||
## 4. Scheme lifecycle
|
||
|
||
### POST `/api/v1/schemes/{scheme_id}/publish`
|
||
Публикует current version.
|
||
|
||
Что делает:
|
||
- `scheme.status = published`
|
||
- `published_at = now()`
|
||
- current version получает статус `published`
|
||
- пишет audit event
|
||
|
||
### POST `/api/v1/schemes/{scheme_id}/unpublish`
|
||
Снимает публикацию.
|
||
|
||
Что делает:
|
||
- `scheme.status = draft`
|
||
- `published_at = null`
|
||
- current version переводится в `draft`
|
||
- пишет audit event
|
||
|
||
### POST `/api/v1/schemes/{scheme_id}/rollback`
|
||
Откатывает current version на указанную version.
|
||
|
||
#### JSON body
|
||
{
|
||
"target_version_number": 1
|
||
}
|
||
|
||
Что делает:
|
||
- переключает `current_version_number`
|
||
- сбрасывает публикацию
|
||
- схема становится `draft`
|
||
- пишет audit event
|
||
|
||
---
|
||
|
||
## 5. Current domain data
|
||
|
||
### GET `/api/v1/schemes/{scheme_id}/current/sectors`
|
||
Сектора текущей версии.
|
||
|
||
### GET `/api/v1/schemes/{scheme_id}/current/groups`
|
||
Группы текущей версии.
|
||
|
||
### GET `/api/v1/schemes/{scheme_id}/current/seats`
|
||
Места текущей версии.
|
||
|
||
Seat содержит:
|
||
- `seat_id`
|
||
- `sector_id`
|
||
- `group_id`
|
||
- `row_label`
|
||
- `seat_number`
|
||
- geometry fields
|
||
- tag / classes
|
||
|
||
---
|
||
|
||
## 6. Pricing
|
||
|
||
## 6.1 Pricing categories
|
||
|
||
### POST `/api/v1/schemes/{scheme_id}/pricing/categories`
|
||
Создать pricing category.
|
||
|
||
#### JSON body
|
||
{
|
||
"name": "VIP",
|
||
"code": "VIP"
|
||
}
|
||
|
||
### PUT `/api/v1/schemes/{scheme_id}/pricing/categories/{pricing_category_id}`
|
||
Обновить pricing category.
|
||
|
||
#### JSON body
|
||
{
|
||
"name": "Econom Plus",
|
||
"code": "ECO_PLUS"
|
||
}
|
||
|
||
### DELETE `/api/v1/schemes/{scheme_id}/pricing/categories/{pricing_category_id}`
|
||
Удалить pricing category.
|
||
|
||
---
|
||
|
||
## 6.2 Price rules
|
||
|
||
### POST `/api/v1/schemes/{scheme_id}/pricing/rules`
|
||
Создать price rule.
|
||
|
||
#### JSON body
|
||
{
|
||
"pricing_category_id": "....",
|
||
"target_type": "sector",
|
||
"target_ref": "vip",
|
||
"amount": "2500.00",
|
||
"currency": "RUB"
|
||
}
|
||
|
||
#### Ограничения v1
|
||
- `target_type` только:
|
||
- `sector`
|
||
- `group`
|
||
- `seat`
|
||
- `currency` только `RUB`
|
||
- `amount` хранится как `NUMERIC(12,2)`
|
||
|
||
### PUT `/api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}`
|
||
Обновить rule.
|
||
|
||
### DELETE `/api/v1/schemes/{scheme_id}/pricing/rules/{price_rule_id}`
|
||
Удалить rule.
|
||
|
||
### GET `/api/v1/schemes/{scheme_id}/pricing`
|
||
Вернуть все pricing categories и rules схемы.
|
||
|
||
---
|
||
|
||
## 7. Effective pricing
|
||
|
||
### GET `/api/v1/schemes/{scheme_id}/current/seats/{seat_id}/price`
|
||
Рассчитать effective price для места.
|
||
|
||
#### Приоритет поиска rules
|
||
1. `seat`
|
||
2. `group`
|
||
3. `sector`
|
||
|
||
#### Ответ
|
||
Возвращает:
|
||
- `seat_id`
|
||
- `sector_id`
|
||
- `group_id`
|
||
- `matched_rule_level`
|
||
- `matched_target_ref`
|
||
- `pricing_category_id`
|
||
- `amount`
|
||
- `currency`
|
||
|
||
Если правило не найдено:
|
||
- `404 No pricing rule matched current seat`
|
||
|
||
Если место не найдено:
|
||
- `404 Seat not found in current scheme version`
|
||
|
||
---
|
||
|
||
## 8. Test mode
|
||
|
||
### GET `/api/v1/schemes/{scheme_id}/test/seats/{seat_id}`
|
||
Preview для frontend test mode.
|
||
|
||
Возвращает:
|
||
- seat metadata
|
||
- `selectable`
|
||
- `has_price`
|
||
- pricing info, если найдено effective rule
|
||
|
||
Логика:
|
||
- если price найден -> `selectable = true`
|
||
- если price не найден -> `selectable = false`
|
||
|
||
---
|
||
|
||
## 9. Audit
|
||
|
||
### GET `/api/v1/schemes/{scheme_id}/audit`
|
||
История событий схемы.
|
||
|
||
Сейчас в audit пишутся:
|
||
- `scheme.created_from_upload`
|
||
- `scheme.published`
|
||
- `scheme.unpublished`
|
||
- `scheme.rolled_back`
|
||
- `scheme.version.created`
|
||
- `pricing.category.created`
|
||
- `pricing.category.updated`
|
||
- `pricing.category.deleted`
|
||
- `pricing.rule.created`
|
||
- `pricing.rule.updated`
|
||
- `pricing.rule.deleted`
|
||
|
||
---
|
||
|
||
## Пример базового сценария
|
||
|
||
### 1. Загрузить SVG
|
||
|
||
curl -s -H 'X-API-Key: admin-local-dev-key' \
|
||
-F 'file=@sample-contract.svg;type=image/svg+xml' \
|
||
http://127.0.0.1:9020/api/v1/schemes/upload
|
||
|
||
### 2. Получить список схем
|
||
|
||
curl -s -H 'X-API-Key: admin-local-dev-key' \
|
||
http://127.0.0.1:9020/api/v1/schemes
|
||
|
||
### 3. Создать pricing category
|
||
|
||
curl -s -X POST -H 'Content-Type: application/json' -H 'X-API-Key: admin-local-dev-key' \
|
||
-d '{"name":"VIP","code":"VIP"}' \
|
||
http://127.0.0.1:9020/api/v1/schemes/<scheme_id>/pricing/categories
|
||
|
||
### 4. Создать pricing rule
|
||
|
||
curl -s -X POST -H 'Content-Type: application/json' -H 'X-API-Key: admin-local-dev-key' \
|
||
-d '{"pricing_category_id":"<category_id>","target_type":"sector","target_ref":"vip","amount":"2500.00","currency":"RUB"}' \
|
||
http://127.0.0.1:9020/api/v1/schemes/<scheme_id>/pricing/rules
|
||
|
||
### 5. Проверить цену места
|
||
|
||
curl -s -H 'X-API-Key: admin-local-dev-key' \
|
||
http://127.0.0.1:9020/api/v1/schemes/<scheme_id>/current/seats/seat-a1/price
|
||
|
||
### 6. Проверить test mode preview
|
||
|
||
curl -s -H 'X-API-Key: admin-local-dev-key' \
|
||
http://127.0.0.1:9020/api/v1/schemes/<scheme_id>/test/seats/seat-a1
|
||
|
||
---
|
||
|
||
## Ограничения текущего v1
|
||
|
||
- редактирование seats/groups/sectors через API пока не реализовано
|
||
- нет bulk update editor API
|
||
- нет optimistic locking
|
||
- нет soft delete policy
|
||
- нет полноценной multi-user edit coordination
|
||
- routes пока не разнесены по отдельным router-модулям
|
||
- нет полного integration/e2e test набора
|
||
- frontend отсутствует
|
||
|
||
---
|
||
|
||
## Что делать дальше
|
||
|
||
### Приоритет 1
|
||
Разработать frontend / test UI:
|
||
- список схем
|
||
- просмотр current seats/sectors/groups
|
||
- test mode click preview
|
||
- pricing management UI
|
||
|
||
### Приоритет 2
|
||
Добавить editor operations:
|
||
- update seat/group/sector in draft version
|
||
- bulk updates
|
||
- draft save flow
|
||
|
||
### Приоритет 3
|
||
Усилить backend:
|
||
- router decomposition
|
||
- service-layer cleanup
|
||
- pagination/filters for audit
|
||
- integration tests
|
||
- concurrency policy
|
||
- publish validation rules
|
||
|