116 lines
4.6 KiB
SQL
116 lines
4.6 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(),
|
|
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;
|