CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE TABLE IF NOT EXISTS admin_users ( id BIGSERIAL PRIMARY KEY, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, role TEXT NOT NULL, status TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL ); CREATE TABLE IF NOT EXISTS admin_audit_logs ( id BIGSERIAL PRIMARY KEY, actor_user_id BIGINT, actor_username TEXT NOT NULL DEFAULT '', actor_role TEXT NOT NULL DEFAULT '', action TEXT NOT NULL, target_type TEXT NOT NULL, target_id TEXT NOT NULL, payload_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_created_at ON admin_audit_logs(created_at DESC); CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_target ON admin_audit_logs(target_type, target_id); CREATE TABLE IF NOT EXISTS fulfillment_profiles ( id BIGSERIAL PRIMARY KEY, profile_key TEXT NOT NULL UNIQUE, name TEXT NOT NULL, executor_key TEXT NOT NULL, requires_claim BOOLEAN NOT NULL DEFAULT FALSE, auto_dispatch BOOLEAN NOT NULL DEFAULT FALSE, inventory_strategy TEXT NOT NULL DEFAULT 'static_pool', config_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL ); CREATE TABLE IF NOT EXISTS fulfillment_profile_requirements ( id BIGSERIAL PRIMARY KEY, profile_id BIGINT NOT NULL REFERENCES fulfillment_profiles(id) ON DELETE CASCADE, role_key TEXT NOT NULL, credential_type TEXT NOT NULL, quantity_per_unit INTEGER NOT NULL DEFAULT 1, is_required BOOLEAN NOT NULL DEFAULT TRUE, config_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL, UNIQUE(profile_id, role_key) ); CREATE TABLE IF NOT EXISTS sku_fulfillment_bindings ( id BIGSERIAL PRIMARY KEY, sku_code TEXT NOT NULL, provider TEXT NOT NULL DEFAULT '', platform TEXT NOT NULL DEFAULT '', shop_id TEXT NOT NULL DEFAULT '', profile_id BIGINT NOT NULL REFERENCES fulfillment_profiles(id) ON DELETE CASCADE, enabled BOOLEAN NOT NULL DEFAULT TRUE, priority INTEGER NOT NULL DEFAULT 100, config_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_sku_fulfillment_bindings_lookup ON sku_fulfillment_bindings(sku_code, provider, platform, shop_id, enabled, priority); CREATE TABLE IF NOT EXISTS product_match_rules ( id BIGSERIAL PRIMARY KEY, provider TEXT NOT NULL DEFAULT '', platform TEXT NOT NULL DEFAULT '', shop_id TEXT NOT NULL DEFAULT '', external_item_id TEXT NOT NULL DEFAULT '', external_sku_code TEXT NOT NULL DEFAULT '', external_sku_name TEXT NOT NULL DEFAULT '', external_sku_name_normalized TEXT NOT NULL DEFAULT '', resolved_sku_code TEXT NOT NULL, enabled BOOLEAN NOT NULL DEFAULT TRUE, priority INTEGER NOT NULL DEFAULT 100, config_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS idx_product_match_rules_unique ON product_match_rules( provider, platform, shop_id, external_item_id, external_sku_code, external_sku_name_normalized, resolved_sku_code ); CREATE INDEX IF NOT EXISTS idx_product_match_rules_lookup ON product_match_rules(provider, platform, shop_id, enabled, priority, id); CREATE TABLE IF NOT EXISTS orders ( id BIGSERIAL PRIMARY KEY, provider TEXT NOT NULL, platform TEXT NOT NULL, shop_id TEXT NOT NULL DEFAULT '', shop_name TEXT NOT NULL DEFAULT '', platform_order_id TEXT NOT NULL, order_status TEXT NOT NULL DEFAULT 'created', pay_status TEXT NOT NULL DEFAULT 'unpaid', buyer_id TEXT NOT NULL DEFAULT '', buyer_name TEXT NOT NULL DEFAULT '', receiver_contact TEXT NOT NULL DEFAULT '', total_amount INTEGER NOT NULL DEFAULT 0, currency TEXT NOT NULL DEFAULT 'CNY', raw_payload_json JSONB NOT NULL DEFAULT '{}'::jsonb, paid_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL, UNIQUE(provider, platform, shop_id, platform_order_id) ); CREATE TABLE IF NOT EXISTS order_items ( id BIGSERIAL PRIMARY KEY, order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE, sku_code TEXT NOT NULL, sku_name TEXT NOT NULL DEFAULT '', quantity INTEGER NOT NULL DEFAULT 1, spec_json JSONB NOT NULL DEFAULT '{}'::jsonb, item_snapshot_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_order_items_order_id ON order_items(order_id); CREATE INDEX IF NOT EXISTS idx_order_items_sku_code ON order_items(sku_code); CREATE TABLE IF NOT EXISTS inventory_items ( id BIGSERIAL PRIMARY KEY, batch_no TEXT NOT NULL DEFAULT '', sku_code TEXT NOT NULL, credential_type TEXT NOT NULL, display_value TEXT NOT NULL DEFAULT '', payload_json JSONB NOT NULL DEFAULT '{}'::jsonb, source_type TEXT NOT NULL DEFAULT 'static_import', status TEXT NOT NULL DEFAULT 'available', invalid_reason TEXT NOT NULL DEFAULT '', inventory_group_code TEXT NOT NULL DEFAULT '', metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, consumed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS idx_inventory_items_unique_payload ON inventory_items(sku_code, credential_type, md5(payload_json::text)); CREATE INDEX IF NOT EXISTS idx_inventory_items_pool ON inventory_items(sku_code, credential_type, status, id); CREATE INDEX IF NOT EXISTS idx_inventory_items_group_lookup ON inventory_items(sku_code, credential_type, inventory_group_code, status, id); CREATE TABLE IF NOT EXISTS admin_user_inventory_group_bindings ( id BIGSERIAL PRIMARY KEY, admin_user_id BIGINT NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE, inventory_group_code TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL, UNIQUE(admin_user_id, inventory_group_code) ); CREATE INDEX IF NOT EXISTS idx_admin_user_inventory_group_bindings_user_id ON admin_user_inventory_group_bindings(admin_user_id, inventory_group_code); CREATE TABLE IF NOT EXISTS fulfillment_tasks ( id BIGSERIAL PRIMARY KEY, order_id BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE, order_item_id BIGINT NOT NULL REFERENCES order_items(id) ON DELETE CASCADE, unit_index INTEGER NOT NULL DEFAULT 1, task_no TEXT NOT NULL UNIQUE, provider TEXT NOT NULL, platform TEXT NOT NULL, shop_id TEXT NOT NULL DEFAULT '', shop_name TEXT NOT NULL DEFAULT '', platform_order_id TEXT NOT NULL, profile_id BIGINT NOT NULL REFERENCES fulfillment_profiles(id) ON DELETE RESTRICT, executor_key TEXT NOT NULL, task_status TEXT NOT NULL DEFAULT 'pending_payment', inventory_status TEXT NOT NULL DEFAULT 'pending', delivery_status TEXT NOT NULL DEFAULT 'pending', result_code TEXT NOT NULL DEFAULT '', result_message TEXT NOT NULL DEFAULT '', claim_token TEXT NOT NULL DEFAULT '', claim_expires_at TIMESTAMPTZ, automation_mode TEXT NOT NULL DEFAULT 'manual', requires_claim BOOLEAN NOT NULL DEFAULT FALSE, user_action_status TEXT NOT NULL DEFAULT 'not_required', attempt_count INTEGER NOT NULL DEFAULT 0, last_error TEXT NOT NULL DEFAULT '', context_json JSONB NOT NULL DEFAULT '{}'::jsonb, claimed_at TIMESTAMPTZ, role_confirmed_at TIMESTAMPTZ, redeemed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL, UNIQUE(order_item_id, unit_index) ); CREATE INDEX IF NOT EXISTS idx_fulfillment_tasks_status ON fulfillment_tasks(task_status, delivery_status); CREATE INDEX IF NOT EXISTS idx_fulfillment_tasks_order ON fulfillment_tasks(order_id, order_item_id); CREATE INDEX IF NOT EXISTS idx_fulfillment_tasks_claim_token ON fulfillment_tasks(claim_token); CREATE TABLE IF NOT EXISTS claim_tokens ( id BIGSERIAL PRIMARY KEY, task_id BIGINT NOT NULL UNIQUE REFERENCES fulfillment_tasks(id) ON DELETE CASCADE, token TEXT NOT NULL UNIQUE, status TEXT NOT NULL DEFAULT 'active', expired_at TIMESTAMPTZ NOT NULL, used_at TIMESTAMPTZ, max_use_count INTEGER NOT NULL DEFAULT 1, used_count INTEGER NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_claim_tokens_status ON claim_tokens(status, expired_at); CREATE TABLE IF NOT EXISTS task_inventory_bindings ( id BIGSERIAL PRIMARY KEY, task_id BIGINT NOT NULL REFERENCES fulfillment_tasks(id) ON DELETE CASCADE, inventory_item_id BIGINT NOT NULL REFERENCES inventory_items(id) ON DELETE RESTRICT, role_key TEXT NOT NULL, quantity INTEGER NOT NULL DEFAULT 1, binding_status TEXT NOT NULL DEFAULT 'reserved', consumed_at TIMESTAMPTZ, released_at TIMESTAMPTZ, metadata_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL, UNIQUE(task_id, role_key, inventory_item_id) ); CREATE INDEX IF NOT EXISTS idx_task_inventory_bindings_task ON task_inventory_bindings(task_id); CREATE INDEX IF NOT EXISTS idx_task_inventory_bindings_inventory ON task_inventory_bindings(inventory_item_id); CREATE TABLE IF NOT EXISTS task_events ( id BIGSERIAL PRIMARY KEY, task_id BIGINT NOT NULL REFERENCES fulfillment_tasks(id) ON DELETE CASCADE, event_type TEXT NOT NULL, payload_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_task_events_task_created_at ON task_events(task_id, created_at DESC); CREATE TABLE IF NOT EXISTS task_runtime_contexts ( id BIGSERIAL PRIMARY KEY, task_id BIGINT NOT NULL UNIQUE REFERENCES fulfillment_tasks(id) ON DELETE CASCADE, runtime_session_id TEXT NOT NULL DEFAULT '', login_type TEXT NOT NULL DEFAULT '', nickname TEXT NOT NULL DEFAULT '', role_id TEXT NOT NULL DEFAULT '', role_name TEXT NOT NULL DEFAULT '', area TEXT NOT NULL DEFAULT '', partition_name TEXT NOT NULL DEFAULT '', screenshot_path TEXT NOT NULL DEFAULT '', artifacts_json JSONB NOT NULL DEFAULT '{}'::jsonb, state_json JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL, updated_at TIMESTAMPTZ NOT NULL ); CREATE TABLE IF NOT EXISTS webhook_events ( id BIGSERIAL PRIMARY KEY, provider TEXT NOT NULL DEFAULT '', platform TEXT NOT NULL DEFAULT '', shop_id TEXT NOT NULL DEFAULT '', shop_name TEXT NOT NULL DEFAULT '', event_type TEXT NOT NULL, event_key TEXT NOT NULL, signature_valid BOOLEAN NOT NULL DEFAULT FALSE, headers_json JSONB NOT NULL DEFAULT '{}'::jsonb, query_json JSONB NOT NULL DEFAULT '{}'::jsonb, body_json JSONB NOT NULL DEFAULT '{}'::jsonb, processed BOOLEAN NOT NULL DEFAULT FALSE, process_error TEXT NOT NULL DEFAULT '', related_order_id BIGINT REFERENCES orders(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL ); CREATE INDEX IF NOT EXISTS idx_webhook_events_lookup ON webhook_events(provider, platform, shop_id, event_key); CREATE INDEX IF NOT EXISTS idx_webhook_events_processed ON webhook_events(processed, created_at DESC);