159 lines
7.8 KiB
Python
159 lines
7.8 KiB
Python
"""把育种各枚举列的历史「中文 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()
|