Initial commit: svg backend
This commit is contained in:
467
README.md
Normal file
467
README.md
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user