Initial project plan and architecture
This commit is contained in:
1 parent
95694c58c2
commit
a55cf6026c
2 files changed
+483
-1
No files matched your search
+318
@@ -0,0 +1,318 @@
|
||||
# ROI24 — Техническая архитектура
|
||||
|
||||
## Общая схема
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ ИСТОЧНИКИ ДАННЫХ │
|
||||
│ 1С:Предприятие │ Банки │ CRM │ Реклама │ Маркетплейсы │
|
||||
└────────┬─────────┬───────┬─────┬────────┴───────────────────┘
|
||||
│ │ │ │
|
||||
┌────▼────┐ ┌──▼────┐ │ │
|
||||
│ 1C Ad. │ │Bank. │ │Ad. │ │CRM Ad. │
|
||||
└────┬────┘ └──┬────┘ │ └──┬──────┘
|
||||
│ │ │ │
|
||||
┌────▼─────────▼───────▼───────▼─────────┐
|
||||
│ Python ETL Pipeline │
|
||||
│ APScheduler / Celery + Redis │
|
||||
└─────────────────┬──────────────────────┘
|
||||
│
|
||||
┌─────────────────▼──────────────────────┐
|
||||
│ PostgreSQL 16 │
|
||||
│ raw_data │ aggregated │ metadata │
|
||||
└─────────────────┬──────────────────────┘
|
||||
│
|
||||
┌─────────────────▼──────────────────────┐
|
||||
│ FastAPI (REST API) │
|
||||
│ GET /api/dashboard/{client_id} │
|
||||
│ GET /api/metrics/{client_id}/{type} │
|
||||
│ GET /api/trend/{client_id} │
|
||||
└─────────────────┬──────────────────────┘
|
||||
│
|
||||
┌─────────────────▼──────────────────────┐
|
||||
│ HTML Dashboard (ECharts) │
|
||||
│ Single-page SPA или server-rendered │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Схема БД (PostgreSQL)
|
||||
|
||||
```sql
|
||||
-- Клиент
|
||||
CREATE TABLE clients (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(256) NOT NULL,
|
||||
niche VARCHAR(100), -- ecommerce, services, retail, b2b
|
||||
status VARCHAR(20) DEFAULT 'active',
|
||||
plan VARCHAR(20) DEFAULT 'start', -- start, business, premium
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Подключённые источники данных
|
||||
CREATE TABLE data_sources (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
client_id UUID REFERENCES clients(id),
|
||||
source_type VARCHAR(50), -- 1c, bank, ad_platform, crm
|
||||
source_name VARCHAR(100), -- СберБизнес, Яндекс.Директ, amoCRM
|
||||
config JSONB NOT NULL, -- credentials, API keys (encrypted)
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
last_sync TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Исходные данные (сырые)
|
||||
CREATE TABLE raw_data (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
source_id UUID REFERENCES data_sources(id),
|
||||
data JSONB NOT NULL, -- сырые данные из API
|
||||
fetched_at TIMESTAMP NOT NULL, -- когда скачаны
|
||||
processed BOOLEAN DEFAULT FALSE -- обработаны ETL
|
||||
);
|
||||
|
||||
-- Агрегированные данные (для дашборда)
|
||||
CREATE TABLE metrics (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
client_id UUID REFERENCES clients(id),
|
||||
metric_key VARCHAR(100) NOT NULL, -- revenue, profit, orders, etc.
|
||||
metric_value DECIMAL(15,2) NOT NULL,
|
||||
period_start DATE NOT NULL,
|
||||
period_end DATE,
|
||||
dimensions JSONB, -- city, channel, manager, etc.
|
||||
source TEXT, -- откуда рассчитано
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(client_id, metric_key, period_start, dimensions::text)
|
||||
);
|
||||
|
||||
-- Индекс для быстрого поиска по дашборду
|
||||
CREATE INDEX idx_metrics_client_period ON metrics(client_id, period_start DESC);
|
||||
CREATE INDEX idx_metrics_key ON metrics(metric_key);
|
||||
|
||||
-- История изменений дашборда (аудит)
|
||||
CREATE TABLE dashboard_changes (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
client_id UUID REFERENCES clients(id),
|
||||
metric_key VARCHAR(100),
|
||||
old_value DECIMAL(15,2),
|
||||
new_value DECIMAL(15,2),
|
||||
change_pct DECIMAL(10,2),
|
||||
changed_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Настройки дашборда (какие метрики показывать клиенту)
|
||||
CREATE TABLE dashboard_config (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
client_id UUID REFERENCES clients(id) UNIQUE,
|
||||
template VARCHAR(50), -- ecommerce, services, retails
|
||||
metrics JSONB, -- какие метрики включены
|
||||
refresh_interval INTEGER DEFAULT 1440, -- минут
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
```
|
||||
|
||||
## ETL Pipeline
|
||||
|
||||
### Структура адаптера
|
||||
|
||||
```
|
||||
roi24/PIPELINE/adapters/
|
||||
├── base.py # Базовый класс адаптера
|
||||
├── 1c/
|
||||
│ ├── __init__.py
|
||||
│ ├── xmlrpc.py # Подключение через XML-RPC 1С
|
||||
│ ├── direct_db.py # Прямой доступ к БД 1С
|
||||
│ └── exporters.py # Экспорт данных
|
||||
├── bank/
|
||||
│ ├── __init__.py
|
||||
│ ├── sberbank.py # СберБизнес
|
||||
│ ├── tinkoff.py # Тинькофф
|
||||
│ ├── tochka.py # Точка Банк
|
||||
│ └── csv_import.py # Импорт из CSV
|
||||
├── ad/
|
||||
│ ├── __init__.py
|
||||
│ ├── yandex_direct.py # Яндекс.Директ API
|
||||
│ └── vk_ads.py # VK Ads
|
||||
├── crm/
|
||||
│ ├── __init__.py
|
||||
│ ├── bitrix24.py # Битрикс24 REST API
|
||||
│ └── amo.py # amoCRM API
|
||||
└── marketplace/
|
||||
├── __init__.py
|
||||
└── ozon.py
|
||||
```
|
||||
|
||||
### Базовый адаптер
|
||||
|
||||
```python
|
||||
# PIPELINE/adapters/base.py
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
class BaseAdapter(ABC):
|
||||
"""Базовый класс для всех адаптеров источников данных."""
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
self.last_sync: Optional[datetime] = None
|
||||
|
||||
@abstractmethod
|
||||
def fetch(self, date_from: str, date_to: str) -> list[dict]:
|
||||
"""Получить данные за период."""
|
||||
pass
|
||||
|
||||
def _save_raw(self, data: list[dict], source_id: str):
|
||||
"""Сохранить сырые данные в PostgreSQL."""
|
||||
pass
|
||||
|
||||
def _aggregate(self, raw: list[dict]) -> list[dict]:
|
||||
"""Преобразовать сырые данные в метрики."""
|
||||
pass
|
||||
|
||||
def sync(self, date_from: str, date_to: str, source_id: str):
|
||||
"""Полный цикл синхронизации."""
|
||||
raw = self.fetch(date_from, date_to)
|
||||
self._save_raw(raw, source_id)
|
||||
metrics = self._aggregate(raw)
|
||||
self.last_sync = datetime.now()
|
||||
return metrics
|
||||
```
|
||||
|
||||
### Типовой цикл ETL
|
||||
|
||||
```
|
||||
1. APScheduler запускает job каждые N минут
|
||||
2. Job для каждого источника данных:
|
||||
a. Вызывает adapter.fetch(date_from, date_to)
|
||||
b. Сохраняет сырые данные в raw_data
|
||||
c. Агрегирует в метрики (metrics table)
|
||||
d. Помечает raw_data как processed
|
||||
3. При изменении агрегированных метрик > 10% —
|
||||
записывает в dashboard_changes
|
||||
```
|
||||
|
||||
## FastAPI API
|
||||
|
||||
### Эндпоинты
|
||||
|
||||
```
|
||||
GET /api/health # health-check
|
||||
GET /api/dashboard/{client_id} # все метрики клиента
|
||||
GET /api/metrics/{client_id}/{metric} # конкретная метрика
|
||||
GET /api/trend/{client_id}/{metric} # тренд (сравнение периодов)
|
||||
GET /api/export/{client_id}/{format} # экспорт в CSV/PDF
|
||||
GET /api/clients # список клиентов
|
||||
POST /api/clients/{id}/connect # подключить источник
|
||||
DELETE /api/clients/{id}/sources/{src_id} # удалить источник
|
||||
```
|
||||
|
||||
### Пример ответа
|
||||
|
||||
```json
|
||||
{
|
||||
"client_id": "uuid-here",
|
||||
"generated_at": "2026-06-20T10:00:00Z",
|
||||
"metrics": {
|
||||
"revenue": {
|
||||
"today": 125000,
|
||||
"yesterday": 98000,
|
||||
"week": 820000,
|
||||
"month": 2450000,
|
||||
"trend": "+12.5%",
|
||||
"trend_direction": "up"
|
||||
},
|
||||
"profit": {
|
||||
"today": 38500,
|
||||
"margin": "30.8%",
|
||||
"trend": "+8.2%"
|
||||
},
|
||||
"orders": {
|
||||
"today": 47,
|
||||
"conversion_rate": "3.2%",
|
||||
"avg_check": 2660
|
||||
},
|
||||
"balance": {
|
||||
"total": 1250000,
|
||||
"debtor": 340000,
|
||||
"creditor": 120000
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Docker Compose (стартовый)
|
||||
|
||||
```yaml
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: roi24
|
||||
POSTGRES_USER: roi24
|
||||
POSTGRES_PASSWORD: change_me
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./storage/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ./api
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
DATABASE_URL: postgresql://roi24:change_me@postgres:5432/roi24
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
scheduler:
|
||||
build:
|
||||
context: ./scripts
|
||||
environment:
|
||||
DATABASE_URL: postgresql://roi24:change_me@postgres:5432/roi24
|
||||
depends_on:
|
||||
- postgres
|
||||
command: ["python", "update_data.py"]
|
||||
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
|
||||
- ./dashboard:/usr/share/nginx/html
|
||||
depends_on:
|
||||
- api
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
## Безопасность
|
||||
|
||||
1. **Контент** credentials в `data_sources.config` — шифровать (AES-256)
|
||||
2. **API** — JWT tokens для каждого клиента
|
||||
3. **Данные** — изоляция между клиентами на уровне `client_id`
|
||||
4. **SSL** — Nginx + Let's Encrypt (certbot)
|
||||
5. **Бэкапы** — ежедневные бэкапы PostgreSQL (pg_dump)
|
||||
|
||||
## Хостинг
|
||||
|
||||
### Вариант для старта (один VPS)
|
||||
|
||||
| Ресурс | Минимум | Рекомендуемый |
|
||||
|--------|---------|--------------|
|
||||
| CPU | 2 vCPU | 4 vCPU |
|
||||
| RAM | 4 ГБ | 8 ГБ |
|
||||
| SSD | 50 ГБ | 100 ГБ NVMe |
|
||||
| Стоимость | ~1500₽/мес | ~3000₽/мес |
|
||||
|
||||
**Recomended providers:** Selectel, Timeweb, Yandex Cloud
|
||||
|
||||
### Этапы масштабирования
|
||||
|
||||
1. **Этап 1 (1-10 клиентов):** Один VPS, всё в одном Docker Compose
|
||||
2. **Этап 2 (10-50 клиентов):** Выделение PostgreSQL на отдельный сервер
|
||||
3. **Этап 3 (50+ клиентов):** Kubernetes + автоскейлинг + Redis кэш
|
||||
Reference in new issue
Block a user