CREATE EXTENSION IF NOT EXISTS pgcrypto; DO $$ BEGIN CREATE TYPE content_type AS ENUM ('news', 'article', 'video', 'audio', 'graphic', 'event'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE content_status AS ENUM ('draft', 'moderation', 'review', 'published', 'returned', 'archived'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE content_visibility AS ENUM ('public', 'authenticated', 'role_restricted'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE subscription_type AS ENUM ('category', 'tag', 'speaker'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE comment_status AS ENUM ('visible', 'moderation', 'hidden'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; CREATE TABLE IF NOT EXISTS roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code TEXT NOT NULL UNIQUE, name TEXT NOT NULL, description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK (code IN ('administrator', 'editor', 'manager', 'user')) ); CREATE TABLE IF NOT EXISTS permissions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code TEXT NOT NULL UNIQUE, description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS role_permissions ( role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (role_id, permission_id) ); CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), login TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, display_name TEXT NOT NULL, email TEXT UNIQUE, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS user_roles ( user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, role_id UUID NOT NULL REFERENCES roles(id) ON DELETE RESTRICT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (user_id, role_id) ); CREATE TABLE IF NOT EXISTS speakers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), display_name TEXT NOT NULL, role_description TEXT, biography TEXT, topics_csv TEXT NOT NULL DEFAULT '', materials_count INTEGER NOT NULL DEFAULT 0, subscribers_count INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS categories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL UNIQUE, slug TEXT NOT NULL UNIQUE, description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS tags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL UNIQUE, slug TEXT NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS content_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL, lead TEXT, body TEXT, content_type content_type NOT NULL, status content_status NOT NULL DEFAULT 'draft', visibility content_visibility NOT NULL DEFAULT 'public', category_id UUID REFERENCES categories(id) ON DELETE SET NULL, author_user_id UUID REFERENCES users(id) ON DELETE SET NULL, author_label TEXT, speaker_id UUID REFERENCES speakers(id) ON DELETE SET NULL, duration TEXT, image_tone TEXT NOT NULL DEFAULT '', moderator_comment TEXT, review_comment TEXT, rating_average DOUBLE PRECISION NOT NULL DEFAULT 0, rating_count INTEGER NOT NULL DEFAULT 0, published_at TIMESTAMPTZ, archived_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS content_tags ( content_id UUID NOT NULL REFERENCES content_items(id) ON DELETE CASCADE, tag_id UUID NOT NULL REFERENCES tags(id) ON DELETE CASCADE, PRIMARY KEY (content_id, tag_id) ); CREATE TABLE IF NOT EXISTS media_files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), content_id UUID REFERENCES content_items(id) ON DELETE CASCADE, uploaded_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL, original_name TEXT NOT NULL, mime_type TEXT NOT NULL, size_bytes BIGINT NOT NULL CHECK (size_bytes >= 0), storage_key TEXT NOT NULL UNIQUE, public_url TEXT, checksum TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS stored_file_blobs ( id UUID PRIMARY KEY, name TEXT NOT NULL, mime_type TEXT NOT NULL, size_bytes BIGINT NOT NULL CHECK (size_bytes >= 0), data BYTEA NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, subscription_type subscription_type NOT NULL, category_id UUID REFERENCES categories(id) ON DELETE CASCADE, tag_id UUID REFERENCES tags(id) ON DELETE CASCADE, speaker_id UUID REFERENCES speakers(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CHECK ( (subscription_type = 'category' AND category_id IS NOT NULL AND tag_id IS NULL AND speaker_id IS NULL) OR (subscription_type = 'tag' AND tag_id IS NOT NULL AND category_id IS NULL AND speaker_id IS NULL) OR (subscription_type = 'speaker' AND speaker_id IS NOT NULL AND category_id IS NULL AND tag_id IS NULL) ) ); CREATE UNIQUE INDEX IF NOT EXISTS subscriptions_unique_category ON subscriptions(user_id, category_id) WHERE subscription_type = 'category'; CREATE UNIQUE INDEX IF NOT EXISTS subscriptions_unique_tag ON subscriptions(user_id, tag_id) WHERE subscription_type = 'tag'; CREATE UNIQUE INDEX IF NOT EXISTS subscriptions_unique_speaker ON subscriptions(user_id, speaker_id) WHERE subscription_type = 'speaker'; CREATE TABLE IF NOT EXISTS comments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), content_id UUID NOT NULL REFERENCES content_items(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, body TEXT NOT NULL, status comment_status NOT NULL DEFAULT 'visible', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, title TEXT NOT NULL, body TEXT, is_read BOOLEAN NOT NULL DEFAULT FALSE, content_id UUID REFERENCES content_items(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), read_at TIMESTAMPTZ ); CREATE TABLE IF NOT EXISTS action_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL, action TEXT NOT NULL, entity_type TEXT NOT NULL, entity_id UUID, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, request_id TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS content_views ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), content_id UUID NOT NULL REFERENCES content_items(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id) ON DELETE SET NULL, anonymous_key TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS users_login_idx ON users(login); CREATE INDEX IF NOT EXISTS content_items_type_status_idx ON content_items(content_type, status); CREATE INDEX IF NOT EXISTS content_items_category_idx ON content_items(category_id); CREATE INDEX IF NOT EXISTS content_items_published_at_idx ON content_items(published_at DESC); CREATE INDEX IF NOT EXISTS content_items_search_idx ON content_items USING GIN (to_tsvector('russian', coalesce(title, '') || ' ' || coalesce(lead, '') || ' ' || coalesce(body, ''))); CREATE INDEX IF NOT EXISTS comments_content_id_idx ON comments(content_id); CREATE INDEX IF NOT EXISTS notifications_user_read_idx ON notifications(user_id, is_read); CREATE INDEX IF NOT EXISTS action_logs_actor_created_idx ON action_logs(actor_user_id, created_at DESC); INSERT INTO roles (code, name, description) VALUES ('administrator', 'Администратор', 'Расширенное управление системой, пользователями, ролями и настройками'), ('editor', 'Редактор', 'Создание и редактирование материалов, участие в модерации'), ('manager', 'Менеджер', 'Публикация, управление контентом и подписками'), ('user', 'Пользователь', 'Просмотр материалов, комментарии и подписки') ON CONFLICT (code) DO UPDATE SET name = EXCLUDED.name, description = EXCLUDED.description;