"""kpi_causality 因果链验证机制迁移脚本 (2026-08-27 P2) 加列: - source_type: varchar(20) 建链来源 AI_suggested/manual/imported - verify_status: varchar(20) 验证状态 pending/data_verified/human_verified/disputed - verified_at: datetime 验证时间 - verified_by: varchar(50) 验证人/AI/脚本 - entity_id: int 多租户隔离 (2026-08-27 收官补齐) 幂等: 列已存在则跳过; entity_id 回填只更新 NULL/0 行。 用法: python scripts/migrate_causality_verification.py """ import logging import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from sqlalchemy import text from app.database import get_engine logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s") logger = logging.getLogger("migrate-causality-verification") COLUMNS = [ ("source_type", "ALTER TABLE kpi_causality ADD COLUMN source_type VARCHAR(20) NOT NULL DEFAULT 'manual' COMMENT '建链来源 AI_suggested/manual/imported'"), ("verify_status", "ALTER TABLE kpi_causality ADD COLUMN verify_status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '验证状态 pending/data_verified/human_verified/disputed'"), ("verified_at", "ALTER TABLE kpi_causality ADD COLUMN verified_at DATETIME NULL COMMENT '验证时间'"), ("verified_by", "ALTER TABLE kpi_causality ADD COLUMN verified_by VARCHAR(50) NULL COMMENT '验证人/AI/脚本'"), ("entity_id", "ALTER TABLE kpi_causality ADD COLUMN entity_id INT NOT NULL DEFAULT 1 COMMENT '企业ID (多租户隔离 2026-08-27)'"), ] def run(): engine = get_engine() with engine.connect() as conn: # 1. 检查表是否存在 exists = conn.execute(text( "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'kpi_causality'" )).scalar() if not exists: logger.error("kpi_causality 表不存在,跳过") return 1 # 2. 现有列 existing = {r[0] for r in conn.execute(text( "SELECT column_name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'kpi_causality'" ))} logger.info("现有列: %s", sorted(existing)) # 3. 加列(幂等) for col, ddl in COLUMNS: if col in existing: logger.info("列 %s 已存在,跳过", col) else: conn.execute(text(ddl)) logger.info("已添加列 %s", col) # 4. 回填 entity_id(无条件从 source KPI 对齐,纠正默认值偏差) # 仅当来源KPI存在才回填;无来源KPI的孤儿链保持原值 conn.execute(text( "UPDATE kpi_causality c JOIN kpi_definitions k ON k.id = c.source_kpi_id " "SET c.entity_id = k.entity_id" )) orphan = conn.execute(text( "SELECT COUNT(*) FROM kpi_causality c LEFT JOIN kpi_definitions k ON k.id = c.source_kpi_id " "WHERE k.id IS NULL" )).scalar() if orphan: logger.warning("%d 条因果链无来源KPI(孤儿链)", orphan) else: logger.info("entity_id 已全部按来源KPI回填") # 5. 验证 cols = {r[0] for r in conn.execute(text( "SELECT column_name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'kpi_causality'" ))} missing = {c for c, _ in COLUMNS} - cols if missing: logger.error("仍有缺失列: %s", missing) return 1 row = conn.execute(text( "SELECT COUNT(*) FROM kpi_causality WHERE entity_id IS NULL OR entity_id = 0" )).scalar() if row: logger.error("仍有 %d 行 entity_id 为空", row) return 1 total = conn.execute(text("SELECT COUNT(*) FROM kpi_causality")).scalar() logger.info("迁移完成: kpi_causality %d 条, 新列: source_type/verify_status/verified_at/verified_by/entity_id", total) return 0 if __name__ == "__main__": sys.exit(run())