|
@@ -0,0 +1,308 @@
|
|
|
|
|
+# Схема базы данных
|
|
|
|
|
+
|
|
|
|
|
+## Общие правила
|
|
|
|
|
+- Первичные ключи: UUID (генерация на уровне БД: `gen_random_uuid()`)
|
|
|
|
|
+- Мягкое удаление: поле `deleted_at` (timestamp, null = не удалён)
|
|
|
|
|
+- Аудит: поля `created_at`, `updated_at` (автоматически через триггеры или на уровне приложения)
|
|
|
|
|
+- Все внешние ключи имеют индексы
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## Таблицы
|
|
|
|
|
+
|
|
|
|
|
+### 1. users — пользователи
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| id | UUID PK | |
|
|
|
|
|
+| email | VARCHAR(255) UNIQUE NOT NULL | Email для входа |
|
|
|
|
|
+| password_hash | VARCHAR(255) NOT NULL | bcrypt хеш |
|
|
|
|
|
+| role | VARCHAR(20) NOT NULL DEFAULT 'customer' | superadmin, moderator, landlord, executor, customer |
|
|
|
|
|
+| status | VARCHAR(20) NOT NULL DEFAULT 'active' | active, banned, pending_verification |
|
|
|
|
|
+| name | VARCHAR(255) | Отображаемое имя |
|
|
|
|
|
+| avatar_url | TEXT | Аватар |
|
|
|
|
|
+| phone | VARCHAR(20) | Телефон |
|
|
|
|
|
+| bio | TEXT | Краткая биография (для исполнителей) |
|
|
|
|
|
+| country | VARCHAR(10) | Код страны (RFC) для провайдера карт/платежей |
|
|
|
|
|
+| created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+| updated_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+| deleted_at | TIMESTAMPTZ | Дата мягкого удаления |
|
|
|
|
|
+
|
|
|
|
|
+**Индексы:**
|
|
|
|
|
+- `idx_users_email` UNIQUE on email
|
|
|
|
|
+- `idx_users_role` on role
|
|
|
|
|
+- `idx_users_status` on status
|
|
|
|
|
+
|
|
|
|
|
+### 2. refresh_tokens — refresh токены
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| id | UUID PK | |
|
|
|
|
|
+| user_id | UUID FK → users(id) | |
|
|
|
|
|
+| token_hash | VARCHAR(255) NOT NULL | SHA-256 хеш токена |
|
|
|
|
|
+| expires_at | TIMESTAMPTZ NOT NULL | 30 дней |
|
|
|
|
|
+| created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+
|
|
|
|
|
+**Индексы:**
|
|
|
|
|
+- `idx_refresh_tokens_token_hash` UNIQUE on token_hash
|
|
|
|
|
+- `idx_refresh_tokens_user_id` on user_id
|
|
|
|
|
+
|
|
|
|
|
+### 3. tags — теги стилей съёмки (справочник)
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| id | VARCHAR(50) PK | Ключ: portrait, fashion, nature, product, wedding, street, interior |
|
|
|
|
|
+| name | VARCHAR(255) NOT NULL | Название: Портрет, Фэшн, Природа |
|
|
|
|
|
+| category | VARCHAR(50) | Группа: style, technique, setting |
|
|
|
|
|
+| sort_order | INT DEFAULT 0 | Порядок сортировки |
|
|
|
|
|
+| created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+
|
|
|
|
|
+### 4. features — характеристики мест (справочник)
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| id | VARCHAR(50) PK | Ключ: parking, flash, cyclorama, makeup_room, wardrobe |
|
|
|
|
|
+| name | VARCHAR(255) NOT NULL | Название: Парковка, Импульсный свет, Циклорама |
|
|
|
|
|
+| category | VARCHAR(50) | Группа: equipment, amenity, infrastructure |
|
|
|
|
|
+| icon | VARCHAR(50) | Иконка |
|
|
|
|
|
+| sort_order | INT DEFAULT 0 | |
|
|
|
|
|
+| created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+
|
|
|
|
|
+### 5. places — места и студии
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| id | UUID PK | |
|
|
|
|
|
+| type | VARCHAR(10) NOT NULL | place или studio |
|
|
|
|
|
+| owner_id | UUID FK → users(id) NOT NULL | |
|
|
|
|
|
+| title | VARCHAR(255) NOT NULL | Название |
|
|
|
|
|
+| description | TEXT | Описание |
|
|
|
|
|
+| address | VARCHAR(500) | Адрес |
|
|
|
|
|
+| coordinates | GEOGRAPHY(Point, 4326) NOT NULL | PostGIS точка (lat, lng) |
|
|
|
|
|
+| cover_image | TEXT | URL обложки |
|
|
|
|
|
+| access_info | TEXT | Как добраться, парковка |
|
|
|
|
|
+| status | VARCHAR(20) NOT NULL DEFAULT 'pending_moderation' | draft, pending_moderation, published, rejected, archived |
|
|
|
|
|
+| moderation_comment | TEXT | Комментарий модератора при отклонении |
|
|
|
|
|
+| rating | DECIMAL(2,1) DEFAULT 0 | Средний рейтинг (0.0-5.0) |
|
|
|
|
|
+| reviews_count | INT DEFAULT 0 | Количество отзывов |
|
|
|
|
|
+| hourly_rate | INT | Цена за час (только studio) |
|
|
|
|
|
+| currency | VARCHAR(3) DEFAULT 'RUB' | Валюта (только studio) |
|
|
|
|
|
+| min_hours | INT DEFAULT 1 | Минимальное количество часов (только studio) |
|
|
|
|
|
+| booking_url | TEXT | Ссылка на бронирование (только studio) |
|
|
|
|
|
+| created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+| updated_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+| deleted_at | TIMESTAMPTZ | |
|
|
|
|
|
+| published_at | TIMESTAMPTZ | Дата публикации |
|
|
|
|
|
+
|
|
|
|
|
+**Индексы:**
|
|
|
|
|
+- `idx_places_coordinates` GIST on coordinates
|
|
|
|
|
+- `idx_places_type` on type
|
|
|
|
|
+- `idx_places_status` on status
|
|
|
|
|
+- `idx_places_owner_id` on owner_id
|
|
|
|
|
+- `idx_places_rating` on rating
|
|
|
|
|
+- `idx_places_published_at` on published_at
|
|
|
|
|
+
|
|
|
|
|
+### 6. place_images — изображения мест
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| id | UUID PK | |
|
|
|
|
|
+| place_id | UUID FK → places(id) NOT NULL | |
|
|
|
|
|
+| url | TEXT NOT NULL | URL изображения |
|
|
|
|
|
+| alt | VARCHAR(255) | Альтернативный текст |
|
|
|
|
|
+| sort_order | INT DEFAULT 0 | Порядок |
|
|
|
|
|
+| is_cover | BOOLEAN DEFAULT false | |
|
|
|
|
|
+| created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+
|
|
|
|
|
+**Индексы:**
|
|
|
|
|
+- `idx_place_images_place_id` on place_id
|
|
|
|
|
+
|
|
|
|
|
+### 7. place_tags — теги мест (M2M)
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| place_id | UUID FK → places(id) | |
|
|
|
|
|
+| tag_id | VARCHAR(50) FK → tags(id) | |
|
|
|
|
|
+| PK | (place_id, tag_id) | |
|
|
|
|
|
+
|
|
|
|
|
+### 8. place_features — характеристики мест (M2M)
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| place_id | UUID FK → places(id) | |
|
|
|
|
|
+| feature_id | VARCHAR(50) FK → features(id) | |
|
|
|
|
|
+| PK | (place_id, feature_id) | |
|
|
|
|
|
+
|
|
|
|
|
+### 9. services — услуги исполнителей (ServiceCard)
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| id | UUID PK | |
|
|
|
|
|
+| executor_id | UUID FK → users(id) NOT NULL | |
|
|
|
|
|
+| title | VARCHAR(255) NOT NULL | Название услуги |
|
|
|
|
|
+| description | TEXT | Подробное описание |
|
|
|
|
|
+| price | INT NOT NULL | Цена |
|
|
|
|
|
+| currency | VARCHAR(3) DEFAULT 'RUB' | |
|
|
|
|
|
+| duration_minutes | INT | Длительность |
|
|
|
|
|
+| status | VARCHAR(20) NOT NULL DEFAULT 'published' | draft, published, archived |
|
|
|
|
|
+| rating | DECIMAL(2,1) DEFAULT 0 | |
|
|
|
|
|
+| reviews_count | INT DEFAULT 0 | |
|
|
|
|
|
+| created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+| updated_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+| deleted_at | TIMESTAMPTZ | |
|
|
|
|
|
+
|
|
|
|
|
+**Индексы:**
|
|
|
|
|
+- `idx_services_executor_id` on executor_id
|
|
|
|
|
+- `idx_services_status` on status
|
|
|
|
|
+- `idx_services_rating` on rating
|
|
|
|
|
+
|
|
|
|
|
+### 10. service_images — портфолио услуг
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| id | UUID PK | |
|
|
|
|
|
+| service_id | UUID FK → services(id) NOT NULL | |
|
|
|
|
|
+| url | TEXT NOT NULL | |
|
|
|
|
|
+| alt | VARCHAR(255) | |
|
|
|
|
|
+| sort_order | INT DEFAULT 0 | |
|
|
|
|
|
+| created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+
|
|
|
|
|
+### 11. service_tags — теги услуг (M2M)
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| service_id | UUID FK → services(id) | |
|
|
|
|
|
+| tag_id | VARCHAR(50) FK → tags(id) | |
|
|
|
|
|
+| PK | (service_id, tag_id) | |
|
|
|
|
|
+
|
|
|
|
|
+### 12. reviews — отзывы
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| id | UUID PK | |
|
|
|
|
|
+| user_id | UUID FK → users(id) NOT NULL | Кто оставил |
|
|
|
|
|
+| target_type | VARCHAR(10) NOT NULL | place или service |
|
|
|
|
|
+| target_id | UUID NOT NULL | ID места или услуги |
|
|
|
|
|
+| rating | SMALLINT NOT NULL CHECK (1-5) | |
|
|
|
|
|
+| text | TEXT | Текст отзыва |
|
|
|
|
|
+| created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+| updated_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+| deleted_at | TIMESTAMPTZ | |
|
|
|
|
|
+
|
|
|
|
|
+**Индексы:**
|
|
|
|
|
+- `idx_reviews_target` on (target_type, target_id)
|
|
|
|
|
+- `idx_reviews_user_id` on user_id
|
|
|
|
|
+- UNIQUE on (user_id, target_type, target_id) — один отзыв на цель
|
|
|
|
|
+
|
|
|
|
|
+### 13. bookings — бронирования студий
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| id | UUID PK | |
|
|
|
|
|
+| place_id | UUID FK → places(id) NOT NULL | |
|
|
|
|
|
+| user_id | UUID FK → users(id) NOT NULL | Кто забронировал |
|
|
|
|
|
+| start_time | TIMESTAMPTZ NOT NULL | |
|
|
|
|
|
+| end_time | TIMESTAMPTZ NOT NULL | |
|
|
|
|
|
+| status | VARCHAR(20) NOT NULL DEFAULT 'pending' | pending, confirmed, cancelled, completed |
|
|
|
|
|
+| total_price | INT | Итоговая цена |
|
|
|
|
|
+| currency | VARCHAR(3) DEFAULT 'RUB' | |
|
|
|
|
|
+| comment | TEXT | Комментарий к брони |
|
|
|
|
|
+| created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+| updated_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+
|
|
|
|
|
+**Индексы:**
|
|
|
|
|
+- `idx_bookings_place_id` on place_id
|
|
|
|
|
+- `idx_bookings_user_id` on user_id
|
|
|
|
|
+- `idx_bookings_time` on (start_time, end_time)
|
|
|
|
|
+
|
|
|
|
|
+### 14. moderation_log — лог модерации
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| id | UUID PK | |
|
|
|
|
|
+| moderator_id | UUID FK → users(id) NOT NULL | |
|
|
|
|
|
+| target_type | VARCHAR(20) NOT NULL | place, service |
|
|
|
|
|
+| target_id | UUID NOT NULL | |
|
|
|
|
|
+| action | VARCHAR(20) NOT NULL | approve, reject, request_changes, publish, archive |
|
|
|
|
|
+| comment | TEXT | Комментарий |
|
|
|
|
|
+| created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+
|
|
|
|
|
+**Индексы:**
|
|
|
|
|
+- `idx_moderation_log_target` on (target_type, target_id)
|
|
|
|
|
+- `idx_moderation_log_moderator_id` on moderator_id
|
|
|
|
|
+
|
|
|
|
|
+### 15. payments — платежи и подписки
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| id | UUID PK | |
|
|
|
|
|
+| user_id | UUID FK → users(id) NOT NULL | |
|
|
|
|
|
+| type | VARCHAR(20) NOT NULL | subscription, donation, listing_fee, booking_commission |
|
|
|
|
|
+| provider | VARCHAR(20) NOT NULL | cloudpayments, stripe |
|
|
|
|
|
+| provider_payment_id | VARCHAR(255) | ID платежа у провайдера |
|
|
|
|
|
+| amount | INT NOT NULL | Сумма в копейках |
|
|
|
|
|
+| currency | VARCHAR(3) DEFAULT 'RUB' | |
|
|
|
|
|
+| status | VARCHAR(20) NOT NULL | pending, succeeded, failed, refunded |
|
|
|
|
|
+| metadata | JSONB | Доп. данные |
|
|
|
|
|
+| created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+| updated_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+
|
|
|
|
|
+**Индексы:**
|
|
|
|
|
+- `idx_payments_user_id` on user_id
|
|
|
|
|
+- `idx_payments_status` on status
|
|
|
|
|
+- `idx_payments_provider_id` on (provider, provider_payment_id)
|
|
|
|
|
+
|
|
|
|
|
+### 16. subscriptions — подписки
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| id | UUID PK | |
|
|
|
|
|
+| user_id | UUID FK → users(id) NOT NULL | |
|
|
|
|
|
+| plan | VARCHAR(50) NOT NULL | studio_listing, executor_promo, premium_user |
|
|
|
|
|
+| status | VARCHAR(20) NOT NULL | active, cancelled, expired |
|
|
|
|
|
+| current_period_start | TIMESTAMPTZ | |
|
|
|
|
|
+| current_period_end | TIMESTAMPTZ | |
|
|
|
|
|
+| provider_subscription_id | VARCHAR(255) | |
|
|
|
|
|
+| created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+| updated_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+
|
|
|
|
|
+### 17. visitor_sessions — сессии посетителей на карте
|
|
|
|
|
+
|
|
|
|
|
+| Поле | Тип | Описание |
|
|
|
|
|
+|------|-----|----------|
|
|
|
|
|
+| id | UUID PK | |
|
|
|
|
|
+| user_id | UUID FK → users(id) | NULL для гостей |
|
|
|
|
|
+| coordinates | GEOGRAPHY(Point, 4326) NOT NULL | |
|
|
|
|
|
+| last_active_at | TIMESTAMPTZ NOT NULL | |
|
|
|
|
|
+| created_at | TIMESTAMPTZ NOT NULL DEFAULT now() | |
|
|
|
|
|
+
|
|
|
|
|
+**Индексы:**
|
|
|
|
|
+- `idx_visitor_sessions_coordinates` GIST on coordinates
|
|
|
|
|
+- `idx_visitor_sessions_last_active` on last_active_at
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## PostGIS
|
|
|
|
|
+
|
|
|
|
|
+Для работы с географическими данными необходимо расширение PostGIS:
|
|
|
|
|
+```sql
|
|
|
|
|
+CREATE EXTENSION IF NOT EXISTS postgis;
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## Миграции
|
|
|
|
|
+
|
|
|
|
|
+Миграции будут выполняться через `golang-migrate/migrate`.
|
|
|
|
|
+Файлы миграций: `backend/migrations/`:
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+backend/migrations/
|
|
|
|
|
+ 000001_create_extensions.up.sql
|
|
|
|
|
+ 000001_create_extensions.down.sql
|
|
|
|
|
+ 000002_create_users.up.sql
|
|
|
|
|
+ 000002_create_users.down.sql
|
|
|
|
|
+ 000003_create_tags_features.up.sql
|
|
|
|
|
+ 000003_create_tags_features.down.sql
|
|
|
|
|
+ 000004_create_places.up.sql
|
|
|
|
|
+ 000004_create_places.down.sql
|
|
|
|
|
+ ...
|
|
|
|
|
+```
|