Перейти к содержанию

Схема базы данных CalmTrader

Версия: 1.0 Дата: 13.01.2026 Контракт: order-001-phase1-ru Задача: 2.4.3 Database models (A4A-32)


1. Обзор

База данных CalmTrader использует PostgreSQL 16 с расширением pgvector для хранения векторных embeddings. Все модели используют UUID в качестве первичных ключей и имеют поле created_at для аудита.

1.1 Технические характеристики

Характеристика Значение
СУБД PostgreSQL 16
Расширения pgvector
ORM SQLAlchemy 2.0
Миграции Alembic
Драйвер asyncpg

1.2 Таблицы

Таблица Описание Записей*
users Пользователи Telegram ~10-100
messages Сообщения (user + bot) ~1000+
events Логирование событий ~500+
images Изображения (art) ~50
user_image_views Просмотры изображений ~100
breathing_audio_parts Аудио дыхательных упражнений (legacy) ~200
audio_parts Унифицированные аудио части ~500

*Примерные значения для MVP


2. ER-диаграмма

2.1 Mermaid диаграмма

erDiagram
    users ||--o{ messages : "has"
    users ||--o{ events : "generates"
    users ||--o{ user_image_views : "views"
    images ||--o{ user_image_views : "viewed_by"

    users {
        uuid id PK
        bigint telegram_id UK
        varchar username
        varchar first_name
        varchar language_code
        boolean is_admin
        varchar response_mode
        varchar tts_provider
        varchar tts_voice
        float tts_speed
        varchar voice_gender
        boolean show_transcript
        timestamptz created_at
    }

    messages {
        uuid id PK
        uuid user_id FK
        text text
        bigint telegram_message_id
        varchar sender_type
        varchar voice_file_url
        timestamptz created_at
    }

    events {
        uuid id PK
        uuid user_id FK
        varchar event_type
        json event_data
        timestamptz created_at
    }

    images {
        uuid id PK
        varchar filename
        varchar minio_path
        varchar event
        text[] tags
        text prompt
        varchar provider
        varchar aspect_ratio
        timestamptz created_at
    }

    user_image_views {
        uuid id PK
        uuid user_id FK
        uuid image_id FK
    }

    breathing_audio_parts {
        uuid id PK
        varchar technique
        varchar part_type
        int variant
        varchar language
        varchar voice_gender
        varchar tts_provider
        varchar tts_voice
        varchar minio_path
        int duration_ms
        text prompt
        json prompt_params
        text transcript
        timestamptz created_at
    }

    audio_parts {
        uuid id PK
        varchar category
        varchar group_name
        varchar part_type
        int variant
        varchar language
        varchar voice_gender
        varchar tts_provider
        varchar tts_voice
        varchar minio_bucket
        varchar minio_path
        int duration_ms
        text prompt
        json prompt_params
        text transcript
        timestamptz created_at
    }

2.2 ASCII диаграмма

┌─────────────────────────────────────────────────────────────────────────────┐
│                              DATABASE SCHEMA                                 │
└─────────────────────────────────────────────────────────────────────────────┘

                    ┌─────────────────────┐
                    │       users         │
                    ├─────────────────────┤
                    │ id (PK, UUID)       │
                    │ telegram_id (UK)    │
                    │ username            │
                    │ first_name          │
                    │ language_code       │
                    │ is_admin            │
                    │ response_mode       │
                    │ tts_provider        │
                    │ tts_voice           │
                    │ tts_speed           │
                    │ voice_gender        │
                    │ show_transcript     │
                    │ created_at          │
                    └─────────┬───────────┘
           ┌──────────────────┼──────────────────┐
           │                  │                  │
           ▼                  ▼                  ▼
┌─────────────────────┐ ┌─────────────────┐ ┌─────────────────────┐
│     messages        │ │     events      │ │  user_image_views   │
├─────────────────────┤ ├─────────────────┤ ├─────────────────────┤
│ id (PK)             │ │ id (PK)         │ │ id (PK)             │
│ user_id (FK)        │ │ user_id (FK)    │ │ user_id (FK)        │
│ text                │ │ event_type      │ │ image_id (FK)       │
│ telegram_message_id │ │ event_data      │ └──────────┬──────────┘
│ sender_type         │ │ created_at      │            │
│ voice_file_url      │ └─────────────────┘            │
│ created_at          │                                 │
└─────────────────────┘                                 ▼
                                              ┌─────────────────────┐
                                              │      images         │
                                              ├─────────────────────┤
                                              │ id (PK)             │
                                              │ filename            │
                                              │ minio_path          │
                                              │ event               │
                                              │ tags[]              │
                                              │ prompt              │
                                              │ provider            │
                                              │ aspect_ratio        │
                                              │ created_at          │
                                              └─────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│                            AUDIO TABLES                                      │
└─────────────────────────────────────────────────────────────────────────────┘

┌───────────────────────────────┐     ┌───────────────────────────────┐
│   breathing_audio_parts       │     │        audio_parts            │
│        (legacy)               │     │       (unified)               │
├───────────────────────────────┤     ├───────────────────────────────┤
│ id (PK)                       │     │ id (PK)                       │
│ technique                     │     │ category                      │
│ part_type                     │     │ group_name                    │
│ variant                       │     │ part_type                     │
│ language                      │     │ variant                       │
│ voice_gender                  │     │ language                      │
│ tts_provider                  │     │ voice_gender                  │
│ tts_voice                     │     │ tts_provider                  │
│ minio_path                    │     │ tts_voice                     │
│ duration_ms                   │     │ minio_bucket                  │
│ prompt                        │     │ minio_path                    │
│ prompt_params                 │     │ duration_ms                   │
│ transcript                    │     │ prompt                        │
│ created_at                    │     │ prompt_params                 │
└───────────────────────────────┘     │ transcript                    │
                                      │ created_at                    │
                                      └───────────────────────────────┘

3. Описание таблиц

3.1 users

Хранит информацию о пользователях Telegram-бота.

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    telegram_id BIGINT NOT NULL UNIQUE,
    username VARCHAR(255),
    first_name VARCHAR(255) NOT NULL,
    language_code VARCHAR(10) DEFAULT 'ru',
    is_admin BOOLEAN DEFAULT FALSE NOT NULL,
    response_mode VARCHAR(10) DEFAULT 'text' NOT NULL,
    tts_provider VARCHAR(20) DEFAULT 'openai' NOT NULL,
    tts_voice VARCHAR(20) DEFAULT 'nova' NOT NULL,
    tts_speed FLOAT DEFAULT 1.0 NOT NULL,
    voice_gender VARCHAR(10) DEFAULT 'female' NOT NULL,
    show_transcript BOOLEAN DEFAULT FALSE NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);

CREATE INDEX ix_users_telegram_id ON users(telegram_id);

Поля:

Поле Тип Nullable Default Описание
id UUID No gen_random_uuid() Первичный ключ
telegram_id BIGINT No - Telegram user ID (уникальный)
username VARCHAR(255) Yes - Telegram username (@handle)
first_name VARCHAR(255) No - Имя пользователя
language_code VARCHAR(10) Yes 'ru' Язык (ru/en)
is_admin BOOLEAN No FALSE Флаг администратора
response_mode VARCHAR(10) No 'text' Режим ответа (text/voice)
tts_provider VARCHAR(20) No 'openai' TTS провайдер (openai/elevenlabs)
tts_voice VARCHAR(20) No 'nova' Голос TTS
tts_speed FLOAT No 1.0 Скорость TTS (0.8/1.0/1.2)
voice_gender VARCHAR(10) No 'female' Пол голоса агента
show_transcript BOOLEAN No FALSE Показывать транскрипт после голоса
created_at TIMESTAMPTZ No NOW() Дата создания

Индексы:

Индекс Поля Тип
users_pkey id PRIMARY KEY
ix_users_telegram_id telegram_id UNIQUE

3.2 messages

Хранит сообщения пользователей и бота.

CREATE TABLE messages (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    text TEXT NOT NULL,
    telegram_message_id BIGINT,
    sender_type VARCHAR(10) DEFAULT 'user',
    voice_file_url VARCHAR(512),
    created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);

CREATE INDEX ix_messages_user_id ON messages(user_id);
CREATE INDEX ix_messages_telegram_message_id ON messages(telegram_message_id);

Поля:

Поле Тип Nullable Default Описание
id UUID No gen_random_uuid() Первичный ключ
user_id UUID No - FK на users.id
text TEXT No - Текст сообщения
telegram_message_id BIGINT Yes - ID сообщения в Telegram (для удаления)
sender_type VARCHAR(10) Yes 'user' Отправитель (user/bot)
voice_file_url VARCHAR(512) Yes - URL голосового файла в MinIO
created_at TIMESTAMPTZ No NOW() Дата создания

Связи:

  • user_idusers.id (CASCADE DELETE)

3.3 events

Логирование событий для аналитики.

CREATE TABLE events (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES users(id) ON DELETE SET NULL,
    event_type VARCHAR(100) NOT NULL,
    event_data JSON,
    created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);

CREATE INDEX ix_events_user_id ON events(user_id);
CREATE INDEX ix_events_event_type ON events(event_type);

Поля:

Поле Тип Nullable Default Описание
id UUID No gen_random_uuid() Первичный ключ
user_id UUID Yes - FK на users.id (может быть NULL)
event_type VARCHAR(100) No - Тип события
event_data JSON Yes - Данные события
created_at TIMESTAMPTZ No NOW() Дата события

Типы событий:

event_type Описание
user_start Первый /start
checkin_start Начало check-in
checkin_complete Завершение check-in
checkout_start Начало checkout
checkout_complete Завершение checkout
breathe_start Начало дыхательного упражнения
breathe_complete Завершение дыхательного упражнения
meditate_start Начало медитации
meditate_complete Завершение медитации
settings_change Изменение настроек

3.4 images

Хранит метаданные изображений (art).

CREATE TABLE images (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    filename VARCHAR(255) NOT NULL,
    minio_path VARCHAR(512) NOT NULL,
    event VARCHAR(50) NOT NULL,
    tags TEXT[] DEFAULT '{}' NOT NULL,
    prompt TEXT,
    provider VARCHAR(50),
    aspect_ratio VARCHAR(20),
    created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);

CREATE INDEX ix_images_event ON images(event);

Поля:

Поле Тип Nullable Default Описание
id UUID No gen_random_uuid() Первичный ключ
filename VARCHAR(255) No - Имя файла
minio_path VARCHAR(512) No - Путь в MinIO
event VARCHAR(50) No - Событие (morning, evening, breathe)
tags TEXT[] No {} Теги для фильтрации
prompt TEXT Yes - Промпт для генерации (AI)
provider VARCHAR(50) Yes - Провайдер (midjourney, google)
aspect_ratio VARCHAR(20) Yes - Соотношение сторон
created_at TIMESTAMPTZ No NOW() Дата создания

3.5 user_image_views

Отслеживает просмотры изображений пользователями.

CREATE TABLE user_image_views (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    image_id UUID NOT NULL REFERENCES images(id) ON DELETE CASCADE,
    UNIQUE(user_id, image_id)
);

CREATE INDEX ix_user_image_views_user_id ON user_image_views(user_id);

Назначение: Гарантирует, что пользователь не увидит одно и то же изображение дважды (для rotation).

3.6 breathing_audio_parts (Legacy)

Хранит аудио части для дыхательных упражнений. Deprecated — используйте audio_parts.

CREATE TABLE breathing_audio_parts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    technique VARCHAR(50) NOT NULL,
    part_type VARCHAR(30) NOT NULL,
    variant INTEGER DEFAULT 1 NOT NULL,
    language VARCHAR(10) NOT NULL,
    voice_gender VARCHAR(10) NOT NULL,
    tts_provider VARCHAR(20) NOT NULL,
    tts_voice VARCHAR(50) NOT NULL,
    minio_path VARCHAR(512) NOT NULL,
    duration_ms INTEGER,
    prompt TEXT NOT NULL,
    prompt_params JSON,
    transcript TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
    UNIQUE(technique, part_type, variant, language, voice_gender, tts_provider)
);

3.7 audio_parts (Unified)

Унифицированная таблица для всех типов аудио.

CREATE TABLE audio_parts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    category VARCHAR(20) NOT NULL,
    group_name VARCHAR(50) NOT NULL,
    part_type VARCHAR(50) NOT NULL,
    variant INTEGER DEFAULT 1 NOT NULL,
    language VARCHAR(10) NOT NULL,
    voice_gender VARCHAR(10) NOT NULL,
    tts_provider VARCHAR(20) NOT NULL,
    tts_voice VARCHAR(50) NOT NULL,
    minio_bucket VARCHAR(100) NOT NULL,
    minio_path VARCHAR(512) NOT NULL,
    duration_ms INTEGER NOT NULL,
    prompt TEXT NOT NULL,
    prompt_params JSON,
    transcript TEXT NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
    UNIQUE(category, group_name, part_type, variant, language, voice_gender, tts_provider)
);

CREATE INDEX ix_audio_parts_category ON audio_parts(category);
CREATE INDEX ix_audio_parts_group_name ON audio_parts(group_name);
CREATE INDEX ix_audio_parts_part_type ON audio_parts(part_type);
CREATE INDEX ix_audio_parts_language ON audio_parts(language);

Поля category:

Категория Описание
breathing Дыхательные упражнения
meditation Голосовые медитации
ambient Фоновые звуки

Поля group_name:

Category group_name examples
breathing box_breathing, physiological_sigh, 4-7-8_breathing
meditation meditation
ambient forest, stream, rain, fireplace, ocean

Поля part_type:

Category part_type examples
breathing intro, inhale, hold, exhale, outro, technique_explanation
meditation intro_relaxation, breath_deep, body_tension, mind_thoughts, outro_return
ambient loop

4. Индексы

4.1 Сводная таблица индексов

Таблица Индекс Поля Тип
users users_pkey id PRIMARY KEY
users ix_users_telegram_id telegram_id UNIQUE
messages messages_pkey id PRIMARY KEY
messages ix_messages_user_id user_id INDEX
messages ix_messages_telegram_message_id telegram_message_id INDEX
events events_pkey id PRIMARY KEY
events ix_events_user_id user_id INDEX
events ix_events_event_type event_type INDEX
images images_pkey id PRIMARY KEY
images ix_images_event event INDEX
user_image_views user_image_views_pkey id PRIMARY KEY
user_image_views ix_user_image_views_user_id user_id INDEX
user_image_views uq_user_image_views_user_image (user_id, image_id) UNIQUE
audio_parts audio_parts_pkey id PRIMARY KEY
audio_parts ix_audio_parts_category category INDEX
audio_parts ix_audio_parts_group_name group_name INDEX
audio_parts ix_audio_parts_part_type part_type INDEX
audio_parts ix_audio_parts_language language INDEX
audio_parts uq_audio_part_params (category, group_name, part_type, variant, language, voice_gender, tts_provider) UNIQUE

5. Constraints

5.1 Foreign Keys

Таблица Constraint Поле Ссылка ON DELETE
messages messages_user_id_fkey user_id users.id CASCADE
events events_user_id_fkey user_id users.id SET NULL
user_image_views user_image_views_user_id_fkey user_id users.id CASCADE
user_image_views user_image_views_image_id_fkey image_id images.id CASCADE

5.2 Unique Constraints

Таблица Constraint Поля
users users_telegram_id_key telegram_id
user_image_views uq_user_image_views_user_image (user_id, image_id)
breathing_audio_parts uq_breathing_audio_part_params (technique, part_type, variant, language, voice_gender, tts_provider)
audio_parts uq_audio_part_params (category, group_name, part_type, variant, language, voice_gender, tts_provider)

6. История миграций

# Файл Описание
001 001_initial.py Начальная схема (users, messages, events)
002 002_add_is_admin.py Добавлено поле is_admin в users
003 003_add_telegram_message_id.py Добавлено telegram_message_id в messages
004 004_agent_tables.py Таблицы для агента (knowledge_documents, client_notes)
005 005_add_client_notes.py Дополнительные поля client_notes
006 006_add_updated_at_to_knowledge_documents.py updated_at для knowledge_documents
007 007_add_voice_file_url.py voice_file_url в messages
008 008_add_response_mode.py response_mode в users
009 009_add_tts_provider.py tts_provider в users
010 010_add_tts_voice.py tts_voice в users
011 011_add_tts_speed.py tts_speed в users
012 012_add_voice_gender.py voice_gender в users
013 013_add_images_tables.py Таблицы images, user_image_views
014 014_add_show_transcript.py show_transcript в users
015 015_add_image_gen_metadata.py prompt, provider, aspect_ratio в images
016 016_add_breathing_audio.py Таблица breathing_audio (устаревшая)
017 017_breathing_parts.py Таблица breathing_audio_parts
018 018_show_transcript_false.py Изменение default для show_transcript
019 019_add_audio_parts.py Унифицированная таблица audio_parts

7. SQLAlchemy модели

7.1 Расположение

apps/api/app/models/
├── __init__.py
├── base.py          # Base, UUIDMixin, TimestampMixin
├── user.py          # User
├── message.py       # Message
├── event.py         # Event
├── image.py         # Image, UserImageView
├── breathing_audio.py  # BreathingAudioPart (legacy)
└── audio_part.py    # AudioPart (unified)

7.2 Base Mixins

class UUIDMixin:
    """Mixin that adds UUID primary key."""
    id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        primary_key=True,
        default=uuid.uuid4,
    )

