improvement for the backend api

This commit is contained in:
ivannoskov
2025-11-19 12:56:04 +03:00
parent b0a9934220
commit d4672ea32b
40 changed files with 2383 additions and 3313 deletions

704
FRONTEND_API.md Normal file
View File

@@ -0,0 +1,704 @@
# API Документация для Frontend
## Базовая информация
- **Base URL**: `/api/v1` (для всех эндпоинтов)
- **Формат данных**: JSON
- **Аутентификация**: JWT Bearer Token (в заголовке `Authorization: Bearer <token>`)
## Авторизация
### Процесс авторизации через Telegram
1. Пользователь запускает Telegram бота командой `/start` с параметром `login`
2. Бот создает одноразовый токен (живет 10 минут) и отправляет ссылку на веб-приложение
3. Фронтенд получает `token` из URL параметра (`/auth/complete?token=...`)
4. Фронтенд отправляет запрос на `GET /api/v1/auth/complete?token=<token>`
5. Бэкенд возвращает JWT токен, который нужно сохранить и использовать для всех последующих запросов
### GET `/api/v1/auth/complete`
Обмен одноразового токена на JWT access token.
**Query параметры:**
- `token` (string, обязательный) - одноразовый токен из Telegram бота
**Response 200:**
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```
**JWT токен содержит:**
- `user_id` (UUID) - ID пользователя
- `telegram_id` (integer) - Telegram ID пользователя
- `username` (string | null) - Telegram username
- `exp` - время истечения токена (7 дней по умолчанию)
**Ошибки:**
- `404` - токен не найден
- `400` - токен уже использован или истек
### GET `/api/v1/auth/me`
Получить информацию о текущем пользователе.
**Headers:**
- `Authorization: Bearer <access_token>`
**Response 200:**
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"telegram_id": 123456789,
"username": "user_name"
}
```
**Поля:**
- `id` (UUID) - ID пользователя в системе
- `telegram_id` (integer) - Telegram ID
- `username` (string | null) - Telegram username (может быть null)
---
## Целевые каналы (Target Channels)
Каналы пользователя, в которых установлен бот.
### GET `/api/v1/target_channels`
Получить список всех целевых каналов пользователя.
**Headers:**
- `Authorization: Bearer <access_token>`
**Response 200:**
```json
{
"target_channels": [
{
"id": "uuid",
"telegram_id": 123456789,
"title": "Название канала",
"username": "channel_username",
"is_active": true
}
]
}
```
### DELETE `/api/v1/target_channels/{channel_id}`
Отключить целевой канал (удалить из системы).
**Headers:**
- `Authorization: Bearer <access_token>`
**Path параметры:**
- `channel_id` (UUID) - ID канала
**Response 200:** пустой ответ
---
## Внешние каналы (External Channels)
Каналы, в которых размещается реклама.
### GET `/api/v1/external_channels/target/{target_channel_id}`
Получить все внешние каналы, связанные с конкретным целевым каналом.
**Headers:**
- `Authorization: Bearer <access_token>`
**Path параметры:**
- `target_channel_id` (UUID) - ID целевого канала
**Response 200:**
```json
{
"external_channels": [
{
"id": "uuid",
"telegram_id": 123456789,
"title": "Внешний канал",
"username": "external_channel",
"description": "Описание канала",
"subscribers_count": 10000
}
]
}
```
### POST `/api/v1/external_channels`
Создать новый внешний канал.
**Headers:**
- `Authorization: Bearer <access_token>`
**Request body:**
```json
{
"telegram_id": 123456789,
"title": "Название канала",
"username": "channel_username",
"description": "Описание",
"subscribers_count": 10000,
"target_channel_ids": ["uuid1", "uuid2"]
}
```
**Поля:**
- `telegram_id` (integer, обязательный) - Telegram ID канала
- `title` (string, обязательный) - название канала
- `username` (string, optional) - username канала (без @)
- `description` (string, optional) - описание канала
- `subscribers_count` (integer, optional) - количество подписчиков
- `target_channel_ids` (array of UUID, обязательный) - список ID целевых каналов для связи
**Response 200:** объект `ExternalChannelOutput`
### PATCH `/api/v1/external_channels/{channel_id}`
Обновить информацию о внешнем канале.
**Headers:**
- `Authorization: Bearer <access_token>`
**Path параметры:**
- `channel_id` (UUID) - ID канала
**Request body:**
```json
{
"title": "Новое название",
"username": "new_username",
"description": "Новое описание",
"subscribers_count": 15000
}
```
**Все поля опциональны.** Отправляйте только те, которые нужно изменить.
**Response 200:** объект `ExternalChannelOutput`
### PATCH `/api/v1/external_channels/{channel_id}/links`
Обновить связи внешнего канала с целевыми каналами.
**Headers:**
- `Authorization: Bearer <access_token>`
**Path параметры:**
- `channel_id` (UUID) - ID канала
**Request body:**
```json
{
"add_target_channel_ids": ["uuid1", "uuid2"],
"remove_target_channel_ids": ["uuid3"]
}
```
**Поля:**
- `add_target_channel_ids` (array of UUID, optional, default=[]) - добавить связи
- `remove_target_channel_ids` (array of UUID, optional, default=[]) - удалить связи
**Response 200:** объект `ExternalChannelOutput`
### DELETE `/api/v1/external_channels/{channel_id}`
Удалить внешний канал.
**Headers:**
- `Authorization: Bearer <access_token>`
**Path параметры:**
- `channel_id` (UUID) - ID канала
**Response 200:** пустой ответ
---
## Креативы (Creatives)
Рекламные сообщения для размещения.
### GET `/api/v1/creatives`
Получить список креативов.
**Headers:**
- `Authorization: Bearer <access_token>`
**Query параметры:**
- `target_channel_id` (UUID, optional) - фильтр по целевому каналу
- `include_archived` (boolean, optional, default=false) - включить архивные
**Response 200:**
```json
{
"creatives": [
{
"id": "uuid",
"name": "Название креатива",
"text": "Текст рекламного сообщения",
"target_channel_id": "uuid",
"target_channel_title": "Название канала",
"created_at": "2024-01-01T12:00:00Z",
"status": "active",
"placements_count": 5
}
]
}
```
**Статусы креатива:**
- `active` - активный
- `archived` - архивный
### GET `/api/v1/creatives/{creative_id}`
Получить конкретный креатив.
**Headers:**
- `Authorization: Bearer <access_token>`
**Path параметры:**
- `creative_id` (UUID) - ID креатива
**Response 200:** объект `CreativeOutput`
### POST `/api/v1/creatives`
Создать новый креатив.
**Headers:**
- `Authorization: Bearer <access_token>`
**Request body:**
```json
{
"name": "Название креатива",
"text": "Текст рекламного сообщения",
"target_channel_id": "uuid"
}
```
**Поля:**
- `name` (string, обязательный) - название креатива
- `text` (string, обязательный) - текст сообщения
- `target_channel_id` (UUID, обязательный) - ID целевого канала
**Response 200:** объект `CreativeOutput`
### PATCH `/api/v1/creatives/{creative_id}`
Обновить креатив.
**Headers:**
- `Authorization: Bearer <access_token>`
**Path параметры:**
- `creative_id` (UUID) - ID креатива
**Request body:**
```json
{
"name": "Новое название",
"text": "Новый текст",
"status": "archived"
}
```
**Все поля опциональны.**
**Response 200:** объект `CreativeOutput`
### DELETE `/api/v1/creatives/{creative_id}`
Удалить креатив.
**Headers:**
- `Authorization: Bearer <access_token>`
**Path параметры:**
- `creative_id` (UUID) - ID креатива
**Response 200:** пустой ответ
---
## Размещения (Placements)
Размещения рекламы во внешних каналах.
### GET `/api/v1/placements`
Получить список размещений.
**Headers:**
- `Authorization: Bearer <access_token>`
**Query параметры:**
- `target_channel_id` (UUID, optional) - фильтр по целевому каналу
- `external_channel_id` (UUID, optional) - фильтр по внешнему каналу
- `creative_id` (UUID, optional) - фильтр по креативу
- `include_archived` (boolean, optional, default=false) - включить архивные
**Response 200:**
```json
{
"placements": [
{
"id": "uuid",
"target_channel_id": "uuid",
"target_channel_title": "Целевой канал",
"external_channel_id": "uuid",
"external_channel_title": "Внешний канал",
"creative_id": "uuid",
"creative_name": "Название креатива",
"placement_date": "2024-01-01T12:00:00Z",
"cost": 1000.5,
"comment": "Комментарий",
"ad_post_url": "https://t.me/channel/123",
"invite_link_type": "approval",
"invite_link": "https://t.me/+unique_invite_link",
"status": "active",
"subscriptions_count": 42,
"views_count": 1500,
"views_availability": "available",
"last_views_fetch_at": "2024-01-02T10:00:00Z",
"created_at": "2024-01-01T12:00:00Z"
}
]
}
```
**Типы пригласительных ссылок (invite_link_type):**
- `public` - обычная открытая ссылка
- `approval` - ссылка с одобрением бота (отслеживание подписчиков)
**Статусы размещения (status):**
- `active` - активное
- `archived` - архивное
**Доступность просмотров (views_availability):**
- `unknown` - ещё не проверялось
- `available` - просмотры доступны через API
- `unavailable` - нет доступа к просмотрам
- `manual` - просмотры вводятся вручную
### GET `/api/v1/placements/{placement_id}`
Получить конкретное размещение.
**Headers:**
- `Authorization: Bearer <access_token>`
**Path параметры:**
- `placement_id` (UUID) - ID размещения
**Response 200:** объект `PlacementOutput`
### POST `/api/v1/placements`
Создать новое размещение.
**Headers:**
- `Authorization: Bearer <access_token>`
**Request body:**
```json
{
"target_channel_id": "uuid",
"external_channel_id": "uuid",
"creative_id": "uuid",
"placement_date": "2024-01-01T12:00:00Z",
"cost": 1000.5,
"comment": "Комментарий",
"ad_post_url": "https://t.me/channel/123",
"invite_link_type": "approval"
}
```
**Поля:**
- `target_channel_id` (UUID, обязательный)
- `external_channel_id` (UUID, обязательный)
- `creative_id` (UUID, обязательный)
- `placement_date` (datetime, обязательный) - дата размещения
- `cost` (float, optional) - стоимость размещения
- `comment` (string, optional) - комментарий
- `ad_post_url` (string, optional) - ссылка на рекламный пост
- `invite_link_type` (enum, optional, default="public") - тип пригласительной ссылки
**Response 200:** объект `PlacementOutput`
### PATCH `/api/v1/placements/{placement_id}`
Обновить размещение.
**Headers:**
- `Authorization: Bearer <access_token>`
**Path параметры:**
- `placement_id` (UUID) - ID размещения
**Request body:**
```json
{
"placement_date": "2024-01-02T12:00:00Z",
"cost": 1500.0,
"comment": "Новый комментарий",
"ad_post_url": "https://t.me/channel/124",
"status": "archived"
}
```
**Все поля опциональны.**
**Response 200:** объект `PlacementOutput`
### DELETE `/api/v1/placements/{placement_id}`
Удалить размещение.
**Headers:**
- `Authorization: Bearer <access_token>`
**Path параметры:**
- `placement_id` (UUID) - ID размещения
**Response 200:** пустой ответ
---
## Просмотры (Views)
Работа с просмотрами рекламных постов.
### GET `/api/v1/placements/{placement_id}/views/history`
Получить историю просмотров размещения.
**Headers:**
- `Authorization: Bearer <access_token>`
**Path параметры:**
- `placement_id` (UUID) - ID размещения
**Query параметры:**
- `from_date` (datetime, optional) - начало периода
- `to_date` (datetime, optional) - конец периода
**Response 200:**
```json
{
"histories": [
{
"id": "uuid",
"placement_id": "uuid",
"views_count": 1500,
"fetched_at": "2024-01-02T10:00:00Z",
"created_at": "2024-01-02T10:00:00Z"
}
]
}
```
### POST `/api/v1/placements/{placement_id}/views/fetch`
Получить актуальные просмотры через Telegram API (запросить обновление).
**Headers:**
- `Authorization: Bearer <access_token>`
**Path параметры:**
- `placement_id` (UUID) - ID размещения
**Response 200:**
```json
{
"placement_id": "uuid",
"views_count": 1500,
"views_availability": "available",
"fetched_at": "2024-01-02T10:00:00Z",
"error_message": null
}
```
**При ошибке получения просмотров:**
```json
{
"placement_id": "uuid",
"views_count": null,
"views_availability": "unavailable",
"fetched_at": "2024-01-02T10:00:00Z",
"error_message": "Не удалось получить просмотры: нет доступа к каналу"
}
```
### POST `/api/v1/placements/{placement_id}/views/manual`
Обновить просмотры вручную (когда автоматическое получение недоступно).
**Headers:**
- `Authorization: Bearer <access_token>`
**Path параметры:**
- `placement_id` (UUID) - ID размещения
**Query параметры:**
- `views_count` (integer, обязательный) - количество просмотров
**Response 200:** объект `PlacementOutput` (обновленное размещение)
---
## Типы данных и enum
### DateTime
Все даты в формате ISO 8601 с timezone (UTC): `2024-01-01T12:00:00Z`
### UUID
Строковое представление UUID: `"550e8400-e29b-41d4-a716-446655440000"`
### CreativeStatus (enum)
- `"active"` - активный
- `"archived"` - архивный
### PlacementStatus (enum)
- `"active"` - активное
- `"archived"` - архивное
### InviteLinkType (enum)
- `"public"` - открытая публичная ссылка
- `"approval"` - ссылка с одобрением ботом
### PostViewsAvailability (enum)
- `"unknown"` - статус не определен
- `"available"` - просмотры доступны через API
- `"unavailable"` - просмотры недоступны
- `"manual"` - просмотры вводятся вручную
---
## Обработка ошибок
### Формат ошибки
Все ошибки возвращаются в стандартном формате FastAPI:
```json
{
"detail": "Описание ошибки"
}
```
### HTTP коды ответов
- `200` - успешный запрос
- `401` - не авторизован (невалидный/истекший токен)
- `403` - доступ запрещен (нет прав на ресурс)
- `404` - ресурс не найден
- `422` - ошибка валидации входных данных
- `500` - внутренняя ошибка сервера
### Авторизация
Для всех защищенных эндпоинтов (все, кроме `/api/v1/auth/complete`) необходимо передавать JWT токен:
```
Authorization: Bearer <access_token>
```
При отсутствии или невалидном токене вернется `401 Unauthorized`.
---
## Примечания
1. **Soft delete**: При удалении сущностей (креативы, размещения, внешние каналы) они помечаются как удаленные (`deleted_at`), но не удаляются физически из БД.
2. **Отслеживание подписчиков**: Работает только для размещений с `invite_link_type: "approval"`. Бот автоматически одобряет запросы на вступление и увеличивает счетчик подписок.
3. **Автоматическое обновление просмотров**: Фоновый worker периодически обновляет просмотры для активных размещений. Можно также запросить обновление вручную через `/api/v1/placements/{id}/views/fetch`.
4. **Целевые каналы подключаются только через Telegram бота**: Нельзя создать целевой канал через API. Нужно добавить бота в канал как администратора, и он автоматически зарегистрирует канал в системе.