13 KiB
Backend Spec: Baton PWA
Версия: 1.1 Дата: 2026-03-20 Статус: Approved (Architect)
1. API Contracts
POST /api/register
Регистрирует UUID→имя пользователя. Идемпотентен: повторный вызов с тем же UUID возвращает существующую запись.
Request:
POST /api/register
Content-Type: application/json
{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"name": "Алиса"
}
| Поле | Тип | Ограничения |
|---|---|---|
| uuid | string | UUID v4, обязателен |
| name | string | 1–100 символов, обязателен |
Response 200 OK:
{
"user_id": 42,
"uuid": "550e8400-e29b-41d4-a716-446655440000"
}
Status codes:
| Код | Причина |
|---|---|
| 200 | Успешно (новый или существующий) |
| 422 | Ошибка валидации Pydantic |
| 500 | Внутренняя ошибка сервера |
POST /api/signal
Принимает сигнал тревоги от PWA. Сохраняет в SQLite, добавляет в очередь агрегатора Telegram.
Request:
POST /api/signal
Content-Type: application/json
{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": 1742478000000,
"geo": {
"lat": 55.7558,
"lon": 37.6173,
"accuracy": 15.0
}
}
| Поле | Тип | Ограничения |
|---|---|---|
| user_id | string | UUID v4 пользователя, обязателен |
| timestamp | int | Unix ms, > 0, обязателен |
| geo | object | null | Необязателен; если передан — все три поля lat/lon/accuracy обязательны |
| geo.lat | float | -90.0 … 90.0 |
| geo.lon | float | -180.0 … 180.0 |
| geo.accuracy | float | > 0, метры |
Response 200 OK:
{
"status": "ok",
"signal_id": 123
}
Status codes:
| Код | Причина |
|---|---|
| 200 | Сигнал принят |
| 422 | Ошибка валидации |
| 500 | Внутренняя ошибка |
POST /api/webhook/telegram
Входящие обновления от Telegram Bot API. Регистрируется через setWebhook при старте сервера.
Headers:
X-Telegram-Bot-Api-Secret-Token: <WEBHOOK_SECRET>
Request body: стандартный Telegram Update object (JSON от Telegram).
Обрабатываемые команды:
| Команда | Действие |
|---|---|
/start |
Регистрация пользователя через Telegram (INSERT OR IGNORE в users с Telegram user_id как UUID) |
Response:
{"ok": true}
Status codes:
| Код | Причина |
|---|---|
| 200 | Обновление обработано |
| 403 | Неверный или отсутствующий X-Telegram-Bot-Api-Secret-Token |
| 422 | Невалидный JSON |
2. DB Schema (SQLite WAL)
-- WAL mode + защита от write-lock (решения #1005, #1002)
PRAGMA journal_mode=WAL;
PRAGMA busy_timeout=5000;
PRAGMA synchronous=NORMAL;
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uuid TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS signals (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_uuid TEXT NOT NULL REFERENCES users(uuid),
timestamp INTEGER NOT NULL,
lat REAL DEFAULT NULL,
lon REAL DEFAULT NULL,
accuracy REAL DEFAULT NULL,
created_at TEXT DEFAULT (datetime('now')),
telegram_batch_id INTEGER DEFAULT NULL
);
CREATE TABLE IF NOT EXISTS telegram_batches (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_text TEXT DEFAULT NULL,
sent_at TEXT DEFAULT NULL,
signals_count INTEGER DEFAULT 0,
status TEXT DEFAULT 'pending'
);
-- Индексы
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_uuid ON users(uuid);
CREATE INDEX IF NOT EXISTS idx_signals_user_uuid ON signals(user_uuid);
CREATE INDEX IF NOT EXISTS idx_signals_created_at ON signals(created_at);
CREATE INDEX IF NOT EXISTS idx_batches_status ON telegram_batches(status);
Инвариант: все три PRAGMA применяются при каждом подключении. Пропуск любой — нарушение решений #1005, #1002.
3. Telegram Aggregator Design
Принцип работы
[POST /signal] ──► add_signal(user_name, ts, geo)
│
[in-memory buffer]
│
asyncio background task (run loop)
│
sleep(10 sec)
│
if buffer not empty:
│
──► flush()
│
формат сообщения
│
send_message()
Буфер и окно
- Хранение: список
list[dict]в памяти, защищённыйasyncio.Lock() - Интервал сброса: 10 секунд (скользящее окно)
- Rate limit: максимум 20 сообщений/минуту в группу, 1 сообщение/секунду (#1014)
- Реализация:
asyncio.sleep(1)после каждого успешногоsend_message
- Реализация:
Формат сообщения
🚨 Получено N сигналов [HH:MM:SS—HH:MM:SS]
Пользователи: name1, name2, name3
📍 С геолокацией: K из N
Если имя пользователя неизвестно — показывать первые 8 символов UUID.
Обработка ошибок Telegram
| Код ответа | Действие |
|---|---|
| 200 OK | Успех, обновить telegram_batch status = 'sent' |
| 429 Too Many Requests | Прочитать retry_after из тела ответа, await asyncio.sleep(retry_after), повторить |
| 400 Bad Request | Логировать ошибку, сигналы не теряются (остаются в SQLite) |
| 5xx | Логировать, повторить через 30 сек (1 попытка) |
Запись batch в SQLite
После успешной отправки:
INSERT INTO telegram_batches (message_text, sent_at, signals_count, status)
VALUES (?, datetime('now'), ?, 'sent');
UPDATE signals SET telegram_batch_id = ? WHERE id IN (...);
4. Middleware & Security
CORS
CORSMiddleware(
allow_origins=[config.FRONTEND_ORIGIN], # env var FRONTEND_ORIGIN
allow_methods=["POST"],
allow_headers=["Content-Type"],
)
FRONTEND_ORIGIN обязателен в .env. Не использовать allow_origins=["*"] в продакшне.
Webhook Secret Validation
Middleware проверяет заголовок X-Telegram-Bot-Api-Secret-Token только для маршрута POST /api/webhook/telegram.
Входящий запрос на /api/webhook/telegram
└── if header != config.WEBHOOK_SECRET → 403 Forbidden (немедленно)
└── else → передать в обработчик
Реализация: Starlette BaseHTTPMiddleware или dependency в роуте (предпочтительно dependency — проще тестировать).
Input Validation
Все входящие тела запросов валидируются через Pydantic v2 models. FastAPI возвращает 422 автоматически при ошибке валидации.
Secrets Management
Все секреты исключительно через переменные окружения (.env + python-dotenv). Ни один секрет не должен логироваться (особенно WEBHOOK_SECRET и BOT_TOKEN).
5. Startup / Shutdown
Startup (lifespan event)
1. init_db()
└── PRAGMA journal_mode=WAL
└── PRAGMA busy_timeout=5000
└── PRAGMA synchronous=NORMAL
└── CREATE TABLE IF NOT EXISTS ... (users, signals, telegram_batches)
└── CREATE INDEX IF NOT EXISTS ...
2. set_webhook(url=config.WEBHOOK_URL, secret=config.WEBHOOK_SECRET)
└── POST https://api.telegram.org/bot{TOKEN}/setWebhook
└── WEBHOOK_URL ОБЯЗАН быть HTTPS (#1011)
└── Если ошибка → залогировать и завершить старт с исключением
3. aggregator = SignalAggregator(interval=10)
asyncio.create_task(aggregator.run())
Shutdown (lifespan event)
1. aggregator.stop() — установить флаг остановки
2. await aggregator.flush() — отправить оставшиеся сигналы из буфера
3. Закрыть все aiosqlite соединения
6. Файловая структура
baton/
├── backend/
│ ├── main.py # FastAPI app, роуты, lifespan events, CORS
│ ├── db.py # SQLite init (WAL), CRUD: register_user, save_signal, get_user_name
│ ├── models.py # Pydantic v2: RegisterRequest/Response, SignalRequest/Response, GeoData
│ ├── telegram.py # send_message, set_webhook, SignalAggregator
│ ├── config.py # Settings через python-dotenv: BOT_TOKEN, CHAT_ID, DB_PATH,
│ │ # WEBHOOK_SECRET, WEBHOOK_URL, FRONTEND_ORIGIN
│ └── middleware.py # WebhookSecretValidator (или FastAPI dependency)
├── tests/
│ ├── conftest.py # Фикстуры: in-memory SQLite, AsyncClient, respx mock
│ ├── test_register.py
│ ├── test_signal.py
│ ├── test_telegram.py
│ ├── test_webhook.py
│ └── test_db.py
├── docs/
│ ├── backend_spec.md # Этот файл
│ └── backend_review.md # Создаётся reviewer после имплементации
├── requirements.txt # fastapi, uvicorn[standard], aiosqlite, httpx, python-dotenv, pydantic>=2.0
├── requirements-dev.txt # pytest, pytest-asyncio, httpx, respx
└── .env.example
7. .env.example
# Telegram
BOT_TOKEN=your_telegram_bot_token_here
CHAT_ID=-1001234567890
WEBHOOK_SECRET=your_random_secret_here
WEBHOOK_URL=https://yourdomain.com/api/webhook/telegram
# Database
DB_PATH=baton.db
# CORS
FRONTEND_ORIGIN=https://yourdomain.com
8. Соответствие решениям
| Решение | Статус | Реализация |
|---|---|---|
| #999: HTTPS для PWA/Geolocation | ✅ | WEBHOOK_URL обязан быть HTTPS; задокументировано в constraints |
| #1001: Background Sync fallback | ✅ | Не касается бэкенда; offline паттерн — на фронте |
| #1002: SQLite write-lock | ✅ | busy_timeout=5000 при каждом подключении |
| #1003: Требования не понижать | ✅ | Все 3 эндпоинта обязательны |
| #1004: bcrypt через run_in_executor | ✅ | UUID auth — без bcrypt; если добавится — только run_in_executor |
| #1005: WAL + busy_timeout + synchronous | ✅ | Все три PRAGMA вместе в init_db() |
| #1009: setWebhook vs getUpdates | ✅ | ТОЛЬКО setWebhook при старте; getUpdates запрещён |
| #1010: webhook secret validation | ✅ | Middleware/dependency на /api/webhook/telegram |
| #1011: HTTPS для webhook | ✅ | WEBHOOK_URL в .env.example — HTTPS |
| #1013: UUID v4 stateless auth | ✅ | register принимает UUID из localStorage |
| #1014: Telegram rate limit 20/min | ✅ | SignalAggregator: 10-сек окно, sleep(1) между отправками |
9. Открытые вопросы (для Director/PM)
- CORS origin — какой домен фронтенда? (Определяется Infra-отделом)
- Количество uvicorn workers — один процесс (aggregator в памяти) vs несколько (aggregator должен быть shared via Redis/SQLite)
- Рекомендация: один uvicorn worker в продакшне для MVP (aggregator в памяти корректен)
- Команды Telegram-бота — только
/startдля регистрации, или нужны другие команды? - Telegram message format — показывать реальные имена или UUID?