237 lines
8.6 KiB
SQL
237 lines
8.6 KiB
SQL
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;
|