Introduce reusable ParserProfile and ParseChannel, pair jobs with enqueue flatten, Events admin CRUD, and drop inline/legacy parser-builder UI and aliases. Co-authored-by: Cursor <cursoragent@cursor.com>
183 lines
7.5 KiB
Markdown
183 lines
7.5 KiB
Markdown
# Архитектура ЦА (Analytics Center)
|
||
|
||
Центр аналитики — ядро MapMil: PostgreSQL, FastAPI, Vue admin/карта, distribution API для ПИ.
|
||
|
||
Общий контекст: [docs/architecture-overview.md](../../docs/architecture-overview.md).
|
||
ПИ отдельно: [DISTRIBUTION.md](DISTRIBUTION.md).
|
||
|
||
---
|
||
|
||
## Общими чертами
|
||
|
||
ЦА:
|
||
|
||
- хранит события, jobs, объекты карты, consumers;
|
||
- отдаёт единственный UI (`ca-frontend`);
|
||
- ставит задания в Redis для ЦП;
|
||
- принимает ingest от ЦП;
|
||
- отдаёт срезы внешним системам через `/api/v1/events`.
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
UI[ca-frontend Vue]
|
||
API[ca-api FastAPI]
|
||
DB[(PostgreSQL)]
|
||
Redis[(Redis)]
|
||
CP[CP workers]
|
||
|
||
UI --> API
|
||
API --> DB
|
||
API -->|enqueue| Redis
|
||
Redis --> CP
|
||
CP -->|/internal/*| API
|
||
```
|
||
|
||
Контейнеры: `ca-db`, `ca-api`, `ca-frontend` (+ общий `redis`).
|
||
|
||
---
|
||
|
||
## Подробнее
|
||
|
||
### Структура
|
||
|
||
```text
|
||
centers/analytics/
|
||
├── ARCHITECTURE.md
|
||
├── DISTRIBUTION.md
|
||
├── api/
|
||
│ ├── Dockerfile
|
||
│ ├── requirements.txt
|
||
│ └── app/
|
||
│ ├── main.py # lifespan: migrations, scheduler, seed
|
||
│ ├── database.py
|
||
│ ├── models.py
|
||
│ ├── schemas.py
|
||
│ ├── deps.py # X-Internal-Token
|
||
│ ├── seed.py
|
||
│ ├── storage.py # uploads
|
||
│ ├── routers/
|
||
│ │ ├── objects.py # /api/health, /api/objects, media
|
||
│ │ ├── map.py # /api/map/*
|
||
│ │ ├── admin.py # /admin/* (jobs, events, consumers)
|
||
│ │ ├── parser_profiles.py # /admin/parser-profiles/*
|
||
│ │ ├── parse_channels.py # /admin/parse-channels/*
|
||
│ │ ├── internal.py # /internal/* (только ЦП)
|
||
│ │ └── v1.py # /api/v1/* (ПИ)
|
||
│ └── services/
|
||
│ ├── jobs.py # Redis RPUSH + flatten Profile/Channel
|
||
│ ├── scheduler.py # периодический re-queue
|
||
│ ├── ingest.py # дедуп + map sync
|
||
│ ├── parser_builder.py # DeepSeek → HeuristicProfile (один раз)
|
||
│ ├── filtering.py
|
||
│ ├── map_query.py
|
||
│ ├── events_query.py
|
||
│ ├── analytics.py
|
||
│ └── migrations.py
|
||
└── frontend/
|
||
├── Dockerfile # Vite build + nginx
|
||
├── nginx.conf # proxy /api /admin /internal → ca-api
|
||
└── src/
|
||
├── views/ # Map, Channels, Profiles, Parsers, …
|
||
├── components/ # карта, CRUD объектов
|
||
├── api/ # HTTP-клиенты
|
||
└── router/index.ts
|
||
```
|
||
|
||
### Модели данных
|
||
|
||
| Модель | Таблица | Назначение |
|
||
|--------|--------|------------|
|
||
| `Event` | `events` | Нормализованное событие; UK `source_url` |
|
||
| `ParserProfile` | `parser_profiles` | Статичный heuristic-профиль (JSON-правила) |
|
||
| `ParseChannel` | `parse_channels` | Канал (Telegram handle) |
|
||
| `ParseJob` | `parse_jobs` | Связка канал+профиль, интервал, статус; FK `profile_id` + `channel_id` |
|
||
| `MapObject` | `map_objects` | Точка на карте (event или ручная) |
|
||
| `ObjectMedia` | `object_media` | Файлы к объектам |
|
||
| `Consumer` | `consumers` | Подписчик ПИ (hash ключа) |
|
||
| `ConsumerFilter` | `consumer_filters` | Фильтры среза ПИ |
|
||
|
||
### HTTP-поверхности
|
||
|
||
| Prefix | Кто вызывает | Содержание |
|
||
|--------|--------------|------------|
|
||
| `/api/*` | UI, публичный health | Карта, объекты, медиа |
|
||
| `/admin/*` | UI admin | Jobs, channels, profiles, events, analytics, consumers |
|
||
| `/internal/*` | Только ЦП | ingest, job status, listener subscriptions |
|
||
| `/api/v1/*` | Внешние клиенты | Events с Bearer-ключом |
|
||
|
||
Internal защищён заголовком `X-Internal-Token` (`INTERNAL_TOKEN`).
|
||
|
||
### Профили, каналы и пары
|
||
|
||
1. **Канал** (`CRUD /admin/parse-channels`) — handle Telegram + метаданные.
|
||
2. **Профиль** (`CRUD /admin/parser-profiles`) — Generate (DeepSeek один раз по образцу) → Preview → Save `heuristic_profile`.
|
||
3. **Пара** (`POST /admin/jobs` с `channel_id` + `profile_id`) → `ParseJob`; при enqueue ЦА **разворачивает** пару в плоский `source_config`:
|
||
|
||
```json
|
||
{
|
||
"channel": "<ParseChannel.channel>",
|
||
"limit": 100,
|
||
"extract_mode": "profile",
|
||
"heuristic_profile": { "...из ParserProfile..." }
|
||
}
|
||
```
|
||
|
||
ЦП по-прежнему получает только Redis payload — без доступа к таблицам Profile/Channel.
|
||
|
||
Roadmap: кастомные пользовательские таблицы подменяют список `target-fields` при генерации; профиль будет ссылаться на `schema_id`.
|
||
|
||
### Jobs и scheduler
|
||
|
||
1. `POST /admin/jobs` / retry → запись `ParseJob` + `enqueue_parse_job` (flatten + RPUSH).
|
||
2. Очередь: `cp:jobs:{family}` из `contracts/queues.py`.
|
||
3. `services/scheduler.py` — тик ~30 с, повторная постановка активных jobs по `interval_seconds`.
|
||
|
||
### Ingest
|
||
|
||
`POST /internal/ingest` → `services/ingest.py`:
|
||
|
||
- дедуп по `source_url`;
|
||
- создание `Event`;
|
||
- при координатах — sync `MapObject`;
|
||
- batch обновляет статус job; `listener: true` — нет.
|
||
|
||
### Frontend (маршруты)
|
||
|
||
| Path | View |
|
||
|------|------|
|
||
| `/` | `MapViewPage.vue` |
|
||
| `/channels` | `ChannelsView.vue` |
|
||
| `/parser-profiles` | `ParserProfilesView.vue` |
|
||
| `/parsers` | `ParsersView.vue` (связки канал+профиль) |
|
||
| `/events` | `EventsView.vue` |
|
||
| `/analytics` | `AnalyticsView.vue` |
|
||
| `/consumers` | `ConsumersView.vue` |
|
||
|
||
Карта: Leaflet, фильтры дат/региона/темы/источника, CRUD объектов (ПКМ), медиа, таймлайн появления (если включён в UI).
|
||
|
||
### Nginx
|
||
|
||
`ca-frontend` слушает `:80` (с хоста `:8080`), проксирует backend-пути на `http://ca-api:8000`. Лимит тела для медиа задаётся в `nginx.conf`.
|
||
|
||
### Env (ЦА)
|
||
|
||
| Переменная | Назначение |
|
||
|------------|------------|
|
||
| `DATABASE_URL` | PostgreSQL |
|
||
| `REDIS_URL` | Очереди |
|
||
| `INTERNAL_TOKEN` | Auth ЦП ↔ ЦА |
|
||
| `TEST_PI_API_KEY` | Seed consumer `test-pi` |
|
||
| `UPLOAD_DIR` | Медиа (по умолчанию `/data/uploads`) |
|
||
| `DEEPSEEK_API_KEY` | Generate профиля на ca-api; не нужен для preview/runtime |
|
||
| `DEEPSEEK_BASE_URL` / `DEEPSEEK_MODEL` | Опционально |
|
||
|
||
### Типовые точки входа в код
|
||
|
||
| Задача | Файл |
|
||
|--------|------|
|
||
| Новый admin endpoint | `routers/admin.py` |
|
||
| Логика ingest | `services/ingest.py` |
|
||
| Фильтры карты | `services/map_query.py` + `routers/map.py` |
|
||
| Новый экран UI | `frontend/src/views/` + `router/index.ts` |
|
||
| Поля парсера в форме | `ParsersView.vue` (+ contracts) |
|