Схема базы данных 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_id→users.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 Рекомендации¶
- Индексы: Добавить составной индекс
(user_id, created_at)на messages для пагинации - Партиционирование: Рассмотреть партиционирование events по месяцам при росте
- Архивация: Настроить архивацию старых 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. Связанные документы¶
- Architecture — Архитектура системы (2.4.1)
- Контракт order-001 — Требования Phase 1
- CLAUDE.md — Инструкции для разработки
Документ создан в рамках выполнения контракта order-001-phase1-ru