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);