Files
dpb/backend/scripts/migrate_dict_english.py
T
34047007@qq.com b95053c52c init: 初始化 dpb 桃育种系统代码库
前后端 + 后端 FastAPI 全量源码、部署脚本与文档。
2026-08-06 00:17:49 +08:00

159 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""把育种各枚举列的历史「中文 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_treatmentis_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=入选(对照 dictalive/eliminated/selected
("bre_tree", "status", "1"): "alive",
# bre_germplasm 历史自由文本:'还好'≈肉质中(对照 dictmid);'3月下旬'≈早花(对照 dictearly
("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()