Prechádzať zdrojové kódy

chore(db): add optional script to split brand from product names

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 mesiacov pred
rodič
commit
dd31fb53b6

+ 165 - 0
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<base>.+?)[((](?P<brand>[^))]+)[))]\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()