#!/usr/bin/env python3 # -*- coding: utf-8 -*- """存量阈值数据修正(project-20260911-threshold-fix ⑤) 修正前: 1) 备份 kpi_definitions 全表 → backend/scripts/backups/kpi_definitions.bak-20260911.sql(mysqldump) 2) 记录 3 行的原值 → backups/threshold_rows_before_20260911.json(可回滚) 修正内容(7 个不可解析表达式 → 0): id=32 F_EVA : Y='>=-100000' R='<-100000' → 解析器支持负数后自然可解析,无需改值(仅验证) id=46 C_REBATE_RATE: G='52.33~107.67'(保留原绿区间) Y='偏离55%' → '<52.33|>107.67'(绿区间外即黄) R='偏离>55%' → '<=36|>=124'(80±27.67×2 ≈ 原"55"口径) id=327 BH_SALES_10 : G=''(补) → '>=200' Y='偏离182878%'→ '>=160' R='偏离>182878%'→ '<120' 口径对齐同族 BH_SALES_1..9:G>=target / Y>=0.8target / R<0.6target 用法: python3 scripts/fix_threshold_data_20260911.py # 预演(只打印) python3 scripts/fix_threshold_data_20260911.py --apply # 备份 + 落库 """ import os import sys import json import subprocess from datetime import datetime BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, BASE) BACKUP_DIR = os.path.join(BASE, "scripts", "backups") envf = os.path.join(BASE, ".env") ENV = {} if os.path.exists(envf): for line in open(envf, encoding="utf-8"): line = line.strip() if line and not line.startswith("#") and "=" in line: k, v = line.split("=", 1) ENV[k.strip()] = v.strip().strip('"').strip("'") os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'")) from app.database import get_session_local from app.models import KPIDefinition from app.utils.threshold_parser import is_valid_threshold # 目标行号 → 新值(None 表示不改动) FIXES = { 46: {"threshold_green": "52.33~107.67", "threshold_yellow": "<52.33|>107.67", "threshold_red": "<=36|>=124"}, 327: {"threshold_green": ">=200", "threshold_yellow": ">=160", "threshold_red": "<120"}, } # 仅验证不修改 VERIFY_ONLY = [32] FIELDS = ("threshold_green", "threshold_yellow", "threshold_red") def scan(db): """返回 (总表达式数, 不可解析明细)""" rows = db.query(KPIDefinition).all() total = 0 bad = [] for k in rows: for f in FIELDS: e = getattr(k, f) if e is None or not str(e).strip(): continue total += 1 if not is_valid_threshold(e): bad.append((k.id, k.kpi_code, f, str(e))) return total, bad def dump_table_backup(): os.makedirs(BACKUP_DIR, exist_ok=True) out = os.path.join(BACKUP_DIR, "kpi_definitions.bak-20260911.sql") cmd = ["mysqldump", f"-u{ENV.get('CMA_DB_USER', 'cma_user')}", f"-p{ENV.get('CMA_DB_PASS', '')}", f"-h{ENV.get('CMA_DB_HOST', '127.0.0.1')}", f"-P{ENV.get('CMA_DB_PORT', '3306')}", ENV.get("CMA_DB_NAME", "cma"), "kpi_definitions"] with open(out, "w", encoding="utf-8") as fh: p = subprocess.run(cmd, stdout=fh, stderr=subprocess.PIPE) if p.returncode != 0: raise SystemExit("mysqldump 失败: " + p.stderr.decode("utf-8", "ignore")[:400]) return out def main(): apply = "--apply" in sys.argv db = get_session_local()() total_before, bad_before = scan(db) print(f"[before] 非空阈值表达式 {total_before} 条, 不可解析 {len(bad_before)} 条") for b in bad_before: print(" BAD", b) # 记录原值(备份 json) rows_before = [] for tid in list(FIXES.keys()) + VERIFY_ONLY: k = db.query(KPIDefinition).filter(KPIDefinition.id == tid).first() if k: rows_before.append({"id": k.id, "kpi_code": k.kpi_code, "kpi_name": k.kpi_name, "target_value": k.target_value, **{f: getattr(k, f) for f in FIELDS}}) os.makedirs(BACKUP_DIR, exist_ok=True) jpath = os.path.join(BACKUP_DIR, "threshold_rows_before_20260911.json") if apply: with open(jpath, "w", encoding="utf-8") as fh: json.dump({"created_at": datetime.now().isoformat(), "rows": rows_before}, fh, ensure_ascii=False, indent=2) sql_path = dump_table_backup() print(f"[backup] json={jpath}") print(f"[backup] sql={sql_path}") else: print(f"[dry-run] 将写入备份 json={jpath}(未执行)") for r in rows_before: print(" before:", r) # 应用修正 for tid, newvals in FIXES.items(): k = db.query(KPIDefinition).filter(KPIDefinition.id == tid).first() if not k: print(f" !! id={tid} 不存在,跳过") continue for f, v in newvals.items(): old = getattr(k, f) print(f" id={tid} {f}: {old!r} -> {v!r}") if apply: setattr(k, f, v) if apply: db.commit() # 回查 for tid in list(FIXES.keys()) + VERIFY_ONLY: k = db.query(KPIDefinition).filter(KPIDefinition.id == tid).first() if k: vals = {f: getattr(k, f) for f in FIELDS} ok = all(is_valid_threshold(v) for v in vals.values()) print(f"[after] id={tid} {k.kpi_code} {vals} parseable={ok}") total_after, bad_after = scan(db) print(f"[after] 非空阈值表达式 {total_after} 条, 不可解析 {len(bad_after)} 条") for b in bad_after: print(" BAD", b) db.close() if not apply: print("※ 预演模式,未落库。加 --apply 执行。") if __name__ == "__main__": main()