kin: BATON-002 [Research] UX Designer
This commit is contained in:
commit
057e500d5f
29 changed files with 3530 additions and 0 deletions
371
docs/backend_spec.md
Normal file
371
docs/backend_spec.md
Normal file
|
|
@ -0,0 +1,371 @@
|
|||
# Backend Spec: Baton PWA
|
||||
|
||||
**Версия:** 1.1
|
||||
**Дата:** 2026-03-20
|
||||
**Статус:** Approved (Architect)
|
||||
|
||||
---
|
||||
|
||||
## 1. API Contracts
|
||||
|
||||
### POST /api/register
|
||||
|
||||
Регистрирует UUID→имя пользователя. **Идемпотентен**: повторный вызов с тем же UUID возвращает существующую запись.
|
||||
|
||||
**Request:**
|
||||
```http
|
||||
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:**
|
||||
```json
|
||||
{
|
||||
"user_id": 42,
|
||||
"uuid": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
**Status codes:**
|
||||
|
||||
| Код | Причина |
|
||||
|-----|---------|
|
||||
| 200 | Успешно (новый или существующий) |
|
||||
| 422 | Ошибка валидации Pydantic |
|
||||
| 500 | Внутренняя ошибка сервера |
|
||||
|
||||
---
|
||||
|
||||
### POST /api/signal
|
||||
|
||||
Принимает сигнал тревоги от PWA. Сохраняет в SQLite, добавляет в очередь агрегатора Telegram.
|
||||
|
||||
**Request:**
|
||||
```http
|
||||
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:**
|
||||
```json
|
||||
{
|
||||
"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:**
|
||||
```json
|
||||
{"ok": true}
|
||||
```
|
||||
|
||||
**Status codes:**
|
||||
|
||||
| Код | Причина |
|
||||
|-----|---------|
|
||||
| 200 | Обновление обработано |
|
||||
| 403 | Неверный или отсутствующий X-Telegram-Bot-Api-Secret-Token |
|
||||
| 422 | Невалидный JSON |
|
||||
|
||||
---
|
||||
|
||||
## 2. DB Schema (SQLite WAL)
|
||||
|
||||
```sql
|
||||
-- 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
|
||||
|
||||
После успешной отправки:
|
||||
```sql
|
||||
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
|
||||
|
||||
```python
|
||||
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
|
||||
|
||||
```env
|
||||
# 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)
|
||||
|
||||
1. **CORS origin** — какой домен фронтенда? (Определяется Infra-отделом)
|
||||
2. **Количество uvicorn workers** — один процесс (aggregator в памяти) vs несколько (aggregator должен быть shared via Redis/SQLite)
|
||||
- **Рекомендация:** один uvicorn worker в продакшне для MVP (aggregator в памяти корректен)
|
||||
3. **Команды Telegram-бота** — только `/start` для регистрации, или нужны другие команды?
|
||||
4. **Telegram message format** — показывать реальные имена или UUID?
|
||||
Loading…
Add table
Add a link
Reference in a new issue