class TimestampMixin:
    """Mixin that adds created_at timestamp."""
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        server_default=func.now(),
        nullable=False,
    )

8. Планируемые изменения

8.1 Phase 2 (TODO)

  • Таблица subscriptions для подписок
  • Таблица payments для платежей
  • Таблица sessions для check-in/checkout сессий
  • Расширение events для детальной аналитики

8.2 Рекомендации

  1. Индексы: Добавить составной индекс (user_id, created_at) на messages для пагинации
  2. Партиционирование: Рассмотреть партиционирование events по месяцам при росте
  3. Архивация: Настроить архивацию старых messages (>6 месяцев)

Приложения

A. Команды управления БД

# Применить миграции
ssh trader-psy "docker compose -f /opt/trader-psy/infrastructure/docker-compose.yml exec api alembic upgrade head"

# Откатить миграцию
ssh trader-psy "docker compose -f /opt/trader-psy/infrastructure/docker-compose.yml exec api alembic downgrade -1"

# Создать новую миграцию
cd apps/api && uv run alembic revision -m "description"

# Подключиться к БД
ssh trader-psy "docker compose -f /opt/trader-psy/infrastructure/docker-compose.yml exec postgres psql -U trader -d trader_psy"

B. Полезные SQL-запросы

-- Количество пользователей
SELECT COUNT(*) FROM users;

-- Активные пользователи за последние 7 дней
SELECT COUNT(DISTINCT user_id)
FROM messages
WHERE created_at > NOW() - INTERVAL '7 days';

-- Топ событий
SELECT event_type, COUNT(*) as cnt
FROM events
GROUP BY event_type
ORDER BY cnt DESC;

-- Размер таблиц
SELECT
    relname as table,
    pg_size_pretty(pg_total_relation_size(relid)) as size
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC;

C. Связанные документы


Документ создан в рамках выполнения контракта order-001-phase1-ru