This commit is contained in:
2026-10-03 20:39:03 +07:00
parent edb6d910f7
commit 1bbc32c3dc
134 changed files with 3782 additions and 0 deletions
@@ -0,0 +1,14 @@
-- +goose Up
CREATE TABLE
IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
roles JSONB NOT NULL DEFAULT '[]'::jsonb,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW ()
);
-- +goose Down
DROP TABLE IF EXISTS users;
@@ -0,0 +1,14 @@
-- +goose Up
CREATE TABLE
IF NOT EXISTS refresh_tokens (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
expires_at TIMESTAMP NOT NULL,
jti VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW (),
CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
);
-- +goose Down
DROP TABLE IF EXISTS refresh_tokens;
@@ -0,0 +1,20 @@
-- +goose Up
CREATE TABLE
IF NOT EXISTS scenarios (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
image TEXT,
scenario JSONB NOT NULL DEFAULT '{}'::jsonb,
author_id INTEGER NOT NULL,
status VARCHAR(50) DEFAULT 'draft',
updated_at TIMESTAMP NOT NULL DEFAULT NOW (),
created_at TIMESTAMP NOT NULL DEFAULT NOW (),
published_at TIMESTAMP,
is_deleted BOOLEAN DEFAULT FALSE,
CONSTRAINT fk_scenarios_author FOREIGN KEY (author_id) REFERENCES users (id) ON DELETE RESTRICT
);
-- +goose Down
DROP TABLE IF EXISTS scenarios;
@@ -0,0 +1,19 @@
-- +goose Up
CREATE TABLE
IF NOT EXISTS games (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
start_at TIMESTAMP NOT NULL,
scenario_id INTEGER NOT NULL,
status VARCHAR(50) DEFAULT 'draft',
updated_at TIMESTAMP NOT NULL DEFAULT NOW (),
created_at TIMESTAMP NOT NULL DEFAULT NOW (),
started_at TIMESTAMP,
ended_at TIMESTAMP,
CONSTRAINT fk_games_scenario FOREIGN KEY (scenario_id) REFERENCES scenarios (id) ON DELETE RESTRICT
);
-- +goose Down
DROP TABLE IF EXISTS games;
@@ -0,0 +1,17 @@
-- +goose Up
CREATE TABLE
IF NOT EXISTS teams (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
status VARCHAR(50) DEFAULT 'register',
creator_id INTEGER NOT NULL,
game_id INTEGER NOT NULL,
password TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW (),
CONSTRAINT fk_teams_game FOREIGN KEY (game_id) REFERENCES games (id) ON DELETE RESTRICT,
CONSTRAINT fk_teams_creator FOREIGN KEY (creator_id) REFERENCES users (id) ON DELETE RESTRICT
);
-- +goose Down
DROP TABLE IF EXISTS teams;
@@ -0,0 +1,13 @@
-- +goose Up
CREATE TABLE
IF NOT EXISTS actions (
id SERIAL PRIMARY KEY,
code TEXT,
team_id INTEGER,
created_at TIMESTAMP NOT NULL DEFAULT NOW (),
CONSTRAINT fk_actions_team FOREIGN KEY (team_id) REFERENCES teams (id) ON DELETE CASCADE
);
-- +goose Down
DROP TABLE IF EXISTS actions;
@@ -0,0 +1,15 @@
-- +goose Up
CREATE TABLE
IF NOT EXISTS applications (
id SERIAL PRIMARY KEY,
name TEXT,
image TEXT,
team_id INTEGER,
status VARCHAR(50) DEFAULT 'need',
created_at TIMESTAMP NOT NULL DEFAULT NOW (),
CONSTRAINT fk_applications_team FOREIGN KEY (team_id) REFERENCES teams (id) ON DELETE CASCADE
);
-- +goose Down
DROP TABLE IF EXISTS applications;
@@ -0,0 +1,26 @@
-- +goose Up
CREATE TABLE
IF NOT EXISTS user_agreements (
id SERIAL PRIMARY KEY,
-- user_id NULL-able: при удалении аккаунта FK-ограничение обнуляет
-- ссылку (ON DELETE SET NULL), а сама запись об акцепте остаётся в
-- обезличенном виде (факт согласия, ст. 9 ч. 4 152-ФЗ — бремя
-- доказывания на операторе). Персональные данные внутри записи
-- (ip, user_agent) обнуляет users_repo.DeleteUser.
user_id INTEGER,
agreement_type VARCHAR(50) NOT NULL,
version VARCHAR(50) NOT NULL,
accepted_at TIMESTAMP NOT NULL DEFAULT NOW (),
ip VARCHAR(45),
user_agent TEXT,
CONSTRAINT fk_user_agreements_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL,
CONSTRAINT uq_user_agreement UNIQUE (user_id, agreement_type, version),
CONSTRAINT chk_user_agreement_type CHECK (agreement_type IN ('terms', 'privacy'))
);
-- Отдельный индекс по user_id не нужен: UNIQUE (user_id, agreement_type, version)
-- уже покрывает выборки по префиксу user_id.
-- +goose Down
DROP TABLE IF EXISTS user_agreements;
@@ -0,0 +1,39 @@
-- +goose Up
-- Сценарии и команды сохраняются при удалении аккаунта автора:
-- привязка к пользователю обнуляется (обезличивание, ст. 3 152-ФЗ),
-- сам контент остаётся в Сервисе (лицензия по п. 7.2 Пользовательского соглашения).
ALTER TABLE scenarios ALTER COLUMN author_id DROP NOT NULL;
ALTER TABLE scenarios DROP CONSTRAINT IF EXISTS fk_scenarios_author;
ALTER TABLE scenarios ADD CONSTRAINT fk_scenarios_author
FOREIGN KEY (author_id) REFERENCES users (id) ON DELETE SET NULL;
ALTER TABLE teams ALTER COLUMN creator_id DROP NOT NULL;
ALTER TABLE teams DROP CONSTRAINT IF EXISTS fk_teams_creator;
ALTER TABLE teams ADD CONSTRAINT fk_teams_creator
FOREIGN KEY (creator_id) REFERENCES users (id) ON DELETE SET NULL;
-- status у сценариев всегда задан (default 'draft'): NULL-значение сломало бы
-- guard'ы вида "status <> 'public'" в scenarios_repo (NULL даёт NULL, т.е. ложь).
-- На БД с историческими NULL-данными сначала приводим их к default'у.
UPDATE scenarios SET status = 'draft' WHERE status IS NULL;
ALTER TABLE scenarios ALTER COLUMN status SET NOT NULL;
-- +goose Down
-- Восстановление прежней схемы: автор обязателен, при удалении пользователя —
-- удаление его контента запрещено (RESTRICT).
DELETE FROM scenarios WHERE author_id IS NULL;
DELETE FROM teams WHERE creator_id IS NULL;
ALTER TABLE scenarios ALTER COLUMN status DROP NOT NULL;
ALTER TABLE scenarios DROP CONSTRAINT IF EXISTS fk_scenarios_author;
ALTER TABLE scenarios ADD CONSTRAINT fk_scenarios_author
FOREIGN KEY (author_id) REFERENCES users (id) ON DELETE RESTRICT;
ALTER TABLE scenarios ALTER COLUMN author_id SET NOT NULL;
ALTER TABLE teams DROP CONSTRAINT IF EXISTS fk_teams_creator;
ALTER TABLE teams ADD CONSTRAINT fk_teams_creator
FOREIGN KEY (creator_id) REFERENCES users (id) ON DELETE RESTRICT;
ALTER TABLE teams ALTER COLUMN creator_id SET NOT NULL;
@@ -0,0 +1,7 @@
-- +goose Up
ALTER TABLE applications
ADD COLUMN IF NOT EXISTS file_type TEXT NOT NULL DEFAULT '';
-- +goose Down
ALTER TABLE applications
DROP COLUMN IF EXISTS file_type;
@@ -0,0 +1,17 @@
-- +goose Up
CREATE TABLE
IF NOT EXISTS team_answers (
id SERIAL PRIMARY KEY,
team_id INTEGER NOT NULL,
question_code TEXT NOT NULL,
answer TEXT NOT NULL DEFAULT '',
score INTEGER,
created_at TIMESTAMP NOT NULL DEFAULT NOW (),
updated_at TIMESTAMP NOT NULL DEFAULT NOW (),
CONSTRAINT fk_team_answers_team FOREIGN KEY (team_id) REFERENCES teams (id) ON DELETE CASCADE,
CONSTRAINT uq_team_answer UNIQUE (team_id, question_code)
);
-- +goose Down
DROP TABLE IF EXISTS team_answers;
@@ -0,0 +1,5 @@
-- +goose Up
ALTER TABLE actions ADD COLUMN via_mcp BOOLEAN NOT NULL DEFAULT FALSE;
-- +goose Down
ALTER TABLE actions DROP COLUMN via_mcp;
@@ -0,0 +1,11 @@
-- +goose Up
CREATE TABLE IF NOT EXISTS screen_sessions (
id BIGSERIAL PRIMARY KEY,
access_token_hash TEXT NOT NULL UNIQUE,
game_id INTEGER REFERENCES games (id) ON DELETE SET NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- +goose Down
DROP TABLE IF EXISTS screen_sessions;
@@ -0,0 +1,9 @@
-- +goose Up
ALTER TABLE screen_sessions
ADD COLUMN IF NOT EXISTS sound_enabled BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN IF NOT EXISTS last_seen_at TIMESTAMP NOT NULL DEFAULT NOW();
-- +goose Down
ALTER TABLE screen_sessions
DROP COLUMN IF EXISTS last_seen_at,
DROP COLUMN IF EXISTS sound_enabled;
@@ -0,0 +1,13 @@
-- +goose Up
CREATE TABLE IF NOT EXISTS game_map_items (
game_id INTEGER NOT NULL REFERENCES games(id) ON DELETE CASCADE,
kind TEXT NOT NULL,
item_id INTEGER NOT NULL,
x INTEGER NOT NULL DEFAULT 40,
y INTEGER NOT NULL DEFAULT 40,
hidden BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (game_id, kind, item_id)
);
-- +goose Down
DROP TABLE IF EXISTS game_map_items;
@@ -0,0 +1,11 @@
-- +goose Up
ALTER TABLE screen_sessions
ADD COLUMN IF NOT EXISTS mode INTEGER NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS state_version BIGINT NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS position_ms BIGINT NOT NULL DEFAULT 0;
-- +goose Down
ALTER TABLE screen_sessions
DROP COLUMN IF EXISTS position_ms,
DROP COLUMN IF EXISTS state_version,
DROP COLUMN IF EXISTS mode;
@@ -0,0 +1,9 @@
-- +goose Up
ALTER TABLE screen_sessions
ADD COLUMN IF NOT EXISTS command INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS volume INTEGER NOT NULL DEFAULT 25;
-- +goose Down
ALTER TABLE screen_sessions
DROP COLUMN IF EXISTS volume,
DROP COLUMN IF EXISTS command;
@@ -0,0 +1,7 @@
-- +goose Up
ALTER TABLE screen_sessions
ALTER COLUMN volume SET DEFAULT 100;
-- +goose Down
ALTER TABLE screen_sessions
ALTER COLUMN volume SET DEFAULT 25;
@@ -0,0 +1,6 @@
-- +goose Up
ALTER TABLE games ADD COLUMN format TEXT NOT NULL DEFAULT 'online'
CHECK (format IN ('online', 'offline'));
-- +goose Down
ALTER TABLE games DROP COLUMN format;
@@ -0,0 +1,13 @@
-- +goose Up
ALTER TABLE teams
ADD COLUMN captain_email TEXT NOT NULL DEFAULT '',
ADD COLUMN participants_count INTEGER NOT NULL DEFAULT 0 CHECK (participants_count >= 0),
ADD COLUMN registration_delivery_id TEXT,
ADD CONSTRAINT teams_registration_delivery_unique UNIQUE (game_id, registration_delivery_id);
-- +goose Down
ALTER TABLE teams
DROP CONSTRAINT teams_registration_delivery_unique,
DROP COLUMN registration_delivery_id,
DROP COLUMN participants_count,
DROP COLUMN captain_email;
@@ -0,0 +1,23 @@
-- +goose Up
ALTER TABLE teams
ADD COLUMN participation_status TEXT NOT NULL DEFAULT 'pending' CHECK (participation_status IN ('pending', 'confirmed', 'cancelled')),
ADD COLUMN participation_source TEXT NOT NULL DEFAULT '',
ADD COLUMN participation_answered_at TIMESTAMPTZ,
ADD COLUMN participation_token_hash BYTEA,
ADD COLUMN participation_sent_at TIMESTAMPTZ,
ADD COLUMN participation_attempts INTEGER NOT NULL DEFAULT 0,
ADD COLUMN participation_next_attempt_at TIMESTAMPTZ,
ADD COLUMN participation_email_error TEXT NOT NULL DEFAULT '';
CREATE UNIQUE INDEX teams_participation_token ON teams (participation_token_hash);
-- +goose Down
DROP INDEX teams_participation_token;
ALTER TABLE teams
DROP COLUMN participation_status,
DROP COLUMN participation_source,
DROP COLUMN participation_answered_at,
DROP COLUMN participation_token_hash,
DROP COLUMN participation_sent_at,
DROP COLUMN participation_attempts,
DROP COLUMN participation_next_attempt_at,
DROP COLUMN participation_email_error;
@@ -0,0 +1,17 @@
-- +goose Up
CREATE TABLE game_reviews (
id SERIAL PRIMARY KEY,
game_id INTEGER NOT NULL REFERENCES games (id) ON DELETE CASCADE,
team_id INTEGER NOT NULL REFERENCES teams (id) ON DELETE CASCADE,
rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5),
text TEXT NOT NULL CHECK (length(btrim(text)) > 0),
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected', 'hidden')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
moderated_at TIMESTAMPTZ,
UNIQUE (game_id, team_id)
);
CREATE INDEX game_reviews_moderation ON game_reviews (status, created_at);
-- +goose Down
DROP TABLE game_reviews;
@@ -0,0 +1,13 @@
-- +goose Up
ALTER TABLE teams
ADD COLUMN review_reminder_sent_at TIMESTAMPTZ,
ADD COLUMN review_reminder_attempts INTEGER NOT NULL DEFAULT 0,
ADD COLUMN review_reminder_next_attempt_at TIMESTAMPTZ,
ADD COLUMN review_reminder_error TEXT NOT NULL DEFAULT '';
-- +goose Down
ALTER TABLE teams
DROP COLUMN review_reminder_sent_at,
DROP COLUMN review_reminder_attempts,
DROP COLUMN review_reminder_next_attempt_at,
DROP COLUMN review_reminder_error;
@@ -0,0 +1,18 @@
-- +goose Up
ALTER TABLE games ADD COLUMN location TEXT;
CREATE TABLE tickets (
id SERIAL PRIMARY KEY,
game_id INTEGER NOT NULL REFERENCES games (id) ON DELETE CASCADE,
token_hash BYTEA NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'used')),
used_at TIMESTAMPTZ,
used_by INTEGER REFERENCES users (id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX tickets_game_id ON tickets (game_id);
-- +goose Down
DROP TABLE IF EXISTS tickets;
ALTER TABLE games DROP COLUMN location;
@@ -0,0 +1,15 @@
-- +goose Up
ALTER TABLE screen_sessions
ADD COLUMN IF NOT EXISTS timer_label TEXT NOT NULL DEFAULT 'Перерыв',
ADD COLUMN IF NOT EXISTS timer_duration_seconds BIGINT NOT NULL DEFAULT 600,
ADD COLUMN IF NOT EXISTS timer_remaining_seconds BIGINT NOT NULL DEFAULT 600,
ADD COLUMN IF NOT EXISTS timer_state INTEGER NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS timer_ends_at TIMESTAMP;
-- +goose Down
ALTER TABLE screen_sessions
DROP COLUMN IF EXISTS timer_ends_at,
DROP COLUMN IF EXISTS timer_state,
DROP COLUMN IF EXISTS timer_remaining_seconds,
DROP COLUMN IF EXISTS timer_duration_seconds,
DROP COLUMN IF EXISTS timer_label;
@@ -0,0 +1,9 @@
-- +goose Up
ALTER TABLE screen_sessions
ALTER COLUMN timer_ends_at TYPE TIMESTAMPTZ
USING timer_ends_at AT TIME ZONE 'Asia/Barnaul';
-- +goose Down
ALTER TABLE screen_sessions
ALTER COLUMN timer_ends_at TYPE TIMESTAMP
USING timer_ends_at AT TIME ZONE 'Asia/Barnaul';
@@ -0,0 +1,6 @@
-- +goose Up
ALTER TABLE game_reviews
ADD COLUMN difficulty INTEGER NOT NULL DEFAULT 2 CHECK (difficulty BETWEEN 1 AND 3);
-- +goose Down
ALTER TABLE game_reviews DROP COLUMN difficulty;