-- Файл: init_schema.sql
-- Версия: 4.1
-- Дата: Январь 2026
BEGIN;
-- Вспомогательная функция
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Core: users
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
owui_id VARCHAR(100) UNIQUE,
role VARCHAR(20) NOT NULL
CHECK (role IN ('staff', 'manager', 'senior', 'director', 'administrator', 'service')),
brand_id VARCHAR(50)
CHECK (brand_id IN ('ohana_market', 'ohana_kids', 'all') OR brand_id IS NULL),
is_active BOOLEAN DEFAULT TRUE,
email VARCHAR(255),
full_name VARCHAR(100),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
last_login TIMESTAMP WITH TIME ZONE
);
CREATE INDEX idx_users_owui_id ON users(owui_id);
CREATE INDEX idx_users_role ON users(role);
CREATE INDEX idx_users_brand_id ON users(brand_id);
CREATE INDEX idx_users_is_active ON users(is_active);
CREATE TRIGGER trigger_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at();
-- Core: sessions
CREATE TABLE sessions (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token VARCHAR(255) UNIQUE NOT NULL,
refresh_token VARCHAR(255),
device_info VARCHAR(255),
ip_address VARCHAR(50),
user_agent TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE,
last_activity TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
CREATE INDEX idx_sessions_token ON sessions(token);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
-- Core: documents
CREATE TABLE documents (
id SERIAL PRIMARY KEY,
original_name VARCHAR(255) NOT NULL,
stored_name VARCHAR(255),
file_size_bytes BIGINT,
mime_type VARCHAR(100),
kb_document_id VARCHAR(100),
archive_path VARCHAR(500),
processing_type VARCHAR(20) CHECK (processing_type IN ('direct', 'convert')),
converted_format VARCHAR(10),
category VARCHAR(50)
CHECK (category IN ('finance', 'contract', 'regulation', 'product',
'correspondence', 'analytics', 'hr', 'logistics',
'marketing', 'other')),
access_level VARCHAR(20)
CHECK (access_level IN ('staff', 'manager', 'senior', 'director')),
brand_id VARCHAR(50)
CHECK (brand_id IN ('ohana_market', 'ohana_kids', 'all')),
suggested_name VARCHAR(255),
summary TEXT,
uploaded_by_id INT REFERENCES users(id),
moderated_by_id INT REFERENCES users(id),
status VARCHAR(30) DEFAULT 'uploaded'
CHECK (status IN ('uploaded', 'routing', 'converting', 'classifying',
'pending_user', 'pending_moderation', 'uploading',
'indexed', 'archived', 'rejected', 'quarantine', 'error')),
rejection_reason TEXT,
pages_count INT,
processing_duration_sec FLOAT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
processing_started_at TIMESTAMP WITH TIME ZONE,
processing_completed_at TIMESTAMP WITH TIME ZONE,
moderated_at TIMESTAMP WITH TIME ZONE,
indexed_at TIMESTAMP WITH TIME ZONE,
archived_at TIMESTAMP WITH TIME ZONE
);
CREATE INDEX idx_documents_status ON documents(status);
CREATE INDEX idx_documents_uploaded_by ON documents(uploaded_by_id);
CREATE INDEX idx_documents_category ON documents(category);
CREATE INDEX idx_documents_brand_id ON documents(brand_id);
CREATE INDEX idx_documents_access_level ON documents(access_level);
CREATE INDEX idx_documents_kb_document_id ON documents(kb_document_id);
CREATE INDEX idx_documents_created_at ON documents(created_at);
CREATE INDEX idx_documents_access_filter ON documents(brand_id, access_level, status);
-- Core: quarantine
CREATE TABLE quarantine (
id SERIAL PRIMARY KEY,
original_name VARCHAR(255) NOT NULL,
quarantine_path VARCHAR(500) NOT NULL,
file_size_bytes BIGINT,
uploaded_by_id INT REFERENCES users(id),
error_code VARCHAR(50) NOT NULL,
error_message TEXT,
error_details JSONB,
retry_count INT DEFAULT 0,
max_retries INT DEFAULT 3,
last_retry_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + INTERVAL '30 days'
);
CREATE INDEX idx_quarantine_uploaded_by ON quarantine(uploaded_by_id);
CREATE INDEX idx_quarantine_error_code ON quarantine(error_code);
CREATE INDEX idx_quarantine_expires_at ON quarantine(expires_at);
-- Core: audit_log
CREATE TABLE audit_log (
id SERIAL PRIMARY KEY,
timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
module VARCHAR(50) NOT NULL,
action VARCHAR(100) NOT NULL,
user_id INT REFERENCES users(id),
brand_id VARCHAR(50),
details JSONB,
ip_address VARCHAR(50),
user_agent TEXT,
success BOOLEAN DEFAULT TRUE,
error_message TEXT
);
CREATE INDEX idx_audit_log_timestamp ON audit_log(timestamp);
CREATE INDEX idx_audit_log_user_id ON audit_log(user_id);
CREATE INDEX idx_audit_log_module ON audit_log(module);
CREATE INDEX idx_audit_log_user_time ON audit_log(user_id, timestamp DESC);
-- Core: notifications
CREATE TABLE notifications (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
event_type VARCHAR(50) NOT NULL,
level VARCHAR(20) DEFAULT 'info' CHECK (level IN ('info', 'warning', 'critical')),
title VARCHAR(255) NOT NULL,
message TEXT,
data JSONB,
source_module VARCHAR(50),
source_id VARCHAR(100),
is_read BOOLEAN DEFAULT FALSE,
read_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_notifications_user_id ON notifications(user_id);
CREATE INDEX idx_notifications_is_read ON notifications(is_read);
CREATE INDEX idx_notifications_created_at ON notifications(created_at);
CREATE INDEX idx_notifications_event_type ON notifications(event_type);
CREATE INDEX idx_notifications_unread ON notifications(user_id, is_read, created_at DESC) WHERE is_read = FALSE;
-- Core: settings
CREATE TABLE settings (
key VARCHAR(100) PRIMARY KEY,
value JSONB NOT NULL,
category VARCHAR(50),
description TEXT,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_by_id INT REFERENCES users(id)
);
CREATE INDEX idx_settings_category ON settings(category);
-- Core: celery_task_log
CREATE TABLE celery_task_log (
id SERIAL PRIMARY KEY,
task_id VARCHAR(255) UNIQUE NOT NULL,
task_name VARCHAR(255) NOT NULL,
args JSONB,
kwargs JSONB,
status VARCHAR(20) DEFAULT 'pending'
CHECK (status IN ('pending', 'started', 'success', 'failure', 'retry', 'revoked')),
result JSONB,
error_message TEXT,
traceback TEXT,
retries INT DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
started_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
runtime_seconds FLOAT
);
CREATE INDEX idx_celery_task_log_task_id ON celery_task_log(task_id);
CREATE INDEX idx_celery_task_log_task_name ON celery_task_log(task_name);
CREATE INDEX idx_celery_task_log_status ON celery_task_log(status);
CREATE INDEX idx_celery_task_log_created_at ON celery_task_log(created_at);
-- Service Account
INSERT INTO users (username, password_hash, role, brand_id, is_active, full_name)
VALUES ('service_account', '---NOT-FOR-LOGIN---', 'service', 'all', TRUE, 'Service Account');
COMMIT;