Skip to content

Контракты и логика связей БД v3

Изменения относительно v2

#Что измененоБылоСтало
1Товар-категория1:N (products.category_id)M:N (product_categories)
2Состояние товараproducts.conditionКатегория типа condition
3Тип аккаунтаusers.account_type (personal/business)
4Бизнес-профильНовая таблица business_profiles
5Руль автоcar_generations.steering
6Руль деталиproducts.steering (left/right/universal)
7Годы в совместимостиproduct_compatibility.year_from/toУбраны (берутся из car_generations)
8steering_overrideproduct_compatibility.steering_overrideУбран → перенесён в products.steering
9Тип детали в OEMoem_numbers.part_typeУбран (дублировал категории)

Схема связей (21 таблица)

┌─────────────────────────────────────────────────────────────────────┐
│                         СПРАВОЧНИКИ                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  car_makes ──1:N──► car_models ──1:N──► car_generations             │
│                                              │                       │
│  regions ──1:N──► cities ──1:N──┬── districts                       │
│                                 └── metro_stations                   │
│                                                                      │
│  categories ──self-ref──► (parent_id)                               │
│                                                                      │
│  oem_numbers ◄──M:N──► oem_cross_references                         │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                         ЯДРО СИСТЕМЫ                                 │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│                        ┌── product_categories ──► categories        │
│                        │                                             │
│  users ──1:N──► products ──1:N──┬── product_images                  │
│    │                   │        ├── product_compatibility ──► cars  │
│    │                   │        └── product_oem ──► oem_numbers     │
│    │                   │                                             │
│    ├── 1:1 ──► business_profiles                                    │
│    ├── 1:N ──► verification_codes                                   │
│    ├── 1:N ──► user_sessions                                        │
│    ├── 1:N ──► favorites ──► products                               │
│    └── 1:N ──► payments ──► products (optional)                     │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                       КОММУНИКАЦИИ                                   │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  users (buyer) ◄──┐                                                 │
│                   ├── conversations ──1:N──► messages               │
│  users (seller) ◄─┘        │                                        │
│                            └──► products                            │
│                                                                      │
│  users (buyer) ◄──┐                                                 │
│                   ├── reviews ──► products (optional)               │
│  users (seller) ◄─┘                                                 │
│                                                                      │
│  users (reporter) ◄──┐                                              │
│  users (reported) ◄──┼── reports ──► products (optional)            │
│  users (resolver) ◄──┘                                              │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                        АНАЛИТИКА                                     │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  search_logs ──► users, car_makes, car_models, categories, cities   │
│  product_views ──► products, users                                  │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Контракты по доменам

1. Пользователи

users

account_type: 'personal' | 'business'
- personal: частное лицо (по умолчанию)
- business: разборка, магазин, ИП

Роль buyer/seller — НЕ хранится, определяется контекстом:
- seller = products.seller_id
- buyer = conversations.buyer_id, reviews.buyer_id

business_profiles

Связь: 1:1 с users (user_id UNIQUE)
Заполняется только при account_type = 'business'
is_verified = true после проверки модератором

Валидация:

  • Для создания товара: phone_verified_at IS NOT NULL
  • Для business: business_profiles должен существовать

2. Категории (новая логика)

category_type:

ТипОписаниеПример
partТип запчастиКузов, Двигатель, Подвеска, Трансмиссия
conditionСостояниеНовая, Б/У, Восстановленная
attributeАтрибутыОригинал, Неоригинал, Левый руль, Правый руль

Правила для товара:

- 1+ категорий типа part (минимум одна)
- 1 категория типа condition (ровно одна)
- 0+ категорий типа attribute (опционально)
- is_primary=true для одной part-категории (основная для отображения)

Пример: Ступичный подшипник BMW E46 б/у оригинал

sql
INSERT INTO product_categories VALUES
  (1, 101, 5, true),   -- Подвеска (part, primary)
  (2, 101, 8, false),  -- Трансмиссия (part)
  (3, 101, 21, false), -- Б/У (condition)
  (4, 101, 31, false); -- Оригинал (attribute)

3. Руль (левый/правый) — ИСПРАВЛЕНО в v3.1

Два уровня:

СущностьПолеЗначенияСмысл
car_generationssteeringleft / right / bothКакой руль у авто этого поколения
productssteeringleft / right / universalДля какого руля подходит деталь

Почему два поля:

Реальный кейс:

Фара правая передняя — OEM совпадает, но геометрия отражателей разная для LHD/RHD. КПП — одинаковый номер, но крепления разные — не взаимозаменяемы.

products.steering:

'universal' — подходит для любых (по умолчанию)
'left'      — только для леворульных
'right'     — только для праворульных

