diff --git a/backend/scripts/backfill_name_pinyin_abbr.py b/backend/scripts/backfill_name_pinyin_abbr.py new file mode 100644 index 000000000..36e06ec43 --- /dev/null +++ b/backend/scripts/backfill_name_pinyin_abbr.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""回填 crm_product / crm_customer 的 name_pinyin_abbr(仅空字段)。 + +用法: + cd backend + venv/bin/python scripts/backfill_name_pinyin_abbr.py --dry-run + venv/bin/python scripts/backfill_name_pinyin_abbr.py + venv/bin/python scripts/backfill_name_pinyin_abbr.py --env-file .env.production +""" +from __future__ import annotations + +import argparse +import os +import sys +from pathlib import Path +from typing import Optional + +BACKEND_ROOT = Path(__file__).resolve().parents[1] +if str(BACKEND_ROOT) not in sys.path: + sys.path.insert(0, str(BACKEND_ROOT)) + + +def load_settings_env(env_file: Optional[str]) -> None: + if env_file: + os.environ["ENV_FILE"] = env_file + from dotenv import load_dotenv + + candidates = [] + if env_file: + p = Path(env_file) + candidates.append(p if p.is_absolute() else BACKEND_ROOT / p) + candidates.extend( + [ + BACKEND_ROOT / ".env.local", + BACKEND_ROOT / ".env", + BACKEND_ROOT / ".env.production", + ] + ) + for path in candidates: + if path.is_file(): + load_dotenv(path, override=True) + break + + +def _empty_pinyin_filter(model): + from sqlalchemy import or_ + + return or_(model.name_pinyin_abbr.is_(None), model.name_pinyin_abbr == "") + + +def backfill_model(db, model, name_attr: str, *, dry_run: bool, batch_size: int) -> int: + from app.utils.pinyin_abbr import build_name_pinyin_abbr + + q = db.query(model).filter(model.del_flag == 0).filter(_empty_pinyin_filter(model)) + total = q.count() + print(f"{model.__tablename__}: empty={total}") + if total == 0: + return 0 + + updated = 0 + dry_printed = 0 + while True: + rows = q.limit(batch_size).all() + if not rows: + break + for row in rows: + try: + name = getattr(row, name_attr) or "" + abbr = build_name_pinyin_abbr(name) + if not abbr: + continue + if dry_run: + if dry_printed < 20: + print(f" would set {row.id}: {abbr}") + dry_printed += 1 + updated += 1 + else: + row.name_pinyin_abbr = abbr + updated += 1 + except Exception as exc: # noqa: BLE001 — batch continue + print(f" skip {getattr(row, 'id', '?')}: {exc}") + if dry_run: + if updated > dry_printed: + print(f" ... and {updated - dry_printed} more") + break + db.commit() + # filled rows leave the empty filter; no offset needed + print(f"{model.__tablename__}: updated={updated}") + return updated + + +def main() -> int: + parser = argparse.ArgumentParser(description="Backfill CRM name_pinyin_abbr (empty only)") + parser.add_argument("--dry-run", action="store_true", help="只统计/预览,不写库") + parser.add_argument("--env-file", default=None, help="指定 dotenv 文件") + parser.add_argument("--batch-size", type=int, default=200) + args = parser.parse_args() + + load_settings_env(args.env_file) + + from app.core.database import SessionLocal + from app.models.crm_models import CrmCustomer, CrmProduct + + db = SessionLocal() + try: + backfill_model( + db, CrmProduct, "product_name", dry_run=args.dry_run, batch_size=args.batch_size + ) + backfill_model( + db, CrmCustomer, "customer_name", dry_run=args.dry_run, batch_size=args.batch_size + ) + finally: + db.close() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())