Схема продуктов (E-Commerce)
Продукты
sql
CREATE TABLE product.products (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
category_id UUID REFERENCES product.categories(id),
sku VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL,
slug VARCHAR(200) NOT NULL UNIQUE,
short_description VARCHAR(500),
description TEXT,
base_price DECIMAL(20,2) NOT NULL,
sale_price DECIMAL(20,2),
cost_price DECIMAL(20,2),
currency VARCHAR(3) NOT NULL DEFAULT 'RUB',
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
-- DRAFT, ACTIVE, OUT_OF_STOCK, DISCONTINUED
stock_quantity INT DEFAULT 0,
low_stock_threshold INT DEFAULT 10,
weight_grams INT,
dimensions JSONB, -- {length, width, height}
career_points_value DECIMAL(20,2) DEFAULT 0, -- MLM-баллы за покупку
is_featured BOOLEAN DEFAULT FALSE,
sort_order INT DEFAULT 0,
published_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_products_category ON product.products(category_id);
CREATE INDEX idx_products_slug ON product.products(slug);
CREATE INDEX idx_products_status ON product.products(status);
CREATE INDEX idx_products_sku ON product.products(sku);Категории продуктов
sql
CREATE TABLE product.categories (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
parent_id UUID REFERENCES product.categories(id),
path LTREE, -- Для эффективных запросов по иерархии
image_url VARCHAR(500),
sort_order INT DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_categories_path ON product.categories USING GIST(path);
CREATE INDEX idx_categories_parent ON product.categories(parent_id);Изображения продуктов
sql
CREATE TABLE product.product_images (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
product_id UUID NOT NULL REFERENCES product.products(id) ON DELETE CASCADE,
url VARCHAR(500) NOT NULL,
alt_text VARCHAR(200),
sort_order INT DEFAULT 0,
is_primary BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_images_product ON product.product_images(product_id);Корзины
sql
CREATE TABLE product.carts (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES core.users(id),
session_id VARCHAR(100), -- Для гостевых корзин
referring_partner_id UUID REFERENCES mlm.partners(id),
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
-- ACTIVE, CONVERTED, ABANDONED
subtotal DECIMAL(20,2) DEFAULT 0,
discount_total DECIMAL(20,2) DEFAULT 0,
total DECIMAL(20,2) DEFAULT 0,
currency VARCHAR(3) DEFAULT 'RUB',
expires_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_carts_user ON product.carts(user_id);
CREATE INDEX idx_carts_session ON product.carts(session_id);
CREATE INDEX idx_carts_status ON product.carts(status);Товары в корзине
sql
CREATE TABLE product.cart_items (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
cart_id UUID NOT NULL REFERENCES product.carts(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES product.products(id),
quantity INT NOT NULL DEFAULT 1,
unit_price DECIMAL(20,2) NOT NULL,
total_price DECIMAL(20,2) NOT NULL,
added_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(cart_id, product_id)
);
CREATE INDEX idx_cart_items_cart ON product.cart_items(cart_id);Конечный автомат заказов
Заказы следуют строгому конечному автомату для обеспечения целостности данных и корректных бизнес-переходов.
Диаграмма состояний
┌─────────────┐
│ PENDING │ <- Начальное состояние (заказ создан)
└──────┬──────┘
│ payment_success
▼
┌─────────────┐
┌──────────│ CONFIRMED │──────────┐
│ └──────┬──────┘ │
│ │ start_processing│ cancel (до отправки)
│ ▼ │
│ ┌─────────────┐ │
│ │ PROCESSING │──────────┤
│ └──────┬──────┘ │
│ │ ship │
│ ▼ ▼
│ ┌─────────────┐ ┌─────────────┐
│ │ SHIPPED │ │ CANCELLED │
│ └──────┬──────┘ └─────────────┘
│ │ deliver
│ ▼
│ ┌─────────────┐
│ │ DELIVERED │
│ └──────┬──────┘
│ │ request_refund (в течение 14 дней)
│ ▼
│ ┌─────────────┐
└─────────→│ REFUNDED │ <- Также достижимо через чарджбэк
└─────────────┘Таблица допустимых переходов
| Из состояния | В состояние | Триггер | Условия |
|---|---|---|---|
| PENDING | CONFIRMED | payment_success | Платеж завершен |
| PENDING | CANCELLED | cancel | Пользователь отменяет до оплаты |
| CONFIRMED | PROCESSING | start_processing | Админ/система начинает выполнение |
| CONFIRMED | CANCELLED | cancel | Только до отправки |
| PROCESSING | SHIPPED | ship | Присвоен трекинг-номер |
| PROCESSING | CANCELLED | cancel | Только до отправки |
| SHIPPED | DELIVERED | deliver | Курьер подтверждает доставку |
| DELIVERED | REFUNDED | refund | В пределах окна возврата (14 дней) |
| ЛЮБОЙ (оплаченный) | REFUNDED | chargeback | Спор с платежным провайдером |
Валидация переходов (Уровень приложения)
typescript
const ORDER_TRANSITIONS: Record<OrderStatus, OrderStatus[]> = {
PENDING: ['CONFIRMED', 'CANCELLED'],
CONFIRMED: ['PROCESSING', 'CANCELLED'],
PROCESSING: ['SHIPPED', 'CANCELLED'],
SHIPPED: ['DELIVERED'],
DELIVERED: ['REFUNDED'],
CANCELLED: [], // Конечное состояние
REFUNDED: [], // Конечное состояние
};
function canTransition(from: OrderStatus, to: OrderStatus): boolean {
return ORDER_TRANSITIONS[from]?.includes(to) ?? false;
}Триггер базы данных для валидации переходов
sql
CREATE OR REPLACE FUNCTION product.validate_order_transition()
RETURNS TRIGGER AS $$
BEGIN
-- Пропускаем, если статус не изменился
IF OLD.status = NEW.status THEN
RETURN NEW;
END IF;
-- Валидируем переход
IF NOT (
(OLD.status = 'PENDING' AND NEW.status IN ('CONFIRMED', 'CANCELLED')) OR
(OLD.status = 'CONFIRMED' AND NEW.status IN ('PROCESSING', 'CANCELLED')) OR
(OLD.status = 'PROCESSING' AND NEW.status IN ('SHIPPED', 'CANCELLED')) OR
(OLD.status = 'SHIPPED' AND NEW.status = 'DELIVERED') OR
(OLD.status = 'DELIVERED' AND NEW.status = 'REFUNDED')
) THEN
RAISE EXCEPTION 'Invalid order status transition: % -> %', OLD.status, NEW.status;
END IF;
-- Устанавливаем временные метки
CASE NEW.status
WHEN 'CONFIRMED' THEN NEW.confirmed_at = NOW();
WHEN 'SHIPPED' THEN NEW.shipped_at = NOW();
WHEN 'DELIVERED' THEN NEW.delivered_at = NOW();
WHEN 'CANCELLED' THEN NEW.cancelled_at = NOW();
WHEN 'REFUNDED' THEN NEW.refunded_at = NOW();
ELSE NULL;
END CASE;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER order_status_transition
BEFORE UPDATE ON product.orders
FOR EACH ROW
EXECUTE FUNCTION product.validate_order_transition();Заказы
sql
CREATE TABLE product.orders (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
order_number VARCHAR(20) NOT NULL UNIQUE,
user_id UUID NOT NULL REFERENCES core.users(id),
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
-- PENDING, CONFIRMED, PROCESSING, SHIPPED, DELIVERED, CANCELLED, REFUNDED
subtotal DECIMAL(20,2) NOT NULL,
discount_total DECIMAL(20,2) DEFAULT 0,
shipping_cost DECIMAL(20,2) DEFAULT 0,
tax_amount DECIMAL(20,2) DEFAULT 0,
total DECIMAL(20,2) NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'RUB',
shipping_address_id UUID REFERENCES product.addresses(id),
billing_address_id UUID REFERENCES product.addresses(id),
shipping_method_id UUID REFERENCES product.shipping_methods(id),
promo_code_id UUID,
referring_partner_id UUID REFERENCES mlm.partners(id),
notes TEXT,
confirmed_at TIMESTAMP,
shipped_at TIMESTAMP,
delivered_at TIMESTAMP,
cancelled_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_orders_user ON product.orders(user_id, created_at DESC);
CREATE INDEX idx_orders_status ON product.orders(status);
CREATE INDEX idx_orders_number ON product.orders(order_number);Товары в заказе
sql
CREATE TABLE product.order_items (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
order_id UUID NOT NULL REFERENCES product.orders(id) ON DELETE CASCADE,
product_id UUID NOT NULL REFERENCES product.products(id),
sku VARCHAR(50) NOT NULL, -- Снимок
name VARCHAR(200) NOT NULL, -- Снимок
quantity INT NOT NULL,
unit_price DECIMAL(20,2) NOT NULL,
total_price DECIMAL(20,2) NOT NULL,
career_points_value DECIMAL(20,2) DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_order_items_order ON product.order_items(order_id);Адреса
sql
CREATE TABLE product.addresses (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES core.users(id) ON DELETE CASCADE,
type VARCHAR(20) NOT NULL, -- SHIPPING, BILLING
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
company VARCHAR(200),
address_line1 VARCHAR(255) NOT NULL,
address_line2 VARCHAR(255),
city VARCHAR(100) NOT NULL,
state VARCHAR(100),
postal_code VARCHAR(20) NOT NULL,
country VARCHAR(3) NOT NULL, -- ISO 3166-1 alpha-3
phone VARCHAR(20),
is_default BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_addresses_user ON product.addresses(user_id);Платежи
sql
CREATE TABLE product.payments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
order_id UUID NOT NULL REFERENCES product.orders(id),
amount DECIMAL(20,2) NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'RUB',
method VARCHAR(20) NOT NULL, -- CARD, EWALLET, BANK_TRANSFER
provider VARCHAR(20) NOT NULL, -- STRIPE, YOOKASSA и т.д.
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
-- PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED
provider_reference VARCHAR(255),
provider_response JSONB,
paid_at TIMESTAMP,
failed_at TIMESTAMP,
refunded_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_payments_order ON product.payments(order_id);
CREATE INDEX idx_payments_status ON product.payments(status);Отправления
sql
CREATE TABLE product.shipments (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
order_id UUID NOT NULL REFERENCES product.orders(id),
carrier VARCHAR(50) NOT NULL, -- CDEK, DHL и т.д.
tracking_number VARCHAR(100),
status VARCHAR(20) NOT NULL DEFAULT 'PREPARING',
-- PREPARING, SHIPPED, IN_TRANSIT, DELIVERED, RETURNED
shipped_at TIMESTAMP,
estimated_delivery DATE,
delivered_at TIMESTAMP,
tracking_events JSONB,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_shipments_order ON product.shipments(order_id);
CREATE INDEX idx_shipments_tracking ON product.shipments(tracking_number);Способы доставки
sql
CREATE TABLE product.shipping_methods (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
name VARCHAR(100) NOT NULL,
code VARCHAR(50) NOT NULL UNIQUE,
description TEXT,
carrier VARCHAR(50) NOT NULL,
base_price DECIMAL(20,2) NOT NULL,
estimated_days_min INT,
estimated_days_max INT,
is_active BOOLEAN DEFAULT TRUE,
sort_order INT DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);