- new photo-ai service: FastAPI + Real-ESRGAN x2plus on CPU, internal only - async job queue in server (POST enhance-ai / GET status), 5min timeout, sequential processing - keep original photo backup (entries.photo_original_path, uploads/.originals), restore-original endpoint - UI: AI button and restore-original button in enhance modal - db: photo_original_path column (init.sql, migration.sql, runtime ensure)
192 lines
6.9 KiB
PL/PgSQL
192 lines
6.9 KiB
PL/PgSQL
CREATE TABLE IF NOT EXISTS branches (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(200) NOT NULL UNIQUE,
|
|
address TEXT,
|
|
phone VARCHAR(50),
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS banned_ips (
|
|
ip VARCHAR(64) PRIMARY KEY,
|
|
reason VARCHAR(100) NOT NULL,
|
|
banned_until TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS groups (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(100) NOT NULL UNIQUE,
|
|
branch_id INT REFERENCES branches(id) ON DELETE SET NULL,
|
|
created_at TIMESTAMPTZ DEFAULT now(),
|
|
day_of_week INT,
|
|
time_start TIME,
|
|
time_end TIME,
|
|
cover_path VARCHAR(255)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS students (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(150) NOT NULL UNIQUE,
|
|
group_id INT REFERENCES groups(id),
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS entries (
|
|
id SERIAL PRIMARY KEY,
|
|
student_name VARCHAR(150) NOT NULL,
|
|
group_id INT NOT NULL REFERENCES groups(id),
|
|
description TEXT NOT NULL,
|
|
description_original TEXT,
|
|
description_ai TEXT,
|
|
ai_status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
|
ai_checked_at TIMESTAMPTZ,
|
|
ai_error TEXT,
|
|
photo_path VARCHAR(255),
|
|
photo_original_path VARCHAR(255),
|
|
deleted_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE entries ADD COLUMN IF NOT EXISTS description_original TEXT;
|
|
ALTER TABLE entries ADD COLUMN IF NOT EXISTS description_ai TEXT;
|
|
ALTER TABLE entries ADD COLUMN IF NOT EXISTS ai_status VARCHAR(20) NOT NULL DEFAULT 'pending';
|
|
ALTER TABLE entries ADD COLUMN IF NOT EXISTS ai_checked_at TIMESTAMPTZ;
|
|
ALTER TABLE entries ADD COLUMN IF NOT EXISTS ai_error TEXT;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_entries_ai_pending ON entries(id) WHERE ai_status = 'pending' AND deleted_at IS NULL;
|
|
|
|
CREATE OR REPLACE FUNCTION notify_entries_changed() RETURNS trigger AS $$
|
|
BEGIN
|
|
IF (TG_OP = 'INSERT') THEN
|
|
PERFORM pg_notify('entries_changed', json_build_object('type', 'entry_created', 'id', NEW.id)::text);
|
|
ELSIF (TG_OP = 'UPDATE' AND OLD.ai_status IS DISTINCT FROM NEW.ai_status) THEN
|
|
PERFORM pg_notify('entries_changed', json_build_object('type', 'ai_status', 'id', NEW.id, 'status', NEW.ai_status, 'error', NEW.ai_error, 'description', NEW.description, 'description_ai', NEW.description_ai, 'description_original', NEW.description_original)::text);
|
|
END IF;
|
|
RETURN NULL;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS trg_entries_notify ON entries;
|
|
CREATE TRIGGER trg_entries_notify AFTER INSERT OR UPDATE OF ai_status ON entries
|
|
FOR EACH ROW EXECUTE FUNCTION notify_entries_changed();
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT
|
|
);
|
|
|
|
INSERT INTO settings (key, value) VALUES ('spam_interval_min', '30')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('ai_prompt', 'Ты — редактор текстов. Исправь ТОЛЬКО грамматические, орфографические и пунктуационные ошибки в тексте. Приведи к правильному регистру буквы. НЕ меняй слова, структуру предложений, стиль или смысл текста. Верни ТОЛЬКО исправленный текст без пояснений.')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('ai_autocheck_enabled', 'true')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('share_show_student_message', 'false')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('share_show_entry_date', 'false')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('share_show_student_names', 'true')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('share_show_group_photos', 'true')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
INSERT INTO settings (key, value) VALUES ('cookie_notice_text', 'Этот сайт использует cookie-файлы для корректной работы. Продолжая просмотр, вы соглашаетесь с их использованием.')
|
|
ON CONFLICT (key) DO NOTHING;
|
|
|
|
CREATE TABLE IF NOT EXISTS share_links (
|
|
id SERIAL PRIMARY KEY,
|
|
token VARCHAR(40) UNIQUE NOT NULL,
|
|
name VARCHAR(200) NOT NULL,
|
|
group_id INT REFERENCES groups(id),
|
|
student_name VARCHAR(150),
|
|
date_from DATE,
|
|
date_to DATE,
|
|
show_student_names BOOLEAN,
|
|
expires_at TIMESTAMPTZ,
|
|
access_password_hash VARCHAR(255),
|
|
message TEXT,
|
|
link_url VARCHAR(500),
|
|
show_student_message BOOLEAN,
|
|
show_entry_date BOOLEAN,
|
|
show_group_photos BOOLEAN,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_share_links_expires_at ON share_links(expires_at);
|
|
|
|
INSERT INTO groups (name) VALUES
|
|
('Beginner'),
|
|
('Intermediate'),
|
|
('Advanced')
|
|
ON CONFLICT (name) DO NOTHING;
|
|
|
|
CREATE TABLE IF NOT EXISTS group_photos (
|
|
id SERIAL PRIMARY KEY,
|
|
group_id INT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
|
photo_path VARCHAR(255) NOT NULL,
|
|
caption TEXT,
|
|
taken_at DATE,
|
|
sort_order INT DEFAULT 0,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_group_photos_group_id ON group_photos(group_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS project_files (
|
|
id SERIAL PRIMARY KEY,
|
|
entry_id INT REFERENCES entries(id) ON DELETE CASCADE,
|
|
token VARCHAR(64) UNIQUE NOT NULL,
|
|
path VARCHAR(255) NOT NULL,
|
|
name VARCHAR(255) NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT now(),
|
|
detached_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS entry_photos (
|
|
id SERIAL PRIMARY KEY,
|
|
entry_id INT NOT NULL REFERENCES entries(id) ON DELETE CASCADE,
|
|
photo_path VARCHAR(255) NOT NULL,
|
|
caption TEXT,
|
|
sort_order INT DEFAULT 0,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_entry_photos_entry_id ON entry_photos(entry_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id SERIAL PRIMARY KEY,
|
|
username VARCHAR(100) NOT NULL UNIQUE,
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
name VARCHAR(150),
|
|
role VARCHAR(20) NOT NULL DEFAULT 'tutor' CHECK (role IN ('admin','tutor')),
|
|
is_active BOOLEAN DEFAULT true,
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_branches (
|
|
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
branch_id INT NOT NULL REFERENCES branches(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (user_id, branch_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token VARCHAR(64) NOT NULL UNIQUE,
|
|
created_at TIMESTAMPTZ DEFAULT now(),
|
|
expires_at TIMESTAMPTZ NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token);
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS audit_log (
|
|
id SERIAL PRIMARY KEY,
|
|
user_id INT REFERENCES users(id) ON DELETE SET NULL,
|
|
action VARCHAR(100) NOT NULL,
|
|
target JSONB,
|
|
ip VARCHAR(45),
|
|
created_at TIMESTAMPTZ DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_audit_log_created_at ON audit_log(created_at DESC);
|