Примеры:

Детальproducts.steeringПочему
Фара правая ксенон (Европа)leftГеометрия для левого руля
Фара правая ксенон (Япония)rightГеометрия для правого руля
КПП (Европа)leftКрепления для LHD
КПП (Япония)rightКрепления для RHD
Ступичный подшипникuniversalОдинаковый для всех

Логика поиска:

sql
-- Пользователь ищет детали для леворульного авто
SELECT p.*
FROM products p
JOIN product_compatibility pc ON p.id = pc.product_id
WHERE pc.make_id = :make
  AND (pc.model_id IS NULL OR pc.model_id = :model)
  AND p.steering IN ('left', 'universal')
  AND p.status = 'active';

4. VIN и OEM поиск

VIN-декодирование (внешний сервис):

VIN → { make, model, year, steering, engine_type, ... }

VIN напрямую не хранится в БД — декодируется на лету через API (NHTSA, auto.ru API).

Поиск по OEM:

sql
-- 1. Нормализовать OEM
-- 2. Найти в oem_numbers
-- 3. Найти аналоги через oem_cross_references
-- 4. Найти товары через product_oem

WITH input_oem AS (
  SELECT id FROM oem_numbers WHERE oem = :normalized_oem
),
all_oems AS (
  SELECT id AS oem_id FROM input_oem
  UNION
  SELECT analog_oem_id FROM oem_cross_references 
  WHERE original_oem_id IN (SELECT id FROM input_oem)
  UNION
  SELECT original_oem_id FROM oem_cross_references 
  WHERE analog_oem_id IN (SELECT id FROM input_oem)
)
SELECT DISTINCT p.*
FROM products p
JOIN product_oem po ON p.id = po.product_id
WHERE po.oem_number_id IN (SELECT oem_id FROM all_oems);

5. Платежи (уточнение)

payments — ТОЛЬКО услуги платформы:

payment_typeОписание
premium_listingПоднятие объявления в выдаче
featuredЗакрепление в топе категории
subscriptionПодписка для business-аккаунтов

НЕ для оплаты товаров! Сделки между пользователями проходят напрямую (наличные, перевод на карту).


Циклические зависимости

Предупреждения dbdiagram о циклах — не ошибки. Это нормальная структура для маркетплейса:

users ─── products ─── conversations ─── users
   │                                      │
   └──────────── reviews ─────────────────┘

В PostgreSQL циклы разрешаются через:

  • DEFERRABLE INITIALLY DEFERRED constraints
  • Порядок INSERT (сначала users, потом products, потом conversations)

Денормализация и триггеры

ТаблицаПолеОбновляется приЛогика
usersratingINSERT/UPDATE/DELETE reviewsAVG(rating) WHERE seller_id
usersreviews_countINSERT/DELETE reviewsCOUNT(*) WHERE seller_id
usersproducts_countINSERT/DELETE productsCOUNT(*) WHERE seller_id
productsviews_countINSERT product_views+1 (rate-limit 1 IP/час)
productsfavorites_countINSERT/DELETE favoritesCOUNT(*)
categoriesproducts_countINSERT/DELETE product_categoriesCOUNT(*)
conversations*_unread_countINSERT/UPDATE messagesCOUNT(*) WHERE is_read=false
conversationslast_message_atINSERT messagesMAX(created_at)

Индексы для производительности

sql
-- Поиск товаров по категории
CREATE INDEX idx_product_categories_category ON product_categories(category_id);

-- Поиск по совместимости
CREATE INDEX idx_compatibility_make_model ON product_compatibility(make_id, model_id);

-- Полнотекстовый поиск
CREATE INDEX idx_products_title_gin ON products USING gin(to_tsvector('russian', title));

-- Геопоиск (PostGIS)
CREATE INDEX idx_cities_location ON cities USING GIST(location);

-- Поиск по OEM
CREATE INDEX idx_oem_numbers_oem ON oem_numbers(oem);
CREATE INDEX idx_product_oem_oem ON product_oem(oem_number_id);

Миграция данных condition → categories

sql
-- 1. Создать категории condition
INSERT INTO categories (name, slug, category_type) VALUES
  ('Новая', 'new', 'condition'),
  ('Б/У', 'used', 'condition'),
  ('Восстановленная', 'refurbished', 'condition');

-- 2. Мигрировать существующие товары
INSERT INTO product_categories (product_id, category_id)
SELECT p.id, c.id
FROM products p
JOIN categories c ON c.slug = p.condition AND c.category_type = 'condition';

-- 3. Удалить колонку (после проверки)
ALTER TABLE products DROP COLUMN condition;