CREATE TABLE IF NOT EXISTS students ( id SERIAL PRIMARY KEY, name VARCHAR(150) NOT NULL UNIQUE, 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() ); ALTER TABLE students ADD COLUMN IF NOT EXISTS group_id INT REFERENCES groups(id); 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; UPDATE settings SET value = 'Ты — редактор текстов. Исправь ТОЛЬКО грамматические, орфографические и пунктуационные ошибки в тексте. Приведи к правильному регистру буквы. НЕ меняй слова, структуру предложений, стиль или смысл текста. Верни ТОЛЬКО исправленный текст без пояснений.' WHERE key = 'ai_prompt' AND value IN ( 'Ты — редактор текстов для педагогического журнала. Исправь грамматические, орфографические и пунктуационные ошибки. Сохрани смысл и стиль. Верни ТОЛЬКО исправленный текст, без пояснений.', 'Ты — редактор текстов. Исправь ошибки. Верни ТОЛЬКО исправленный текст.' ); 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, created_at TIMESTAMPTZ DEFAULT now() ); ALTER TABLE share_links ADD COLUMN IF NOT EXISTS message TEXT; ALTER TABLE share_links ADD COLUMN IF NOT EXISTS link_url VARCHAR(500); ALTER TABLE share_links ADD COLUMN IF NOT EXISTS show_student_message BOOLEAN; ALTER TABLE share_links ADD COLUMN IF NOT EXISTS show_entry_date BOOLEAN; ALTER TABLE share_links ADD COLUMN IF NOT EXISTS show_group_photos BOOLEAN; DO $$ BEGIN IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'share_links' AND column_name = 'anonymize_names') THEN ALTER TABLE share_links ADD COLUMN IF NOT EXISTS show_student_names BOOLEAN; UPDATE share_links SET show_student_names = (NOT anonymize_names) WHERE anonymize_names IS NOT NULL; ALTER TABLE share_links DROP COLUMN anonymize_names; END IF; END $$; DO $$ BEGIN INSERT INTO students (name) SELECT DISTINCT student_name FROM entries WHERE student_name IS NOT NULL AND student_name <> '' ON CONFLICT (name) DO NOTHING; END $$; CREATE TABLE IF NOT EXISTS project_files ( id SERIAL PRIMARY KEY, entry_id INT NOT NULL REFERENCES entries(id) ON DELETE CASCADE, token VARCHAR(64), path VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, created_at TIMESTAMPTZ DEFAULT now() ); UPDATE project_files SET token = md5(random()::text || clock_timestamp()::text) WHERE token IS NULL; ALTER TABLE project_files ALTER COLUMN token SET NOT NULL; CREATE UNIQUE INDEX IF NOT EXISTS project_files_token_key ON project_files(token); ALTER TABLE project_files ALTER COLUMN entry_id DROP NOT NULL; ALTER TABLE project_files ADD COLUMN IF NOT EXISTS 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); ALTER TABLE entries ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; 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; 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; DELETE FROM settings WHERE key = 'share_anonymize_names'; 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; DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM settings WHERE key = 'ai_autocheck_migrated') THEN UPDATE entries SET description_original = description WHERE description_original IS NULL; UPDATE entries SET ai_status = 'skipped' WHERE ai_status = 'pending'; INSERT INTO settings (key, value) VALUES ('ai_autocheck_migrated', '1'); END IF; END $$; 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() ); ALTER TABLE groups ADD COLUMN IF NOT EXISTS branch_id INT REFERENCES branches(id) ON DELETE SET NULL; ALTER TABLE groups ADD COLUMN IF NOT EXISTS day_of_week INT; ALTER TABLE groups ADD COLUMN IF NOT EXISTS time_start TIME; ALTER TABLE groups ADD COLUMN IF NOT EXISTS time_end TIME; ALTER TABLE groups ADD COLUMN IF NOT EXISTS cover_path VARCHAR(255); 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); ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS user_id INT REFERENCES users(id) ON DELETE SET NULL; 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, created_at TIMESTAMPTZ DEFAULT now() ); ALTER TABLE group_photos ADD COLUMN IF NOT EXISTS sort_order INT DEFAULT 0; CREATE INDEX IF NOT EXISTS idx_group_photos_group_id ON group_photos(group_id); UPDATE entries e SET photo_path = p.photo_path FROM ( SELECT DISTINCT ON (entry_id) entry_id, photo_path FROM entry_photos ORDER BY entry_id, sort_order, id ) p WHERE e.photo_path IS NULL AND p.entry_id = e.id; 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();