Перейти к основному содержимому

Данные — 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_dailydayмесяц24 мес.rnp
ads_dailydayмесяц24 мес.rnp
finance_dailydayмесяц24 мес.rnp
rnp_dailydayмесяц24 мес.rnp
inventory_movementsoccurred_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}:wantedmodules/wb-api
Суточная квота токенов LLMop:llm:quota:{userId}:{YYYY-MM-DD}modules/llm
Лимитер входящих HTTP-запросовключи @nestjs/throttlercore/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, агрегат строк отчёта), аналитика рекламы автобиддера, аналитика РНП (сбрасывается после пересчёта витрины) и список складов бронировщика.

Связанные ADR​