"""把育种各枚举列的历史「中文 dict_label」迁移为「英码 dict_value」,幂等,迁移前自动备份。 背景:方案 B 下 dict_value 存英码、dict_label 存中文,页面/导入最终都以英码落库。 但历史数据(表单直存中文、早期种子数据)里仍是中文 label,与本设计不一致。 本脚本以 sys_dict_data 为唯一权威(label→value),把每个枚举列里「恰好等于某 dict_label 的值」翻译为对应 dict_value;对「既非 label 也非 value」的脏值,登记到 UNMATCHED_FIXES (人工确认为目标英码)后一并转换,其余无法识别的值原样保留并明确上报,供人工处理。 用法(backend/ 目录): ENVIRONMENT=dev "C:/ai/miniconda3/envs/dpb/python.exe" scripts/migrate_dict_english.py --check # 只读预览 ENVIRONMENT=dev "C:/ai/miniconda3/envs/dpb/python.exe" scripts/migrate_dict_english.py # 执行 幂等性:重复执行时字典 label 已不存在(已被替换为英码),UPDATE 影响 0 行,可安全重跑。 备份:每张受影响表先执行 `CREATE TABLE IF NOT EXISTS bre_backup_<表> AS SELECT * FROM <表>`, 表前缀 bre_backup_ 避免污染业务库命名;已存在则不重复备份。 """ import argparse import os import sys BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if BACKEND_DIR not in sys.path: sys.path.insert(0, BACKEND_DIR) from sqlalchemy import create_engine, text # noqa: E402 from app.config.setting import settings # noqa: E402 # (表, 列, 字典类型) —— 与波次3 映射清单一致(is_preset 值本就是 "1"/"0",无需迁移,故不在列) # 字典类型名已校准对齐线上库:variety_type->peach_variety_type; # treatment_method->seed_treatment;is_selected->identify_status(选育 6 态)。 COLUMNS = [ ("bre_germplasm", "variety_type", "peach_variety_type"), ("bre_germplasm", "maturity_period", "maturity_period"), ("bre_germplasm", "firmness", "firmness"), ("bre_germplasm", "flowering_period", "flowering_period"), ("bre_site", "eco_type", "eco_type"), ("bre_plot", "row_orientation", "row_orientation"), ("bre_tree", "row_orientation", "row_orientation"), ("bre_tree", "status", "tree_status"), ("bre_personnel", "gender", "gender"), ("bre_personnel", "role", "personnel_role"), ("bre_cross_combination", "cross_method", "cross_method"), ("bre_seed_treatment", "treatment_method", "seed_treatment"), ("bre_tree_photo", "photo_type", "photo_type"), ("bre_selection_result", "is_selected", "identify_status"), ] # 既非 dict_label 也非 dict_value 的历史脏值 → 目标英码(人工确认后登记;生产执行前请评审) UNMATCHED_FIXES = { # tree.status:早期数值代码 1=存活/2=淘汰/3=入选(对照 dict:alive/eliminated/selected) ("bre_tree", "status", "1"): "alive", # bre_germplasm 历史自由文本:'还好'≈肉质中(对照 dict:mid);'3月下旬'≈早花(对照 dict:early) ("bre_germplasm", "firmness", "还好"): "mid", ("bre_germplasm", "flowering_period", "3月下旬"): "early", } def main() -> None: parser = argparse.ArgumentParser(description="枚举列中文 label → 英码 value 迁移") parser.add_argument("--check", action="store_true", help="仅预览将执行的 UPDATE 与未识别值,不落库") args = parser.parse_args() engine = create_engine(settings.DB_URI) print(f"# 连接: {settings.DB_URI}") with engine.connect() as conn: # 收集各字典类型 label→value dict_rows = conn.execute( text("SELECT dict_type, dict_label, dict_value FROM sys_dict_data WHERE is_deleted = false") ).fetchall() label2value: dict[str, dict[str, str]] = {} values: dict[str, set[str]] = {} for dt, label, value in dict_rows: label2value.setdefault(dt, {})[label] = value values.setdefault(dt, set()).add(value) print(f"# 字典类型: {len(label2value)} 组") for table, col, dt in COLUMNS: print(f"\n== {table}.{col} <- dict_type={dt}") # 备份(仅首次执行时建表) backup_table = f"bre_backup_{table}" if not args.check: try: with engine.begin() as c: c.execute(text(f"CREATE TABLE IF NOT EXISTS {backup_table} AS SELECT * FROM {table}")) except Exception as e: # noqa: BLE001 print(f"# [备份失败] {backup_table}: {e!s}") else: print(f"# 将备份为 {backup_table}") # 当前列取值分布 cur = conn.execute( text( f"SELECT {col} AS v, count(*) AS n FROM {table} " f"WHERE {col} IS NOT NULL AND {col} <> '' GROUP BY {col} ORDER BY n DESC" ) ).fetchall() # 1) label → value 翻译 if dt in label2value: for label, value in label2value[dt].items(): sql = f"UPDATE {table} SET {col} = :value WHERE {col} = :label" if args.check: # 预览:统计将被更新的行数(枚举值迁移与软删无关,不做 is_deleted 过滤) cnt = conn.execute( text(f"SELECT count(*) FROM {table} WHERE {col} = :label"), {"label": label}, ).scalar() if cnt: print(f" UPDATE {label!r} -> {value!r} 影响 {cnt} 行") else: with engine.begin() as c: r = c.execute(text(sql), {"label": label, "value": value}) if r.rowcount: print(f" UPDATE {label!r} -> {value!r} 影响 {r.rowcount} 行") else: print(f"# [注意] 字典类型 {dt} 无数据,跳过 label 翻译") # 2) 人工登记的脏值修复 for (t, c, bad), target in UNMATCHED_FIXES.items(): if t == table and c == col: sql = f"UPDATE {table} SET {col} = :target WHERE {col} = :bad" if args.check: cnt = conn.execute( text(f"SELECT count(*) FROM {table} WHERE {col} = :bad"), {"bad": bad} ).scalar() if cnt: print(f" 人工修复 {bad!r} -> {target!r} 影响 {cnt} 行") else: with engine.begin() as c: r = c.execute(text(sql), {"bad": bad, "target": target}) if r.rowcount: print(f" 人工修复 {bad!r} -> {target!r} 影响 {r.rowcount} 行") # 3) 迁移后仍未识别值(既非 label 也非 value)→ 上报 remaining = conn.execute( text( f"SELECT {col} AS v, count(*) AS n FROM {table} " f"WHERE {col} IS NOT NULL AND {col} <> '' GROUP BY {col} ORDER BY n DESC" ) ).fetchall() known = set(label2value.get(dt, {})) | values.get(dt, set()) bad_vals = [(v, n) for v, n in remaining if v not in known] if bad_vals: for v, n in bad_vals: print(f" [未识别] {v!r} × {n} —— 既非字典 label 也非英码,请人工处理") else: print(" 列内已无非 label 值") print("\n# 完成。重复执行幂等;备份表为 bre_backup_*。") if args.check: print("# --check 模式:未执行任何变更。") if __name__ == "__main__": main()