-- nika-gallery: полная схема PostgreSQL 16 -- Создано: ops-agent 2026-08-05 CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- Пользователи CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, password TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'buyer' CHECK (role IN ('owner','admin','buyer','subscriber')), telegram_id BIGINT, email_verified_at TIMESTAMPTZ, refresh_token TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Категории работ CREATE TABLE categories ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, sort_order INT NOT NULL DEFAULT 0 ); -- Работы CREATE TABLE works ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, description TEXT, medium TEXT, -- техника: масло, акварель... dimensions TEXT, -- размер: 60x80 см year INT, price NUMERIC(12,2), status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','published','sold')), auction_mode TEXT NOT NULL DEFAULT 'none' CHECK (auction_mode IN ('none','primary','secondary')), telegram_message_id BIGINT, created_by UUID REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Категории работ (M2M) CREATE TABLE work_categories ( work_id UUID REFERENCES works(id) ON DELETE CASCADE, category_id INT REFERENCES categories(id) ON DELETE CASCADE, PRIMARY KEY (work_id, category_id) ); -- Медиа CREATE TABLE media ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), work_id UUID REFERENCES works(id) ON DELETE CASCADE, url TEXT NOT NULL, thumb_url TEXT, type TEXT NOT NULL DEFAULT 'image' CHECK (type IN ('image','video')), sort_order INT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Аукционы CREATE TABLE auctions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), work_id UUID REFERENCES works(id) ON DELETE CASCADE, type TEXT NOT NULL CHECK (type IN ('primary','secondary')), start_price NUMERIC(12,2) NOT NULL, current_price NUMERIC(12,2) NOT NULL, reserve_price NUMERIC(12,2), min_step NUMERIC(12,2) NOT NULL DEFAULT 500, start_at TIMESTAMPTZ NOT NULL, end_at TIMESTAMPTZ NOT NULL, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','active','ended','sold','cancelled')), winner_id UUID REFERENCES users(id), created_by UUID REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Ставки CREATE TABLE bids ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), auction_id UUID REFERENCES auctions(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id), amount NUMERIC(12,2) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Подписки CREATE TABLE subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL UNIQUE, name TEXT, confirm_token TEXT NOT NULL DEFAULT gen_random_uuid()::text, unsub_token TEXT NOT NULL DEFAULT gen_random_uuid()::text, confirmed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Категории подписки (опционально) CREATE TABLE subscription_categories ( subscription_id UUID REFERENCES subscriptions(id) ON DELETE CASCADE, category_id INT REFERENCES categories(id) ON DELETE CASCADE, PRIMARY KEY (subscription_id, category_id) ); -- История рассылок CREATE TABLE newsletter_sends ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), subject TEXT NOT NULL, body_html TEXT NOT NULL, recipient_count INT NOT NULL DEFAULT 0, sent_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Комментарии CREATE TABLE comments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), work_id UUID REFERENCES works(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id), body TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','approved','rejected')), created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Лайки CREATE TABLE likes ( work_id UUID REFERENCES works(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (work_id, user_id) ); -- Отзывы CREATE TABLE reviews ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), work_id UUID REFERENCES works(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id) ON DELETE CASCADE, rating SMALLINT NOT NULL CHECK (rating BETWEEN 1 AND 5), body TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE (work_id, user_id) ); -- Уведомления владельца CREATE TABLE notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), owner_id UUID REFERENCES users(id), type TEXT NOT NULL CHECK (type IN ('comment','like','review','bid','sale','new_bid_lost')), entity_id UUID, -- id комментария / лайка / ставки / аукциона preview_text TEXT, -- «Иван: "Красиво!" (60 символов)» admin_url TEXT, -- /admin/comments/uuid read_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Telegram-синхронизация CREATE TABLE telegram_sync ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), work_id UUID REFERENCES works(id) ON DELETE CASCADE, channel_id TEXT NOT NULL, message_id BIGINT NOT NULL, direction TEXT NOT NULL DEFAULT 'push' CHECK (direction IN ('push','pull')), synced_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- Индексы CREATE INDEX idx_works_status ON works(status); CREATE INDEX idx_works_slug ON works(slug); CREATE INDEX idx_auctions_status ON auctions(status); CREATE INDEX idx_auctions_end_at ON auctions(end_at); CREATE INDEX idx_bids_auction ON bids(auction_id, created_at DESC); CREATE INDEX idx_comments_work ON comments(work_id, status); CREATE INDEX idx_notifications ON notifications(owner_id, read_at, created_at DESC); -- View: работы с агрегатами CREATE VIEW works_extended AS SELECT w.*, COUNT(DISTINCT l.user_id) AS likes_count, COUNT(DISTINCT c.id) FILTER (WHERE c.status='approved') AS comments_count, AVG(r.rating) AS avg_rating, COUNT(DISTINCT r.id) AS reviews_count, m.url AS cover_url, m.thumb_url AS cover_thumb FROM works w LEFT JOIN likes l ON l.work_id = w.id LEFT JOIN comments c ON c.work_id = w.id LEFT JOIN reviews r ON r.work_id = w.id LEFT JOIN LATERAL ( SELECT url, thumb_url FROM media WHERE work_id = w.id ORDER BY sort_order LIMIT 1 ) m ON true GROUP BY w.id, m.url, m.thumb_url;