# 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 кэш