From dd31fb53b66de077af25648830d3275a07f8d67d Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Tue, 28 Jul 2026 09:36:32 +0800 Subject: [PATCH] chore(db): add optional script to split brand from product names Co-authored-by: Cursor --- ..._20260727_split_brand_from_product_name.py | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 backend/scripts/migrate_20260727_split_brand_from_product_name.py diff --git a/backend/scripts/migrate_20260727_split_brand_from_product_name.py b/backend/scripts/migrate_20260727_split_brand_from_product_name.py new file mode 100644 index 000000000..2fd722cdb --- /dev/null +++ b/backend/scripts/migrate_20260727_split_brand_from_product_name.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""可选:从产品名称括号段拆出品牌。 + +示例:「片碱(君正)」→ product_name=片碱,crm_product_brand.brand_name=君正。 + +默认 dry-run:导出匹配清单 CSV,不改库。 +业务确认后加 --apply 才执行更新。 + +不修改历史报价/合同/计划行上的 product_name 快照。 + +用法: + cd backend && ./venv/bin/python scripts/migrate_20260727_split_brand_from_product_name.py + cd backend && ./venv/bin/python scripts/migrate_20260727_split_brand_from_product_name.py --csv /tmp/brands.csv + cd backend && ./venv/bin/python scripts/migrate_20260727_split_brand_from_product_name.py --apply +""" +from __future__ import annotations + +import argparse +import csv +import re +import sys +from pathlib import Path + +# 允许直接 python scripts/... 运行 +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from app.core.database import SessionLocal +from app.core.utils import generate_uuid +from app.models.crm_models import CrmProduct, CrmProductBrand + +# 全角/半角括号均可:片碱(君正) / 片碱(君正) +NAME_WITH_BRAND_RE = re.compile( + r"^(?P.+?)[((](?P[^))]+)[))]\s*$" +) + + +def parse_name(name: str): + text = (name or "").strip() + m = NAME_WITH_BRAND_RE.match(text) + if not m: + return None + base = m.group("base").strip() + brand = m.group("brand").strip() + if not base or not brand: + return None + return base, brand + + +def collect_candidates(db): + rows = ( + db.query(CrmProduct) + .filter(CrmProduct.del_flag == 0) + .order_by(CrmProduct.create_time.desc()) + .all() + ) + out = [] + for row in rows: + parsed = parse_name(row.product_name or "") + if not parsed: + continue + base, brand = parsed + out.append( + { + "id": row.id, + "tenant_no": row.tenant_no, + "product_no": row.product_no or "", + "old_name": row.product_name, + "new_name": base, + "brand_name": brand, + } + ) + return out + + +def write_csv(path: Path, rows: list[dict]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8-sig", newline="") as f: + writer = csv.DictWriter( + f, + fieldnames=["id", "tenant_no", "product_no", "old_name", "new_name", "brand_name"], + ) + writer.writeheader() + writer.writerows(rows) + + +def apply_updates(db, rows: list[dict]) -> int: + changed = 0 + for item in rows: + product = ( + db.query(CrmProduct) + .filter( + CrmProduct.id == item["id"], + CrmProduct.del_flag == 0, + ) + .first() + ) + if not product: + continue + if (product.product_name or "").strip() != item["old_name"]: + continue + exists = ( + db.query(CrmProductBrand) + .filter( + CrmProductBrand.product_id == product.id, + CrmProductBrand.tenant_no == product.tenant_no, + CrmProductBrand.brand_name == item["brand_name"], + CrmProductBrand.del_flag == 0, + ) + .first() + ) + if not exists: + db.add( + CrmProductBrand( + id=generate_uuid(), + product_id=product.id, + brand_name=item["brand_name"], + status="on", + tenant_no=product.tenant_no, + order_num=0, + create_by=product.update_by or product.create_by, + create_dept_id=product.create_dept_id or "", + update_by=product.update_by or product.create_by, + ) + ) + product.product_name = item["new_name"] + changed += 1 + db.commit() + return changed + + +def main(): + parser = argparse.ArgumentParser(description="从产品名括号段拆品牌(需业务确认后 --apply)") + parser.add_argument( + "--csv", + default="scripts/output/product_brand_split_candidates.csv", + help="导出候选清单路径", + ) + parser.add_argument( + "--apply", + action="store_true", + help="确认后执行:改 product_name 并插入 crm_product_brand", + ) + args = parser.parse_args() + + db = SessionLocal() + try: + rows = collect_candidates(db) + csv_path = Path(args.csv) + if not csv_path.is_absolute(): + csv_path = ROOT / csv_path + write_csv(csv_path, rows) + print(f"匹配 {len(rows)} 条,已导出: {csv_path}") + if not args.apply: + print("dry-run 完成。业务确认后加 --apply 再执行。") + return + changed = apply_updates(db, rows) + print(f"已更新 {changed} 条产品名称,并补品牌子表(已存在同名品牌则跳过插入)。") + finally: + db.close() + + +if __name__ == "__main__": + main()