feat(billing): 完成异步结算与请求执行闭环

统一任务成功、Attempt 与结算 Outbox 的事务边界,增加多实例安全结算、人工复核、请求幂等与执行租约。钱包决策使用九位精确金额,并通过审计保护约束保留流水事实;同时补充管理接口、指标与 PostgreSQL 集成测试。
This commit is contained in:
2026-07-21 00:25:53 +08:00
parent 7cea21f765
commit 5b2b94b1bd
33 changed files with 2884 additions and 418 deletions
@@ -1,16 +1,24 @@
ALTER TABLE gateway_tasks
ADD COLUMN IF NOT EXISTS billing_version text NOT NULL DEFAULT 'effective-pricing-v2',
ADD COLUMN IF NOT EXISTS billing_status text NOT NULL DEFAULT 'not_started',
ADD COLUMN IF NOT EXISTS billing_currency text NOT NULL DEFAULT 'resource',
ADD COLUMN IF NOT EXISTS pricing_snapshot jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS billing_version text DEFAULT 'effective-pricing-v2',
ADD COLUMN IF NOT EXISTS billing_status text DEFAULT 'not_started',
ADD COLUMN IF NOT EXISTS billing_currency text DEFAULT 'resource',
ADD COLUMN IF NOT EXISTS pricing_snapshot jsonb DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS request_fingerprint text,
ADD COLUMN IF NOT EXISTS idempotency_key_hash text,
ADD COLUMN IF NOT EXISTS reservation_amount numeric(38, 9) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS idempotency_request_hash text,
ADD COLUMN IF NOT EXISTS reservation_amount numeric(38, 9) DEFAULT 0,
ADD COLUMN IF NOT EXISTS execution_token uuid,
ADD COLUMN IF NOT EXISTS execution_lease_expires_at timestamptz,
ADD COLUMN IF NOT EXISTS billing_updated_at timestamptz,
ADD COLUMN IF NOT EXISTS billing_settled_at timestamptz;
UPDATE gateway_tasks
SET billing_version = COALESCE(billing_version, 'effective-pricing-v2'),
billing_status = COALESCE(billing_status, 'not_started'),
billing_currency = COALESCE(billing_currency, 'resource'),
pricing_snapshot = COALESCE(pricing_snapshot, '{}'::jsonb),
reservation_amount = COALESCE(reservation_amount, 0);
ALTER TABLE gateway_tasks
ADD CONSTRAINT gateway_tasks_billing_status_v2_check CHECK (
billing_status IN (
@@ -32,28 +40,40 @@ CREATE INDEX IF NOT EXISTS idx_gateway_tasks_billing_status_v2
ON gateway_tasks(billing_status, billing_updated_at, created_at);
ALTER TABLE gateway_task_attempts
ADD COLUMN IF NOT EXISTS pricing_snapshot jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS pricing_snapshot jsonb DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS request_fingerprint text,
ADD COLUMN IF NOT EXISTS upstream_submission_status text NOT NULL DEFAULT 'not_submitted',
ADD COLUMN IF NOT EXISTS upstream_submission_status text DEFAULT 'not_submitted',
ADD COLUMN IF NOT EXISTS upstream_submission_updated_at timestamptz;
UPDATE gateway_task_attempts
SET pricing_snapshot = COALESCE(pricing_snapshot, '{}'::jsonb),
upstream_submission_status = COALESCE(upstream_submission_status, 'not_submitted');
ALTER TABLE gateway_task_attempts
ADD CONSTRAINT gateway_task_attempts_submission_v2_check CHECK (
upstream_submission_status IN ('not_submitted', 'submitting', 'response_received')
) NOT VALID;
ALTER TABLE settlement_outbox
ADD COLUMN IF NOT EXISTS action text NOT NULL DEFAULT 'settle',
ADD COLUMN IF NOT EXISTS amount numeric(38, 9) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS currency text NOT NULL DEFAULT 'resource',
ADD COLUMN IF NOT EXISTS pricing_snapshot jsonb NOT NULL DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS action text DEFAULT 'settle',
ADD COLUMN IF NOT EXISTS amount numeric(38, 9) DEFAULT 0,
ADD COLUMN IF NOT EXISTS currency text DEFAULT 'resource',
ADD COLUMN IF NOT EXISTS pricing_snapshot jsonb DEFAULT '{}'::jsonb,
ADD COLUMN IF NOT EXISTS locked_by text,
ADD COLUMN IF NOT EXISTS lock_token uuid,
ADD COLUMN IF NOT EXISTS locked_at timestamptz,
ADD COLUMN IF NOT EXISTS last_error_code text,
ADD COLUMN IF NOT EXISTS last_error_message text,
ADD COLUMN IF NOT EXISTS completed_at timestamptz,
ADD COLUMN IF NOT EXISTS manual_review_reason text;
ADD COLUMN IF NOT EXISTS manual_review_reason text,
ADD COLUMN IF NOT EXISTS retry_idempotency_key_hash text,
ADD COLUMN IF NOT EXISTS retry_requested_at timestamptz;
UPDATE settlement_outbox
SET action = COALESCE(action, 'settle'),
amount = COALESCE(amount, 0),
currency = COALESCE(currency, 'resource'),
pricing_snapshot = COALESCE(pricing_snapshot, '{}'::jsonb);
ALTER TABLE settlement_outbox
ADD CONSTRAINT settlement_outbox_action_v2_check CHECK (action IN ('settle', 'release')) NOT VALID,
@@ -71,7 +91,9 @@ CREATE INDEX IF NOT EXISTS idx_settlement_outbox_stale_v2
WHERE status = 'processing';
ALTER TABLE model_pricing_rules
ADD COLUMN IF NOT EXISTS is_free boolean NOT NULL DEFAULT false;
ADD COLUMN IF NOT EXISTS is_free boolean DEFAULT false;
UPDATE model_pricing_rules SET is_free = false WHERE is_free IS NULL;
ALTER TABLE model_pricing_rules
ADD CONSTRAINT model_pricing_rules_explicit_free_v2_check CHECK (
@@ -82,9 +104,18 @@ ALTER TABLE gateway_wallet_accounts
ADD CONSTRAINT gateway_wallet_nonnegative_frozen_v2_check CHECK (frozen_balance >= 0) NOT VALID,
ADD CONSTRAINT gateway_wallet_balance_covers_frozen_v2_check CHECK (balance >= frozen_balance) NOT VALID;
CREATE TABLE IF NOT EXISTS gateway_wallet_account_audit_guards (
account_id uuid PRIMARY KEY REFERENCES gateway_wallet_accounts(id) ON DELETE RESTRICT,
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO gateway_wallet_account_audit_guards (account_id)
SELECT id FROM gateway_wallet_accounts
ON CONFLICT (account_id) DO NOTHING;
ALTER TABLE gateway_wallet_transactions
ADD CONSTRAINT gateway_wallet_transactions_account_restrict_v2
FOREIGN KEY (account_id) REFERENCES gateway_wallet_accounts(id) ON DELETE RESTRICT NOT VALID;
ADD CONSTRAINT gateway_wallet_transactions_audit_guard_v2
FOREIGN KEY (account_id) REFERENCES gateway_wallet_account_audit_guards(account_id) ON DELETE RESTRICT NOT VALID;
UPDATE gateway_tasks task
SET billing_status = 'not_required',