Данные — PostgreSQL и Redis
PostgreSQL
Основное хранилище — PostgreSQL 16 с расширениями pgcrypto (генерация UUID) и
vector (pgvector, для RAG). Локально и в CI используется образ pgvector/pgvector:pg16;
в production — управляемый PostgreSQL в Coolify (расширение vector должно быть доступно).
Доступ — TypeORM 0.3 через @nestjs/typeorm (ADR-0001).
Параметры подключения собирает buildTypeOrmOptions (core/database/typeorm.options.ts) —
один билдер и для приложения, и для CLI миграций.
Соглашения схемы
- Имена таблиц и столбцов —
snake_case(SnakeNamingStrategy); свойства сущностей —camelCase. - Первичный ключ —
id uuid DEFAULT gen_random_uuid(). created_at/updated_at—timestamptz, UTC (базовая сущностьBaseEntity); мягкое удаление —deleted_at(SoftDeletableEntity).- Полуструктурированные данные WB —
jsonb. - Идентификаторы WB (
nm_id,wb_shop_idи т. п.) —bigint(в TypeScript —string). - Все доменные данные кабинета привязаны к
wb_account_id; удаление кабинета каскадно удаляет его номенклатуру. - Тяжёлые выборки (отчёты, P&L, аналитика) агрегируются на стороне SQL, период отчёта
ограничен
REPORT_MAX_PERIOD_DAYS(по умолчанию 366) (ADR-0046). - Массовая запись — чанками по 500 строк (
core/database/chunked-write.ts) с идемпотентнымINSERT … ON CONFLICT DO UPDATE. - Параметры сессии пула:
statement_timeout=DB_STATEMENT_TIMEOUT_MS(по умолчанию 30 с),max_parallel_workers_per_gather=DB_MAX_PARALLEL_WORKERS_PER_GATHER(по умолчанию 0).
Миграции
- Схема меняется только миграциями (
synchronize: false). Миграции лежат вbackend/src/core/database/migrations/, учёт — таблицаtypeorm_migrations. - Каждая миграция обязана откатываться: в CI выполняется
migration:run, затем e2e-тест «применить → откатить → применить» (test/migrations.e2e-spec.ts). - Приложение при старте миграции не применяет (
migrationsRun: false). В контейнере их применяет entrypoint (backend/docker-entrypoint.sh→node dist/core/database/run-migrations.js) до запуска приложения. Раннер берёт сессионныйpg_advisory_lock, поэтому параллельный старт нескольких реплик безопасен. При сбое миграции контейнер падает и Coolify оставляет работать предыдущую версию. - Локально без Docker:
npm run migration:run(через ts-node); сиды —npm run seed:run(только dev/тесты).
Временные ряды и партиционирование
Решение — ADR-0008 (временные ряды): ванильный
PostgreSQL без TimescaleDB, месячные RANGE-партиции, дневные агрегаты — обычные таблицы
с идемпотентным upsert (не materialized views), время хранится в UTC, перевод в
Europe/Moscow — на уровне выдачи.
Фактическая реализация:
- Сырые данные WB (заказы, продажи, строки отчёта реализации, статистика рекламы,
воронка) хранятся в обычных таблицах модулей (
wb_orders,wb_sales,wb_report_lines,ad_campaign_stats,rnp_funnel_stats) с идемпотентным upsert. Отдельные партиционированные fact-таблицы из ADR (sales_factsи т. п.) не заводились. - Партиционированы дневные витрины РНП и журнал движения товара:
| Таблица | Ключ партиционирования | Гранулярность | Хранение | Владелец |
|---|---|---|---|---|
sales_daily | day | месяц | 24 мес. | rnp |
ads_daily | day | месяц | 24 мес. | rnp |
finance_daily | day | месяц | 24 мес. | rnp |
rnp_daily | day | месяц | 24 мес. | rnp |
inventory_movements | occurred_at | месяц | 24 мес. | inventory |
Родительскую таблицу с PARTITION BY RANGE создаёт миграция модуля-владельца; дочерние
партиции обслуживает менеджер партиций (core/partitioning, PartitionManagerService).
Реестр таблиц собирается в src/partition-registry.module.ts из спецификаций модулей
(RNP_PARTITIONED_TABLES, INVENTORY_PARTITIONED_TABLES).
Менеджер запускается по расписанию 0 3 * * 1 (понедельник 03:00 UTC, очередь
partition-manager) и для каждой таблицы:
- создаёт недостающие партиции на текущий месяц и 3 месяца вперёд (
CREATE TABLE IF NOT EXISTS … PARTITION OF), имена —<table>_YYYY_MM; - удаляет партиции, данные которых целиком старше срока хранения (
DROP TABLE).
Сбой по одной таблице изолирован. Метрики: optima_partitions_created_total,
optima_partitions_dropped_total, optima_partition_manager_errors_total.
Подробнее — backend/docs/partitioning.md.
Пересчёт дневных агрегатов (re-aggregate)
WB присылает данные задним числом, поэтому дневные витрины пересчитываются точечно:
ReAggregateService.enqueue(accountId, day, domain) ставит джобу в очередь re-aggregate
с детерминированным jobId (дебаунс и защита от наложений). Обработчики-роллаперы
регистрируются в реестре RE_AGGREGATE_HANDLERS; сейчас это домены РНП:
продажи (sales_daily), реклама (ads_daily), финансы (finance_daily) и итоговый merge
(rnp_daily). Неизвестный домен завершает джобу без ретраев.
Кто ставит пересчёт: плановый и пользовательский импорт фидов оцифровки, импорт отчёта
реализации, rnp-finance-sync, а ночной rnp-merge-sync пересчитывает окно 3 дня и
«самовосстанавливает» дни за 90 дней, где есть заказы, но нет витрины. Подробнее — в
Фоновых задачах и backend/docs/rnp-schedules.md.
Векторное хранилище (pgvector)
Для RAG используется таблица knowledge_chunks (ADR-0015):
embedding vector(1536)+ HNSW-индексvector_cosine_ops; поиск —ORDER BY embedding <=> $1 LIMIT k;- таблицей владеет миграция (TypeORM колонку
vectorне моделирует), доступ — сырым параметризованным SQL вKnowledgeChunkRepository; - колонки контроля доступа
material_id,visibility,status,author_user_id(ADR-0047) — фильтрация прав выполняется в SQL доLIMIT.
Подробнее — ИИ-подсистема.
Двоичные файлы
Отдельного объектного хранилища нет: у контейнеров в Coolify эфемерная файловая система,
поэтому загружаемые файлы хранятся в PostgreSQL в колонках bytea
(select: false, чтобы не тянуть их в обычные выборки):
ab_test_variant_images— фото вариантов A/B-теста (JPEG/PNG/WebP, до 10 МБ, ADR-0062);kb_files— исходные документы базы знаний.
Основные таблицы по доменам
| Домен | Таблицы |
|---|---|
| Пользователи и сессии (F2) | users, refresh_tokens |
| Кабинеты и доступ (F2) | wb_accounts, brand_access |
| Каталог (F4) | brands, shops, products |
| Аудит (F6) | audit_logs (иммутабельные записи, jsonb before/after/metadata) |
| LLM (F7) | prompt_templates, knowledge_chunks |
| 5.1 Оцифровка | wb_orders, wb_sales, wb_report_lines, wb_sync_cursors |
| 5.2 Цены | price_changes, price_bulk_jobs |
| 5.3 Репрайсер | repricer_rules, repricer_runs |
| 5.4 Автобиддер | ad_campaigns, ad_campaign_stats, ad_campaign_budgets, bid_strategies, ad_bid_changes |
| 5.5 Автоответы | autoreply_feedbacks, autoreply_drafts, autoreply_scenarios, autoreply_reply_templates, autoreply_complaints, autoreply_complaint_rules |
| 5.6 РНП | rnp_funnel_stats, rnp_product_lists, sales_daily, ads_daily, finance_daily, rnp_daily |
| 5.7 Бухгалтерия | accounting_transactions (ручные операции; остальное считается из оцифровки) |
| 5.8 Бронировщик | booking_tasks, booking_task_history, notifications |
| 5.9 ИИ-помощник | assistant_queries |
| 5.10 ИИ-агент | ai_agent_approvals (история действий — в audit_logs) |
| 5.11 Товарный учёт | inventory_stocks, inventory_stock_snapshots, inventory_movements |
| 5.12 Юр. блок | claim_templates, claims |
| 5.13 Контент | generated_contents |
| 5.14 A/B-тесты | ab_tests, ab_test_variants, ab_test_variant_periods, ab_test_variant_metrics, ab_test_variant_images |
| 5.15 База знаний | kb_sections, kb_materials, kb_access_grants, kb_files |
Полные определения — сущности backend/src/modules/<module>/entities/*.entity.ts
и документация кода.
Redis
Redis (7.x) используется для трёх задач. Подключение — REDIS_URL.
| Назначение | Ключи | Где |
|---|---|---|
| Кэш чтений | op:cache:*, теги op:cache:tag:* | core/cache (CacheService) |
| Лимитер исходящих запросов к WB (token-bucket) | wb:rl:v2:{accountId}:{scope}:{path} | modules/wb-api |
| Per-account gate WB-тяжёлых джоб | wb:gate:{accountId}, wb:gate:{accountId}:wanted | modules/wb-api |
| Суточная квота токенов LLM | op:llm:quota:{userId}:{YYYY-MM-DD} | modules/llm |
| Лимитер входящих HTTP-запросов | ключи @nestjs/throttler | core/security |
| Очереди BullMQ | префикс QUEUE_PREFIX (по умолчанию optima) | core/queue |
BullMQ работает на выделенных соединениях (блокирующие команды), а кэш, лимитеры и
квоты — на общем клиенте REDIS_CLIENT (ADR-0007).
Кэш
CacheService (core/cache) — слой cache-aside поверх Redis (подробно — backend/docs/cache.md):
get/set/getOrSet/invalidateTag; TTL обязателен (бессрочных ключей нет);- ключи
<area>:<entity>:<id>…,wb_account_id— сразу послеarea:entity(изоляция кабинетов); - теги — Redis Set'ы,
invalidateTagмассово сносит класс ключей (например, после синка); getOrSetдедуплицирует одновременные промахи внутри процесса; между репликами распределённой блокировки нет;nullот загрузчика не кэшируется.
Сейчас кэшируются чтения оцифровки (отчёты, сводка, P&L, агрегат строк отчёта), аналитика рекламы автобиддера, аналитика РНП (сбрасывается после пересчёта витрины) и список складов бронировщика.