www_site/services/api/migrations/001_initial.sql

117 lines
4.7 KiB
SQL

CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT UNIQUE,
email TEXT UNIQUE,
phone TEXT UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin')),
plan TEXT NOT NULL DEFAULT 'free' CHECK (plan IN ('free', 'vip')),
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
email_verified BOOLEAN NOT NULL DEFAULT false,
phone_verified BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_login_at TIMESTAMPTZ,
CHECK (email IS NOT NULL OR phone IS NOT NULL)
);
CREATE TABLE IF NOT EXISTS sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS sessions_user_idx ON sessions(user_id, expires_at);
CREATE TABLE IF NOT EXISTS plan_policies (
code TEXT PRIMARY KEY CHECK (code IN ('free', 'vip')),
period_limit BIGINT NOT NULL CHECK (period_limit >= 0),
max_text_length INTEGER NOT NULL CHECK (max_text_length >= 0),
max_concurrency INTEGER NOT NULL CHECK (max_concurrency >= 0),
rate_limit INTEGER NOT NULL CHECK (rate_limit >= 0),
allowed_voices JSONB NOT NULL DEFAULT '[]'::jsonb,
version INTEGER NOT NULL DEFAULT 1,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS membership_grants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
plan TEXT NOT NULL CHECK (plan IN ('free', 'vip')),
starts_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
created_by UUID REFERENCES users(id),
reason TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (expires_at IS NULL OR expires_at > starts_at)
);
CREATE INDEX IF NOT EXISTS membership_active_idx ON membership_grants(user_id, starts_at, expires_at, revoked_at);
CREATE TABLE IF NOT EXISTS quota_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
period_start TIMESTAMPTZ NOT NULL,
period_end TIMESTAMPTZ NOT NULL,
limit_snapshot BIGINT NOT NULL CHECK (limit_snapshot >= 0),
adjustment BIGINT NOT NULL DEFAULT 0,
used BIGINT NOT NULL DEFAULT 0 CHECK (used >= 0),
reserved BIGINT NOT NULL DEFAULT 0 CHECK (reserved >= 0),
version BIGINT NOT NULL DEFAULT 0,
UNIQUE (user_id, period_start, period_end),
CHECK (period_end > period_start)
);
CREATE TABLE IF NOT EXISTS usage_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
quota_account_id UUID NOT NULL REFERENCES quota_accounts(id) ON DELETE CASCADE,
task_id UUID,
type TEXT NOT NULL CHECK (type IN ('reserve', 'consume', 'release', 'adjust')),
amount BIGINT NOT NULL,
idempotency_key TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS usage_adjustment_idem_idx ON usage_records(user_id, idempotency_key) WHERE idempotency_key IS NOT NULL;
CREATE TABLE IF NOT EXISTS verification_challenges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
channel TEXT NOT NULL CHECK (channel IN ('email', 'phone')),
purpose TEXT NOT NULL CHECK (purpose IN ('registration', 'contact_binding', 'contact_change', 'password_reset')),
target TEXT NOT NULL,
code_digest TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
attempt_count INTEGER NOT NULL DEFAULT 0,
consumed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS admin_audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
actor_id UUID NOT NULL REFERENCES users(id),
action TEXT NOT NULL,
target_type TEXT NOT NULL,
target_id UUID NOT NULL,
before_value JSONB,
after_value JSONB,
reason TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO plan_policies(code, period_limit, max_text_length, max_concurrency, rate_limit, allowed_voices)
VALUES
('free', 10000, 5000, 1, 10, '[]'::jsonb),
('vip', 100000, 10000, 3, 30, '[]'::jsonb)
ON CONFLICT (code) DO NOTHING;