-- 管理员身份与密码摘要。 create table admin_users ( id text primary key, password_hash text not null, created_at timestamptz not null, updated_at timestamptz not null, constraint admin_users_time_ck check (created_at <= updated_at) ); -- 管理操作审计事件。 create table admin_audit_events ( id text primary key, actor_kind text not null, actor_admin_user_id text, actor_ref text not null, admin_request_id text, action text not null, entity_kind text not null, entity_ref text not null, config_revision bigint, changed_fields text[] not null default '{}', created_at timestamptz not null, constraint admin_audit_events_actor_kind_ck check ( actor_kind in ('admin_session', 'admin_api_key', 'system', 'anonymous') ), constraint admin_audit_events_config_revision_ck check ( config_revision is null or config_revision > 0 ), constraint admin_audit_events_changed_fields_ck check ( cardinality(changed_fields) <= 64 and array_position(changed_fields, null) is null ), constraint admin_audit_events_actor_fk foreign key (actor_admin_user_id) references admin_users (id) on update restrict on delete set null, constraint admin_audit_events_actor_identity_ck check ( actor_admin_user_id is null or (actor_kind = 'admin_session' and actor_ref = 'admin:' || actor_admin_user_id) ) ); create index admin_audit_events_actor_idx on admin_audit_events (actor_admin_user_id) where actor_admin_user_id is not null; create index admin_audit_events_created_idx on admin_audit_events (created_at desc, id desc); -- 客户端 API 密钥、鉴权范围与限额。 create table client_api_keys ( id text primary key, name text not null, label text, key text not null unique, enabled boolean not null default true, max_concurrency bigint not null default 0, requests_per_minute bigint not null default 0, last_used_at timestamptz, created_at timestamptz not null, updated_at timestamptz not null, constraint client_api_keys_key_ck check ( key ~ '^sk_[A-Za-z0-9_-]{43}$' ), constraint client_api_keys_limits_ck check ( max_concurrency >= 0 and requests_per_minute >= 0 ), constraint client_api_keys_time_ck check (created_at <= updated_at) ); create index client_api_keys_created_idx on client_api_keys (created_at desc, id desc); -- 全局运行参数与配置版本。 create table runtime_settings ( id bigint primary key, config_revision bigint not null default 1, admin_api_key text, refresh_margin_seconds bigint not null default 3600, refresh_concurrency bigint not null default 2, max_concurrent_per_account bigint not null default 3, request_interval_ms bigint not null default 50, rotation_strategy text not null default 'smart', model_mappings_json jsonb not null default '{}'::jsonb, usage_retention_days bigint not null default 31, ops_event_retention_days bigint not null default 30, audit_retention_days bigint not null default 90, updated_at timestamptz not null, min_codex_desktop_version text, min_codex_cli_version text, constraint runtime_settings_singleton_ck check (id = 1), constraint runtime_settings_revision_ck check (config_revision > 0), constraint runtime_settings_refresh_ck check ( refresh_margin_seconds > 0 and refresh_concurrency > 0 and max_concurrent_per_account > 0 and request_interval_ms >= 0 ), constraint runtime_settings_rotation_ck check ( rotation_strategy in ('smart', 'quota_reset_priority', 'round_robin', 'sticky') ), constraint runtime_settings_model_mappings_ck check ( jsonb_typeof(model_mappings_json) = 'object' and octet_length(model_mappings_json::text) <= 131072 ), constraint runtime_settings_retention_ck check ( usage_retention_days >= 31 and ops_event_retention_days > 0 and audit_retention_days > 0 ), constraint runtime_settings_client_versions_ck check ( ( min_codex_desktop_version is null or ( octet_length(min_codex_desktop_version) between 1 and 64 and min_codex_desktop_version = btrim(min_codex_desktop_version) and min_codex_desktop_version !~ '[[:cntrl:]]' ) ) and ( min_codex_cli_version is null or ( octet_length(min_codex_cli_version) between 1 and 64 and min_codex_cli_version = btrim(min_codex_cli_version) and min_codex_cli_version !~ '[[:cntrl:]]' ) ) ) ); -- 上游 Provider 账号、凭据与可用性状态。 create table provider_accounts ( id text primary key, provider_kind text not null, name text not null, email text, upstream_user_id text, upstream_account_id text, plan_type text, authentication_kind text not null, provider_credentials_json jsonb not null, credential_revision bigint not null default 1, has_refresh_token boolean not null, access_token_expires_at timestamptz, next_refresh_at timestamptz, enabled boolean not null default true, credential_state text not null default 'unknown', provider_quota_json jsonb, last_error_message text, credential_observed_at timestamptz not null, quota_observed_at timestamptz, created_at timestamptz not null, updated_at timestamptz not null, quota_access_state text not null default 'unknown', quota_evidence text, quota_access_observed_at timestamptz, quota_reset_at timestamptz, last_error_reason text, concurrency_limit bigint, weight smallint not null default 1, constraint provider_accounts_revision_ck check (credential_revision > 0), constraint provider_accounts_authentication_kind_ck check ( authentication_kind ~ '^[a-z][a-z0-9_]{0,63}$' ), constraint provider_accounts_credentials_ck check ( jsonb_typeof(provider_credentials_json) = 'object' and octet_length(provider_credentials_json::text) <= 262144 ), constraint provider_accounts_quota_ck check ( provider_quota_json is null or ( jsonb_typeof(provider_quota_json) = 'object' and octet_length(provider_quota_json::text) <= 131072 ) ), constraint provider_accounts_refresh_ck check ( has_refresh_token or next_refresh_at is null ), constraint provider_accounts_credential_state_ck check ( credential_state in ('unknown', 'ready', 'expired', 'banned', 'invalid') ), constraint provider_accounts_quota_access_state_ck check ( quota_access_state in ('unknown', 'allowed', 'exhausted') ), constraint provider_accounts_quota_evidence_ck check ( quota_evidence is null or quota_evidence in ( 'provider_denied', 'account_limit_reached', 'usage_limit_reached', 'payment_required' ) ), constraint provider_accounts_quota_fact_ck check ( (quota_access_state = 'unknown' and quota_evidence is null and quota_reset_at is null) or ( quota_access_state = 'allowed' and quota_evidence is null and quota_access_observed_at is not null and quota_reset_at is null ) or ( quota_access_state = 'exhausted' and quota_evidence is not null and quota_access_observed_at is not null ) ), constraint provider_accounts_quota_observation_ck check ( (provider_quota_json is null) = (quota_observed_at is null) ), constraint provider_accounts_error_reason_ck check ( last_error_reason is null or last_error_reason in ( 'account_unverified', 'access_token_expired', 'credential_expired', 'credential_invalid', 'account_banned' ) ), constraint provider_accounts_concurrency_limit_ck check ( concurrency_limit is null or concurrency_limit between 1 and 4294967295 ), constraint provider_accounts_weight_ck check ( weight between 1 and 100 ), constraint provider_accounts_time_ck check ( created_at <= updated_at and credential_observed_at <= updated_at and (quota_access_observed_at is null or quota_access_observed_at <= updated_at) and (quota_observed_at is null or quota_observed_at <= updated_at) ) ); create unique index provider_accounts_upstream_identity_uq on provider_accounts ( provider_kind, upstream_user_id, coalesce(upstream_account_id, '') ); create index provider_accounts_runtime_idx on provider_accounts (provider_kind, enabled, id); create index provider_accounts_credential_state_idx on provider_accounts (credential_state, id); create index provider_accounts_access_expiry_idx on provider_accounts (access_token_expires_at, id); create index provider_accounts_email_idx on provider_accounts (provider_kind, lower(email)) where email is not null; -- 与 Provider 无关的账号组,以及账号和客户端 API 密钥的组成员关系。 create table account_groups ( id text primary key, name text not null, description text, enabled boolean not null default true, created_at timestamptz not null, updated_at timestamptz not null, color text not null, constraint account_groups_id_ck check ( id ~ '^grp_[0-9a-f]{32}$' ), constraint account_groups_name_ck check ( char_length(btrim(name)) between 1 and 100 and name = btrim(name) and name !~ '[[:cntrl:]]' ), constraint account_groups_description_ck check ( description is null or ( octet_length(description) <= 4096 and description !~ '[[:cntrl:]]' ) ), constraint account_groups_time_ck check ( created_at <= updated_at ), constraint account_groups_color_ck check (color ~ '^#[0-9A-F]{8}$') ); create unique index account_groups_name_uq on account_groups (lower(name)); create table account_group_accounts ( account_group_id text not null, provider_account_id text not null, created_at timestamptz not null, primary key (account_group_id, provider_account_id), constraint account_group_accounts_group_fk foreign key (account_group_id) references account_groups (id) on update restrict on delete cascade, constraint account_group_accounts_account_fk foreign key (provider_account_id) references provider_accounts (id) on update restrict on delete cascade ); create index account_group_accounts_account_idx on account_group_accounts (provider_account_id, account_group_id); create table client_api_key_groups ( client_api_key_id text not null, account_group_id text not null, created_at timestamptz not null, primary key (client_api_key_id, account_group_id), constraint client_api_key_groups_key_fk foreign key (client_api_key_id) references client_api_keys (id) on update restrict on delete cascade, constraint client_api_key_groups_group_fk foreign key (account_group_id) references account_groups (id) on update restrict on delete restrict ); create index client_api_key_groups_group_idx on client_api_key_groups (account_group_id, client_api_key_id); -- 模型请求、上游尝试、用量、计费与结果。 create table model_requests ( id text primary key, client_api_key_id text, client_api_key_ref text not null, config_revision bigint not null, protocol text not null, operation text not null, endpoint text not null, client_transport text not null, requested_model_id text, provider_kind text, upstream_model_id text, service_tier text, provider_account_id text, provider_account_ref text, provider_account_name_snapshot text, provider_account_email_snapshot text, provider_account_authentication_kind_snapshot text, upstream_transport text, http_version text, websocket_pool text, provider_observation_json jsonb, attempt_count integer not null default 0, upstream_send_state text not null default 'not_sent', downstream_committed_at timestamptz, outcome text not null default 'running', client_status_code integer, upstream_status_code integer, client_response_id bytea, upstream_request_id text, upstream_response_id bytea, error_kind text, provider_error_code text, error_message text, retry_after_ms bigint, input_tokens bigint, output_tokens bigint, cached_tokens bigint, cache_write_tokens bigint, reasoning_tokens bigint, image_input_tokens bigint, image_output_tokens bigint, total_tokens bigint, image_generation_succeeded boolean, cost_source text not null default 'unavailable', cost_amount numeric(20, 10), cost_currency text, transport_decision_wait_ms bigint, connect_ms bigint, headers_ms bigint, first_event_ms bigint, first_reasoning_ms bigint, first_text_ms bigint, first_token_ms bigint, provider_processing_ms bigint, latency_ms bigint, client_ip inet, user_agent text, reasoning_effort text, reasoning_preset text, request_kind text, subagent_kind text, compact boolean not null default false, image_generation_requested boolean not null default false, started_at timestamptz not null, deadline_at timestamptz not null, completed_at timestamptz, routing_scope text not null, routing_group_refs text[] not null default '{}', routing_group_names_snapshot jsonb not null default '[]'::jsonb, admission_decision_ms bigint, account_selection_wait_ms bigint, capacity_used_slots bigint, capacity_total_slots bigint, raw_upstream_error text, continuation_affinity_hash text, continuation_previous_response_id_hash text, continuation_requested boolean not null default false, continuation_unavailable_reason text, upstream_connection_id text, upstream_connection_exit_reason text, upstream_connection_age_ms bigint, upstream_connection_idle_ms bigint, recovery_request_id text, recovered_at timestamptz, recovery_attempt_count integer not null default 0, recovery_retry_delay_ms bigint, recovery_total_latency_ms bigint, diagnostic_trace_json jsonb, constraint model_requests_client_ref_ck check ( client_api_key_id is null or client_api_key_id = client_api_key_ref ), constraint model_requests_account_ref_ck check ( provider_account_id is null or provider_account_id = provider_account_ref ), constraint model_requests_revision_attempt_ck check ( config_revision > 0 and attempt_count >= 0 ), constraint model_requests_send_state_ck check ( upstream_send_state in ('not_sent', 'sent', 'ambiguous') and ( ( attempt_count = 0 and upstream_send_state = 'not_sent' ) or ( attempt_count > 0 and provider_kind is not null and provider_account_ref is not null and upstream_transport is not null ) ) ), constraint model_requests_outcome_ck check ( outcome in ('running', 'succeeded', 'failed', 'cancelled', 'incomplete') ), constraint model_requests_status_ck check ( (client_status_code is null or client_status_code between 100 and 599) and (upstream_status_code is null or upstream_status_code between 100 and 599) and (retry_after_ms is null or retry_after_ms >= 0) ), constraint model_requests_tokens_ck check ( (input_tokens is null or input_tokens >= 0) and (output_tokens is null or output_tokens >= 0) and (cached_tokens is null or cached_tokens >= 0) and (cache_write_tokens is null or cache_write_tokens >= 0) and (reasoning_tokens is null or reasoning_tokens >= 0) and (image_input_tokens is null or image_input_tokens >= 0) and (image_output_tokens is null or image_output_tokens >= 0) and (total_tokens is null or total_tokens >= 0) ), constraint model_requests_image_generation_ck check ( ( not image_generation_requested and image_generation_succeeded is null ) or ( image_generation_requested and ( (outcome = 'running' and image_generation_succeeded is null) or (outcome <> 'running' and image_generation_succeeded is not null) ) ) ), constraint model_requests_websocket_pool_ck check ( websocket_pool is null or websocket_pool in ('new', 'reuse') ), constraint model_requests_provider_observation_ck check ( provider_observation_json is null or jsonb_typeof(provider_observation_json) = 'object' ), constraint model_requests_service_tier_ck check ( service_tier is null or ( octet_length(service_tier) between 1 and 64 and service_tier !~ '[[:cntrl:]]' ) ), constraint model_requests_routing_scope_ck check ( routing_scope in ('legacy_provider', 'all', 'groups') ), constraint model_requests_routing_group_names_ck check ( jsonb_typeof(routing_group_names_snapshot) = 'array' and ( ( routing_scope in ('legacy_provider', 'all') and cardinality(routing_group_refs) = 0 and jsonb_array_length(routing_group_names_snapshot) = 0 ) or ( routing_scope = 'groups' and cardinality(routing_group_refs) > 0 and array_position(routing_group_refs, null) is null and jsonb_array_length(routing_group_names_snapshot) = cardinality(routing_group_refs) ) ) ), constraint model_requests_cost_ck check ( cost_source in ('provider_reported', 'calculated', 'unavailable') and ( ( cost_source = 'unavailable' and cost_amount is null and cost_currency is null ) or ( cost_source in ('provider_reported', 'calculated') and cost_amount is not null and cost_amount >= 0 and cost_currency ~ '^[A-Z]{3}$' ) ) ), constraint model_requests_latency_ck check ( (transport_decision_wait_ms is null or transport_decision_wait_ms >= 0) and (connect_ms is null or connect_ms >= 0) and (headers_ms is null or headers_ms >= 0) and (first_event_ms is null or first_event_ms >= 0) and (first_reasoning_ms is null or first_reasoning_ms >= 0) and (first_text_ms is null or first_text_ms >= 0) and (first_token_ms is null or first_token_ms >= 0) and (provider_processing_ms is null or provider_processing_ms >= 0) and (latency_ms is null or latency_ms >= 0) and ( latency_ms is null or ( (transport_decision_wait_ms is null or transport_decision_wait_ms <= latency_ms) and (connect_ms is null or connect_ms <= latency_ms) and (headers_ms is null or headers_ms <= latency_ms) and (first_event_ms is null or first_event_ms <= latency_ms) and (first_reasoning_ms is null or first_reasoning_ms <= latency_ms) and (first_text_ms is null or first_text_ms <= latency_ms) and (first_token_ms is null or first_token_ms <= latency_ms) and (provider_processing_ms is null or provider_processing_ms <= latency_ms) ) ) ), constraint model_requests_lifecycle_ck check ( started_at <= deadline_at and ( (outcome = 'running' and completed_at is null) or ( outcome <> 'running' and completed_at is not null and completed_at >= started_at ) ) ), constraint model_requests_client_fk foreign key (client_api_key_id) references client_api_keys (id) on update restrict on delete set null, constraint model_requests_account_fk foreign key (provider_account_id) references provider_accounts (id) on update restrict on delete set null, constraint model_requests_runtime_pressure_ck check ( (admission_decision_ms is null or admission_decision_ms >= 0) and (account_selection_wait_ms is null or account_selection_wait_ms >= 0) and ( (capacity_used_slots is null and capacity_total_slots is null) or ( capacity_used_slots >= 0 and capacity_total_slots > 0 and capacity_used_slots <= capacity_total_slots ) ) ), constraint model_requests_continuation_hashes_ck check ( (continuation_affinity_hash is null or continuation_affinity_hash ~ '^[0-9a-f]{64}$') and (continuation_previous_response_id_hash is null or continuation_previous_response_id_hash ~ '^[0-9a-f]{64}$') and (continuation_requested or continuation_previous_response_id_hash is null) ), constraint model_requests_continuation_reason_ck check ( continuation_unavailable_reason is null or ( octet_length(continuation_unavailable_reason) between 1 and 64 and continuation_unavailable_reason ~ '^[a-z][a-z0-9_]*$' ) ), constraint model_requests_upstream_connection_ck check ( ( upstream_connection_id is null and upstream_connection_exit_reason is null and upstream_connection_age_ms is null and upstream_connection_idle_ms is null ) or ( octet_length(upstream_connection_id) between 1 and 128 and upstream_connection_id !~ '[[:cntrl:]]' and octet_length(upstream_connection_exit_reason) between 1 and 64 and upstream_connection_exit_reason ~ '^[a-z][a-z0-9_]*$' and upstream_connection_age_ms >= 0 and upstream_connection_idle_ms between 0 and upstream_connection_age_ms ) ), constraint model_requests_recovery_ck check ( recovery_attempt_count >= 0 and ( ( recovered_at is null and recovery_request_id is null and recovery_retry_delay_ms is null and recovery_total_latency_ms is null ) or ( recovered_at is not null and recovered_at >= completed_at and recovery_request_id is not null and octet_length(recovery_request_id) between 1 and 128 and recovery_attempt_count > 0 and recovery_retry_delay_ms >= 0 and recovery_total_latency_ms >= recovery_retry_delay_ms ) ) ), constraint model_requests_diagnostic_trace_object check (diagnostic_trace_json is null or jsonb_typeof(diagnostic_trace_json) = 'object'), constraint model_requests_fact_completeness_ck check ( (provider_account_id is null or provider_account_ref is not null) and (cost_source = 'unavailable' or cost_currency is not null) and num_nonnulls(capacity_used_slots, capacity_total_slots) in (0, 2) and num_nonnulls( upstream_connection_id, upstream_connection_exit_reason, upstream_connection_age_ms, upstream_connection_idle_ms ) in (0, 4) and ( recovered_at is null or ( completed_at is not null and recovery_retry_delay_ms is not null and recovery_total_latency_ms is not null ) ) ) ); create index model_requests_client_idx on model_requests (client_api_key_id) where client_api_key_id is not null; create index model_requests_account_idx on model_requests (provider_account_id) where provider_account_id is not null; create index model_requests_started_idx on model_requests (started_at desc, id desc); create index model_requests_client_ref_idx on model_requests (client_api_key_ref, started_at desc, id desc); create index model_requests_account_ref_idx on model_requests (provider_account_ref, started_at desc, id desc) where provider_account_ref is not null; create index model_requests_requested_model_idx on model_requests (requested_model_id, started_at desc, id desc); create index model_requests_upstream_model_idx on model_requests (upstream_model_id, started_at desc, id desc) where upstream_model_id is not null; create index model_requests_provider_idx on model_requests (provider_kind, started_at desc, id desc) where provider_kind is not null; create index model_requests_outcome_idx on model_requests (outcome, started_at desc, id desc); create index model_requests_running_deadline_idx on model_requests (deadline_at, id) where outcome = 'running'; create index model_requests_retention_idx on model_requests (completed_at) where outcome <> 'running'; create index model_requests_routing_group_refs_idx on model_requests using gin (routing_group_refs); create index model_requests_pending_continuation_recovery_idx on model_requests ( client_api_key_ref, continuation_affinity_hash, completed_at desc, id desc ) where error_kind = 'continuation_recovery_required' and recovered_at is null and continuation_affinity_hash is not null; create index model_requests_recovery_request_idx on model_requests (recovery_request_id) where recovery_request_id is not null; create index model_requests_pending_ws_transport_recovery_idx on model_requests ( client_api_key_ref, continuation_affinity_hash, completed_at desc, id desc ) where provider_kind = 'openai' and outcome = 'failed' and error_kind = 'upstream_unavailable' and upstream_transport = 'websocket' and upstream_send_state = 'ambiguous' and downstream_committed_at is null and recovered_at is null and continuation_affinity_hash is not null; -- 运行期警告与错误事件。 create table ops_events ( id text primary key, model_request_id text, attempt_index integer, level text not null, component text not null, operation text not null, provider_kind text, provider_account_id text, provider_account_ref text, provider_account_name_snapshot text, provider_account_email_snapshot text, provider_account_authentication_kind_snapshot text, upstream_model_id text, failure_kind text not null, status_code integer, provider_error_code text, retry_after_ms bigint, upstream_request_id text, latency_ms bigint, message text not null, created_at timestamptz not null, upstream_send_state text, raw_upstream_error text, constraint ops_events_request_attempt_ck check ( ( model_request_id is null and attempt_index is null ) or ( model_request_id is not null and attempt_index is not null and attempt_index > 0 ) ), constraint ops_events_level_ck check (level in ('warning', 'error')), constraint ops_events_values_ck check ( (status_code is null or status_code between 100 and 599) and (retry_after_ms is null or retry_after_ms >= 0) and (latency_ms is null or latency_ms >= 0) ), constraint ops_events_account_ref_ck check ( provider_account_id is null or provider_account_id = provider_account_ref ), constraint ops_events_request_fk foreign key (model_request_id) references model_requests (id) on update restrict on delete cascade, constraint ops_events_account_fk foreign key (provider_account_id) references provider_accounts (id) on update restrict on delete set null, constraint ops_events_upstream_send_state_ck check ( upstream_send_state is null or upstream_send_state in ('not_sent', 'sent', 'ambiguous') ), constraint ops_events_account_snapshot_present_ck check ( provider_account_id is null or provider_account_ref is not null ) ); create index ops_events_request_idx on ops_events (model_request_id, attempt_index, id); create index ops_events_account_idx on ops_events (provider_account_id) where provider_account_id is not null; create index ops_events_created_idx on ops_events (created_at desc, id desc); create index ops_events_account_ref_idx on ops_events (provider_account_ref, created_at desc, id desc) where provider_account_ref is not null; create index ops_events_retention_idx on ops_events (created_at) where model_request_id is null; insert into runtime_settings (id, updated_at) values (1, now()); -- S3 兼容对象存储、备份计划与保留策略。 create table backup_settings ( id bigint primary key, storage_revision bigint not null default 1, endpoint text, region text, bucket text, access_key_id text, secret_access_key text, prefix text, force_path_style boolean not null default false, schedule_enabled boolean not null default false, cron_expression text, schedule_timezone text default 'Asia/Shanghai', retention_days bigint not null default 0, retention_count bigint not null default 0, next_run_at timestamptz, last_verified_at timestamptz, updated_at timestamptz not null, constraint backup_settings_singleton_ck check (id = 1), constraint backup_settings_revision_ck check (storage_revision > 0), constraint backup_settings_endpoint_ck check ( endpoint is null or ( endpoint ~ '^https?://' and endpoint !~ '[[:space:][:cntrl:]]' and endpoint !~ '://[^/]*@' ) ), constraint backup_settings_lengths_ck check ( (bucket is null or (octet_length(bucket) between 1 and 255 and bucket !~ '[[:cntrl:]]')) and (region is null or (octet_length(region) between 1 and 64 and region !~ '[[:cntrl:]]')) and (prefix is null or (octet_length(prefix) <= 1024 and prefix !~ '[[:cntrl:]]')) and ( access_key_id is null or (octet_length(access_key_id) between 1 and 255 and access_key_id !~ '[[:cntrl:]]') ) and ( secret_access_key is null or ( octet_length(secret_access_key) between 1 and 512 and secret_access_key !~ '[[:cntrl:]]' ) ) ), constraint backup_settings_schedule_ck check ( (cron_expression is null or cron_expression ~ '^[^[:space:]]+([[:space:]]+[^[:space:]]+){4}$') and ( schedule_timezone is null or (octet_length(schedule_timezone) between 1 and 64 and schedule_timezone !~ '[[:cntrl:]]') ) and retention_days >= 0 and retention_count >= 0 and ( not schedule_enabled or ( endpoint is not null and bucket is not null and region is not null and access_key_id is not null and secret_access_key is not null and prefix is not null and cron_expression is not null and schedule_timezone is not null and last_verified_at is not null ) ) ), -- 计划启停与游标一致性:启用必有 next_run_at,关闭必清空。 constraint backup_settings_cursor_ck check ( (not schedule_enabled and next_run_at is null) or (schedule_enabled and next_run_at is not null) ) ); insert into backup_settings ( id, storage_revision, schedule_enabled, retention_days, retention_count, force_path_style, schedule_timezone, updated_at ) values (1, 1, false, 0, 0, false, 'Asia/Shanghai', now()); -- 备份任务状态与归档信息。 create table backup_records ( id text primary key, trigger_kind text not null, status text not null, scheduled_at timestamptz, object_key text not null, size_bytes bigint, sha256 text, expires_at timestamptz, attempt_count integer not null default 0, error_code text, error_message text, started_at timestamptz, completed_at timestamptz, created_at timestamptz not null, updated_at timestamptz not null, constraint backup_records_id_ck check (id ~ '^backup_[0-9a-f]{32}$'), constraint backup_records_trigger_kind_ck check ( trigger_kind in ('manual', 'scheduled') and ( (trigger_kind = 'manual' and scheduled_at is null) or (trigger_kind = 'scheduled' and scheduled_at is not null) ) ), constraint backup_records_status_ck check ( status in ('queued', 'dumping', 'uploading', 'completed', 'failed', 'deleting') ), constraint backup_records_attempt_ck check (attempt_count >= 0), constraint backup_records_lifecycle_ck check ( created_at <= updated_at and (started_at is null or started_at >= created_at) and (completed_at is null or completed_at >= created_at) and ( ( status = 'queued' and started_at is null and completed_at is null ) or ( status in ('dumping', 'uploading') and started_at is not null and completed_at is null ) or ( status in ('completed', 'failed', 'deleting') and started_at is not null and completed_at is not null ) ) ), constraint backup_records_artifact_ck check ( ( status not in ('uploading', 'completed') or (size_bytes is not null and sha256 is not null) ) and (size_bytes is null or size_bytes >= 0) and (sha256 is null or sha256 ~ '^[0-9a-f]{64}$') ), constraint backup_records_error_ck check ( (error_code is null) = (error_message is null) and (status <> 'failed' or (error_code is not null and error_message is not null)) and ( status not in ('queued', 'dumping', 'uploading', 'completed') or error_code is null ) ), -- expires_at 为空,或不早于创建时间(创建时 now + 天数 恒晚于 created_at)。 constraint backup_records_expiry_ck check ( expires_at is null or expires_at >= created_at ), constraint backup_records_completion_order_ck check ( completed_at is null or completed_at >= started_at ) ); create index backup_records_created_idx on backup_records (created_at desc, id desc); create index backup_records_status_idx on backup_records (status, created_at, id); create unique index backup_records_scheduled_uq on backup_records (scheduled_at) where trigger_kind = 'scheduled' and scheduled_at is not null; create unique index backup_records_active_uq on backup_records ((1)) where status in ('queued', 'dumping', 'uploading'); create unique index backup_records_object_key_uq on backup_records (object_key) where object_key is not null; create index backup_records_scheduled_completed_idx on backup_records (completed_at desc, id desc) where trigger_kind = 'scheduled' and status = 'completed'; create index backup_records_retention_idx on backup_records (expires_at) where expires_at is not null and status in ('completed', 'failed');