-- 113 迁移前置兼容修复。 -- -- 113 已经进入生产,文件必须保持字节不变。旧快照库中存在两类历史脏数据, -- 会让原版 113 在“加新约束/唯一索引”时失败: -- 1. sales_daily_facts 仍有旧约束不接受的渠道值; -- 2. cost_exceptions 同一范围的旧行会生成重复 scope_key。 -- -- 本文件不是迁移,不写 schema_migrations。它只做幂等兼容准备: -- - 113 已执行时立即跳过; -- - 清理旧库数据,让随后执行的原版 113 可以通过。 -- -- 由 deploy/ubuntu-deploy.sh 在数据库迁移前自动执行。 DO $preflight$ DECLARE v_has_113 BOOLEAN := FALSE; v_sales_has_mode BOOLEAN := FALSE; v_scope_duplicates BIGINT := 0; v_scope_missing BIGINT := 0; BEGIN IF to_regclass('public.schema_migrations') IS NOT NULL THEN EXECUTE $query$ SELECT EXISTS ( SELECT 1 FROM public.schema_migrations migration WHERE to_jsonb(migration)->>'filename' = '113_finance_channel_cutover.sql' OR to_jsonb(migration)->>'name' = '113_finance_channel_cutover' ) $query$ INTO v_has_113; END IF; IF v_has_113 THEN RAISE NOTICE '113 已执行,跳过前置兼容修复'; RETURN; END IF; IF to_regclass('public.sales_daily_facts') IS NOT NULL THEN SELECT EXISTS ( SELECT 1 FROM pg_attribute WHERE attrelid = 'public.sales_daily_facts'::regclass AND attname = 'fulfillment_mode' AND NOT attisdropped ) INTO v_sales_has_mode; IF v_sales_has_mode THEN EXECUTE $sql$ ALTER TABLE public.sales_daily_facts DROP CONSTRAINT IF EXISTS chk_sales_daily_facts_fulfillment_mode $sql$; EXECUTE $sql$ UPDATE public.sales_daily_facts SET fulfillment_mode = CASE WHEN fulfillment_mode = 'legacy_platform' THEN 'archived' ELSE 'unclassified' END WHERE fulfillment_mode NOT IN ( 'work_order', 'affiliate_dash', 'third_party_voucher', 'manual', 'unclassified', 'archived' ) $sql$; -- 迁移前临时使用新旧并集的约束;原版 113 会立即替换成最终约束。 EXECUTE $sql$ ALTER TABLE public.sales_daily_facts ADD CONSTRAINT chk_sales_daily_facts_fulfillment_mode CHECK ( fulfillment_mode IN ( 'work_order', 'affiliate_dash', 'third_party_voucher', 'manual', 'unclassified', 'archived' ) ) $sql$; RAISE NOTICE 'sales_daily_facts 历史渠道值已兼容归一'; END IF; END IF; IF to_regclass('public.cost_exceptions') IS NOT NULL THEN EXECUTE $sql$ ALTER TABLE public.cost_exceptions ADD COLUMN IF NOT EXISTS scope_key TEXT, ADD COLUMN IF NOT EXISTS item_count INTEGER NOT NULL DEFAULT 1, ADD COLUMN IF NOT EXISTS first_seen_at TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS last_seen_at TIMESTAMPTZ $sql$; EXECUTE $sql$ WITH duplicate_scope_keys AS ( SELECT scope_key FROM public.cost_exceptions WHERE status <> 'resolved' AND NULLIF(BTRIM(scope_key), '') IS NOT NULL GROUP BY scope_key HAVING COUNT(*) > 1 ) UPDATE public.cost_exceptions ce SET scope_key = COALESCE(NULLIF(BTRIM(ce.reason), ''), 'legacy') || '|' || COALESCE(ce.order_item_id::text, ce.subject_id::text, ce.id::text) || '|legacy:' || ce.id, first_seen_at = COALESCE(ce.first_seen_at, ce.created_at), last_seen_at = COALESCE(ce.last_seen_at, ce.created_at) WHERE ce.scope_key IS NULL OR BTRIM(ce.scope_key) = '' OR ce.first_seen_at IS NULL OR ce.last_seen_at IS NULL OR ( ce.status <> 'resolved' AND ce.scope_key IN (SELECT scope_key FROM duplicate_scope_keys) ) $sql$; EXECUTE $sql$ SELECT COUNT(*) FROM ( SELECT scope_key FROM public.cost_exceptions WHERE status <> 'resolved' AND NULLIF(BTRIM(scope_key), '') IS NOT NULL GROUP BY scope_key HAVING COUNT(*) > 1 ) duplicate_scope_keys $sql$ INTO v_scope_duplicates; EXECUTE $sql$ SELECT COUNT(*) FROM public.cost_exceptions WHERE scope_key IS NULL OR BTRIM(scope_key) = '' $sql$ INTO v_scope_missing; IF v_scope_duplicates > 0 THEN RAISE EXCEPTION 'cost_exceptions 仍有 % 组重复 scope_key,113 无法创建唯一索引', v_scope_duplicates; END IF; IF v_scope_missing > 0 THEN RAISE EXCEPTION 'cost_exceptions 仍有 % 行 scope_key 为空,113 的唯一索引前置数据不完整', v_scope_missing; END IF; RAISE NOTICE 'cost_exceptions 历史 scope_key 已保证唯一'; END IF; END $preflight$;