Архитектура базы данных
Техническая документация схемы данных Rodnoy Files — многопользовательское семейное фотохранилище с ИИ-функциями
1 Обзор архитектуры
Rodnoy Files использует трёхуровневую архитектуру хранения данных: PostgreSQL 16 как основную реляционную БД для структурированных данных (пользователи, файлы, альбомы, права доступа), Redis для сессий, кэша и очередей задач, и S3-совместимое объектное хранилище (MinIO / Selectel) для самих медиафайлов. Такой подход обеспечивает надёжность транзакций для метаданных и масштабируемость для больших объёмов медиа.
2 Основные сущности
Ниже описаны ключевые таблицы БД. Каждая таблица имеет колонку user_id для изоляции данных (row-level security). Все ID используют UUID v7 для монотонной сортировки по времени.
users — Пользователи
| Поле | Тип | Описание |
|---|---|---|
id | UUID PK | Уникальный идентификатор |
email | VARCHAR(255) UNIQUE | Логин, также для восстановления доступа |
password_hash | VARCHAR(255) | argon2id хеш пароля |
name | VARCHAR(100) | Отображаемое имя |
avatar_file_id | UUID FK | Ссылка на фото-аватар |
plan_id | INT FK | Текущий тарифный план |
storage_used_bytes | BIGINT | Занятое место (байт), обновляется при загрузке/удалении |
encryption_key_enc | TEXT | Зашифрованный мастер-ключ E2E (AES-256-GCM) |
two_factor_secret | VARCHAR(255) | TOTP секрет для 2FA (опционально) |
created_at | TIMESTAMPTZ | Дата регистрации |
files — Фотографии и медиа
| Поле | Тип | Описание |
|---|---|---|
id | UUID PK | Уникальный ID файла |
user_id | UUID FK | Владелец файла |
original_name | VARCHAR(500) | Оригинальное имя файла (DSC_0847.HEIC) |
storage_path | TEXT | Путь в S3: users/{user_id}/{uuid}.{ext} |
thumbnail_path | TEXT | Путь к превью (800px, WebP) |
mime_type | VARCHAR(50) | image/jpeg, image/heic, video/mp4 |
width / height | INT | Разрешение оригинала |
size_bytes | BIGINT | Размер файла в байтах |
checksum_sha256 | VARCHAR(64) | Для определения дубликатов |
exif_data | JSONB | EXIF: GPS, дата съёмки, камера, ориентация |
taken_at | TIMESTAMPTZ | Дата съёмки (из EXIF или загрузки) |
is_favorite | BOOLEAN | Добавлено в избранное |
ai_embedding | vector(512) | pgvector эмбеддинг для AI-поиска (CLIP) |
uploaded_at | TIMESTAMPTZ | Дата загрузки на сервер |
albums / album_files — Альбомы
| Поле | Тип | Описание |
|---|---|---|
id / user_id / name | UUID / FK / VARCHAR | Идентификатор, владелец, название альбома |
cover_file_id | UUID FK | Обложка альбома |
is_ai_generated | BOOLEAN | Создан ИИ автоматически |
ai_category | VARCHAR(50) | Категория: nature, people, travel, food... |
album_files | JOIN таблица | album_id + file_id + position (ORDER BY) |
persons / faces — Распознавание людей
| Поле | Тип | Описание |
|---|---|---|
persons.id / user_id / name | UUID / FK / VARCHAR | ID человека, владелец, имя |
persons.avatar_file_id | UUID FK | Фото-аватар человека (среднее или выбранное) |
persons.photo_count | INT | Денормализованный счётчик (обновляется триггером) |
persons.is_hidden | BOOLEAN | Скрыт из раздела "Люди" |
faces.file_id / person_id | FK / FK | Связь лицо-фото-человек |
faces.bbox / confidence | JSONB / FLOAT | Координаты bounding box [x,y,w,h], уверенность ИИ 0-1 |
shares — Общие ссылки
| Поле | Тип | Описание |
|---|---|---|
id / user_id | UUID / FK | ID ссылки, кто поделился |
share_type | ENUM | file / album / folder |
target_id | UUID | ID файла или альбома |
token | VARCHAR(32) UNIQUE | Секретный токен для URL (crypto_random) |
password_hash | VARCHAR(255) | Пароль на ссылку (опционально) |
expires_at | TIMESTAMPTZ | Срок действия ссылки (NULL = бесконечно) |
max_views / view_count | INT / INT | Лимит и счётчик просмотров |
is_active | BOOLEAN | Активна ли ссылка |
family_groups / family_members — Семейные аккаунты
| Поле | Тип | Описание |
|---|---|---|
family_groups.id / owner_id / name | UUID / FK / VARCHAR | Группа, создатель, название |
family_members.group_id / user_id | FK / FK | Связь участник-группа |
family_members.role | ENUM | owner / admin / member / viewer |
family_members.can_upload / can_delete | BOOLEAN | Права на загрузку и удаление |
plans / subscriptions — Тарифы и подписки
| Поле | Тип | Описание |
|---|---|---|
plans.id / name / price_monthly | INT / VARCHAR / INT | Базовый(0), Премиум(299), Семейный(599) |
plans.storage_limit_bytes | BIGINT | Лимит хранилища (5 ГБ, 100 ГБ, 1 ТБ) |
plans.features | JSONB | Список доступных функций (E2E, AI, family) |
subscriptions.user_id / plan_id | FK / FK | Текущая подписка пользователя |
subscriptions.expires_at | TIMESTAMPTZ | Дата окончания (NULL = бессрочно) |
subscriptions.payment_provider_id | VARCHAR | ID в платёжной системе (ЮKassa) |
3 Многопользовательский режим
Изоляция данных — каждая таблица содержит user_id как обязательную колонку. Все SQL-запросы на уровне бэкенда включают фильтр WHERE user_id = $current_user. Дополнительно используется PostgreSQL Row-Level Security (RLS) как последний рубеж защиты на случай ошибки в прикладном коде.
Конкурентные загрузки — при массовой загрузке пользователь может отправлять до 10 файлов параллельно. Каждый файл проходит через очередь Redis (Celery). Сервер сначала записывает запись в БД со статусом uploading, затем файл попадает в S3, после чего статус обновляется на ready и запускается фоновая задача генерации превью и AI-обработки.
Rate limiting — Redis хранит счётчики запросов по ключу ratelimit:{user_id}:{endpoint} с TTL 60 секунд. Лимиты: 100 запросов/мин для API, 30 загрузок/мин, 5 AI-запросов/мин для бесплатных пользователей.
Квоты хранилища — при каждой загрузке проверяется users.storage_used_bytes + file_size <= plans.storage_limit_bytes. При превышении возвращается ошибка 429. Общий занятый объём обновляется через триггер БД при вставке/удалении файлов для защиты от race conditions.
4 Безопасность данных
E2E шифрование (AES-256-GCM) — при включённом шифровании файлы шифруются на клиенте ДО отправки на сервер. Схема: мастер-ключ пользователя хранится в зашифрованном виде в БД (users.encryption_key_enc). Мастер-ключ расшифровывается паролем пользователя (argon2id KDF). Для каждого файла генерируется уникальный Data Encryption Key (DEK), который шифруется мастер-ключом и хранится рядом с файлом в S3-метаданных.
Сессии — JWT access token (15 мин) + refresh token (30 дней) хранятся в Redis. При каждом запросе проверяется валидность токена через Redis lookup. При смене пароля все сессии пользователя инвалидируются массовым удалением ключей session:{user_id}:*.
Ссылки-шеры — токены генерируются через crypto.random(24) (192 бит энтропии). При наличии пароля — хеш хранится через argon2id. При каждом доступе проверяются: is_active, expires_at, max_views. Логи доступов (IP, User-Agent) записываются в отдельную таблицу share_access_logs.
5 Производительность
Индексы — на каждой таблице созданы: первичный ключ (UUID), B-tree индекс на user_id, композитные индексы на частые запросы (user_id + uploaded_at для галереи, user_id + name для поиска людей). Для AI-поиска используется расширение pgvector с IVFFlat индексом на колонку ai_embedding (ANN-поиск, top-50 похожих за <50ms).
Партиционирование — таблица files партиционируется по RANGE на uploaded_at (ежемесячные партиции). При достижении 100 млн записей — дальнейшее партиционирование по user_id (hash). Таблица faces партиционируется по user_id (hash, 32 партиции).
Redis-кэш — дашборд-статистика (кол-во фото, альбомов, людей, занятое место) кэшируется на 5 минут с инвалидацией при загрузке/удалении. Обложки альбомов кэшируются на 1 час. Список людей кэшируется до следующего обновления через AI.
Фоновые задачи (Celery) — после загрузки файла ставятся задачи: генерация превью (WebP 800px), извлечение EXIF, определение дубликатов (perceptual hash), распознавание лиц (InsightFace), генерация AI-эмбеддинга (CLIP). Задачи ставятся в очередь с приоритетом: превью > EXIF > дубликаты > лица > эмбеддинг.
6 Масштабирование
Начальная конфигурация — один сервер (VPS 8 vCPU, 32 ГБ RAM) с PostgreSQL, Redis, Celery worker, Nginx. Достаточно для 1 000 пользователей с 10 000 фото каждый. S3-хранилище (Selectel/MinIO) на отдельном сервере или облачном сервисе.
Рост до 10 000+ пользователей — добавление read replica PostgreSQL (hot standby) для тяжелых аналитических запросов (AI-поиск, статистика). Вертикальное масштабирование Celery workers. CDN (Cloudflare/Bunny) для раздачи превью и публичных шар.
Рост до 100 000+ пользователей — миграция на управляемый PostgreSQL (Supabase/Neon) с автоматическим масштабированием. Citus для горизонтального шардирования по user_id. Отдельные очереди Celery для AI-задач (GPU-воркеры). S3 с версионированием и lifecycle-правилами для автоматического удаления старых превью.
7 ER-диаграмма
Структура связей между основными таблицами. Каждая таблица изолирована по user_id (не показано для чистоты диаграммы).
8 Этапы внедрения
Таблицы: users, files, albums, album_files. Регистрация/авторизация через JWT. Загрузка файлов в S3. Генерация превью. CRUD альбомов. Базовая галерея с пагинацией. Индексы на user_id + uploaded_at. Подключение pgvector (пока без эмбеддингов).
Таблицы: persons, faces, cleanup_suggestions. Celery workers для фоновых задач. Распознавание лиц (InsightFace). Определение дубликатов (perceptual hash pHash + dHash). Определение размытых/плохих фото. Автоматическая категоризация альбомов. AI-поиск через CLIP эмбеддинги + pgvector ANN.
Таблицы: shares, share_access_logs, family_groups, family_members. Генерация публичных ссылок с токенами. Пароль на ссылки. Срок действия. Семейные группы с ролевой моделью. Семейное хранилище (shared vault — файлы видны всем участникам группы).
Таблицы: plans, subscriptions, payment_history. Интеграция ЮKassa. Управление подписками. E2E шифрование: генерация мастер-ключа на клиенте, шифрование файлов перед загрузкой, хранение DEK в S3-метаданных. 2FA через TOTP. Вебхуки для платежных уведомлений.