Initial commit: svg backend

This commit is contained in:
adminko
2026-03-19 13:39:32 +03:00
commit 85fb2f4bb9
78 changed files with 6161 additions and 0 deletions

467
README.md Normal file
View File

@@ -0,0 +1,467 @@
# 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