Files
XQKqueue/server/migrations/000001_initial.up.sql
2026-07-12 15:53:24 +08:00

235 lines
11 KiB
SQL

CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE users (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
username varchar(80) NOT NULL,
password_hash text NOT NULL,
role varchar(16) NOT NULL,
active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT users_username_format CHECK (username = lower(username) AND username ~ '^[a-z0-9][a-z0-9._-]{2,79}$'),
CONSTRAINT users_role_check CHECK (role IN ('ADMIN', 'STAFF')),
CONSTRAINT users_username_unique UNIQUE (username)
);
CREATE TABLE projects (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
code varchar(24) NOT NULL,
name varchar(120) NOT NULL,
status varchar(16) NOT NULL DEFAULT 'RUNNING',
timezone varchar(64) NOT NULL DEFAULT 'Asia/Shanghai',
ticket_prefix varchar(8) NOT NULL DEFAULT 'A',
call_batch_size integer NOT NULL DEFAULT 5,
grace_period_minutes integer NOT NULL DEFAULT 5,
eta_mode varchar(24) NOT NULL DEFAULT 'FIXED_BATCH',
average_batch_interval_seconds integer NOT NULL DEFAULT 600,
continuous_rate_per_minute numeric(10,2) NOT NULL DEFAULT 2.0,
eta_buffer_minutes integer NOT NULL DEFAULT 5,
display_token_hash char(64),
device_simulation_mode varchar(16) NOT NULL DEFAULT 'SUCCESS',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT projects_code_format CHECK (code = upper(code) AND code ~ '^[A-Z0-9][A-Z0-9_-]{1,23}$'),
CONSTRAINT projects_code_unique UNIQUE (code),
CONSTRAINT projects_status_check CHECK (status IN ('NOT_OPEN', 'RUNNING', 'PAUSED', 'ENDED')),
CONSTRAINT projects_ticket_prefix_check CHECK (ticket_prefix ~ '^[A-Z0-9]{1,8}$'),
CONSTRAINT projects_batch_size_check CHECK (call_batch_size BETWEEN 1 AND 100),
CONSTRAINT projects_grace_check CHECK (grace_period_minutes BETWEEN 0 AND 120),
CONSTRAINT projects_eta_mode_check CHECK (eta_mode IN ('FIXED_BATCH', 'CONTINUOUS')),
CONSTRAINT projects_batch_interval_check CHECK (average_batch_interval_seconds BETWEEN 1 AND 86400),
CONSTRAINT projects_rate_check CHECK (continuous_rate_per_minute > 0 AND continuous_rate_per_minute <= 10000),
CONSTRAINT projects_eta_buffer_check CHECK (eta_buffer_minutes BETWEEN 0 AND 1440),
CONSTRAINT projects_display_token_hash_check CHECK (display_token_hash IS NULL OR display_token_hash ~ '^[0-9a-f]{64}$'),
CONSTRAINT projects_display_token_hash_unique UNIQUE (display_token_hash),
CONSTRAINT projects_device_mode_check CHECK (device_simulation_mode IN ('DISABLED', 'SUCCESS', 'FAILURE'))
);
CREATE TABLE user_projects (
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, project_id)
);
CREATE TABLE auth_sessions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash char(64) NOT NULL,
expires_at timestamptz NOT NULL,
last_seen_at timestamptz NOT NULL DEFAULT now(),
revoked_at timestamptz,
created_ip inet,
user_agent varchar(512) NOT NULL DEFAULT '',
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT auth_sessions_token_hash_check CHECK (token_hash ~ '^[0-9a-f]{64}$'),
CONSTRAINT auth_sessions_token_hash_unique UNIQUE (token_hash)
);
CREATE INDEX auth_sessions_user_active_idx
ON auth_sessions (user_id, expires_at)
WHERE revoked_at IS NULL;
CREATE TABLE queue_sessions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
business_date date NOT NULL,
status varchar(16) NOT NULL DEFAULT 'RUNNING',
next_ticket_number integer NOT NULL DEFAULT 1,
revision bigint NOT NULL DEFAULT 0,
opened_at timestamptz NOT NULL DEFAULT now(),
closed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT queue_sessions_status_check CHECK (status IN ('RUNNING', 'PAUSED', 'ENDED')),
CONSTRAINT queue_sessions_next_ticket_check CHECK (next_ticket_number > 0),
CONSTRAINT queue_sessions_revision_check CHECK (revision >= 0),
CONSTRAINT queue_sessions_project_date_unique UNIQUE (project_id, business_date),
CONSTRAINT queue_sessions_id_project_unique UNIQUE (id, project_id)
);
CREATE TABLE queue_tickets (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
queue_session_id uuid NOT NULL,
ticket_number integer NOT NULL,
display_number varchar(24) NOT NULL,
public_token_hash char(64) NOT NULL,
phone_ciphertext bytea NOT NULL,
phone_nonce bytea NOT NULL,
phone_hmac char(64) NOT NULL,
last_name_ciphertext bytea,
last_name_nonce bytea,
honorific varchar(16) NOT NULL DEFAULT '游客',
status varchar(16) NOT NULL DEFAULT 'WAITING',
joined_at timestamptz NOT NULL DEFAULT now(),
called_at timestamptz,
arrived_at timestamptz,
completed_at timestamptz,
missed_at timestamptz,
personal_data_purge_at timestamptz NOT NULL DEFAULT (now() + interval '30 days'),
created_by uuid NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT queue_tickets_session_project_fk FOREIGN KEY (queue_session_id, project_id)
REFERENCES queue_sessions(id, project_id) ON DELETE RESTRICT,
CONSTRAINT queue_tickets_number_check CHECK (ticket_number > 0),
CONSTRAINT queue_tickets_public_token_hash_check CHECK (public_token_hash ~ '^[0-9a-f]{64}$'),
CONSTRAINT queue_tickets_phone_nonce_check CHECK (octet_length(phone_nonce) = 12),
CONSTRAINT queue_tickets_phone_hmac_check CHECK (phone_hmac ~ '^[0-9a-f]{64}$'),
CONSTRAINT queue_tickets_last_name_pair_check CHECK (
(last_name_ciphertext IS NULL AND last_name_nonce IS NULL)
OR (last_name_ciphertext IS NOT NULL AND octet_length(last_name_nonce) = 12)
),
CONSTRAINT queue_tickets_honorific_check CHECK (honorific IN ('游客', '先生', '女士')),
CONSTRAINT queue_tickets_status_check CHECK (status IN ('WAITING', 'CALLED', 'ARRIVED', 'COMPLETED', 'MISSED', 'CANCELED')),
CONSTRAINT queue_tickets_project_display_unique UNIQUE (project_id, queue_session_id, display_number),
CONSTRAINT queue_tickets_project_number_unique UNIQUE (project_id, queue_session_id, ticket_number),
CONSTRAINT queue_tickets_public_token_unique UNIQUE (public_token_hash),
CONSTRAINT queue_tickets_id_project_unique UNIQUE (id, project_id)
);
CREATE INDEX queue_tickets_fifo_idx
ON queue_tickets (project_id, queue_session_id, ticket_number)
WHERE status = 'WAITING';
CREATE INDEX queue_tickets_active_phone_idx
ON queue_tickets (project_id, phone_hmac, joined_at DESC)
WHERE status IN ('WAITING', 'CALLED', 'ARRIVED');
CREATE TABLE call_batches (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
queue_session_id uuid NOT NULL,
batch_sequence integer NOT NULL,
revision bigint NOT NULL,
status varchar(16) NOT NULL DEFAULT 'CALLED',
requested_by uuid NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
called_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT call_batches_session_project_fk FOREIGN KEY (queue_session_id, project_id)
REFERENCES queue_sessions(id, project_id) ON DELETE RESTRICT,
CONSTRAINT call_batches_sequence_check CHECK (batch_sequence > 0),
CONSTRAINT call_batches_revision_check CHECK (revision > 0),
CONSTRAINT call_batches_status_check CHECK (status IN ('CALLED', 'COMPLETED')),
CONSTRAINT call_batches_session_sequence_unique UNIQUE (queue_session_id, batch_sequence),
CONSTRAINT call_batches_session_revision_unique UNIQUE (queue_session_id, revision),
CONSTRAINT call_batches_id_project_unique UNIQUE (id, project_id)
);
CREATE TABLE call_batch_tickets (
call_batch_id uuid NOT NULL,
ticket_id uuid NOT NULL,
project_id uuid NOT NULL,
position integer NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (call_batch_id, ticket_id),
CONSTRAINT call_batch_tickets_batch_project_fk FOREIGN KEY (call_batch_id, project_id)
REFERENCES call_batches(id, project_id) ON DELETE CASCADE,
CONSTRAINT call_batch_tickets_ticket_project_fk FOREIGN KEY (ticket_id, project_id)
REFERENCES queue_tickets(id, project_id) ON DELETE RESTRICT,
CONSTRAINT call_batch_tickets_position_check CHECK (position > 0),
CONSTRAINT call_batch_tickets_ticket_once_unique UNIQUE (ticket_id),
CONSTRAINT call_batch_tickets_position_unique UNIQUE (call_batch_id, position)
);
CREATE TABLE audit_entries (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid REFERENCES projects(id) ON DELETE RESTRICT,
actor_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
action varchar(64) NOT NULL,
entity_type varchar(64) NOT NULL,
entity_id uuid,
details jsonb NOT NULL DEFAULT '{}'::jsonb,
request_id varchar(80) NOT NULL DEFAULT '',
ip_address inet,
user_agent varchar(512) NOT NULL DEFAULT '',
retain_until timestamptz NOT NULL DEFAULT (now() + interval '1 year'),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX audit_entries_project_created_idx
ON audit_entries (project_id, created_at DESC);
CREATE INDEX audit_entries_actor_created_idx
ON audit_entries (actor_user_id, created_at DESC);
CREATE TABLE idempotency_keys (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
scope varchar(40) NOT NULL,
key varchar(128) NOT NULL,
request_hash char(64) NOT NULL,
response_code integer NOT NULL,
response_body jsonb NOT NULL,
resource_id uuid,
expires_at timestamptz NOT NULL DEFAULT (now() + interval '24 hours'),
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT idempotency_request_hash_check CHECK (request_hash ~ '^[0-9a-f]{64}$'),
CONSTRAINT idempotency_response_code_check CHECK (response_code BETWEEN 200 AND 599),
CONSTRAINT idempotency_scope_key_unique UNIQUE (project_id, user_id, scope, key)
);
CREATE INDEX idempotency_keys_expiry_idx ON idempotency_keys (expires_at);
CREATE TABLE device_simulations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
call_batch_id uuid NOT NULL,
adapter varchar(40) NOT NULL DEFAULT 'SIMULATOR',
outcome varchar(16) NOT NULL,
detail varchar(500) NOT NULL DEFAULT '',
attempted_at timestamptz NOT NULL DEFAULT now(),
completed_at timestamptz NOT NULL DEFAULT now(),
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT device_simulations_batch_project_fk FOREIGN KEY (call_batch_id, project_id)
REFERENCES call_batches(id, project_id) ON DELETE CASCADE,
CONSTRAINT device_simulations_outcome_check CHECK (outcome IN ('SUCCESS', 'FAILURE', 'SKIPPED')),
CONSTRAINT device_simulations_batch_unique UNIQUE (call_batch_id)
);
CREATE INDEX device_simulations_project_created_idx
ON device_simulations (project_id, created_at DESC);