backfill_name_pinyin_abbr.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. #!/usr/bin/env python3
  2. """回填 crm_product / crm_customer 的 name_pinyin_abbr(仅空字段)。
  3. 用法:
  4. cd backend
  5. venv/bin/python scripts/backfill_name_pinyin_abbr.py --dry-run
  6. venv/bin/python scripts/backfill_name_pinyin_abbr.py
  7. venv/bin/python scripts/backfill_name_pinyin_abbr.py --env-file .env.production
  8. """
  9. from __future__ import annotations
  10. import argparse
  11. import os
  12. import sys
  13. from pathlib import Path
  14. from typing import Optional
  15. BACKEND_ROOT = Path(__file__).resolve().parents[1]
  16. if str(BACKEND_ROOT) not in sys.path:
  17. sys.path.insert(0, str(BACKEND_ROOT))
  18. def load_settings_env(env_file: Optional[str]) -> None:
  19. if env_file:
  20. os.environ["ENV_FILE"] = env_file
  21. from dotenv import load_dotenv
  22. candidates = []
  23. if env_file:
  24. p = Path(env_file)
  25. candidates.append(p if p.is_absolute() else BACKEND_ROOT / p)
  26. candidates.extend(
  27. [
  28. BACKEND_ROOT / ".env.local",
  29. BACKEND_ROOT / ".env",
  30. BACKEND_ROOT / ".env.production",
  31. ]
  32. )
  33. for path in candidates:
  34. if path.is_file():
  35. load_dotenv(path, override=True)
  36. break
  37. def _empty_pinyin_filter(model):
  38. from sqlalchemy import or_
  39. return or_(model.name_pinyin_abbr.is_(None), model.name_pinyin_abbr == "")
  40. def backfill_model(db, model, name_attr: str, *, dry_run: bool, batch_size: int) -> int:
  41. from app.utils.pinyin_abbr import build_name_pinyin_abbr
  42. q = db.query(model).filter(model.del_flag == 0).filter(_empty_pinyin_filter(model))
  43. total = q.count()
  44. print(f"{model.__tablename__}: empty={total}")
  45. if total == 0:
  46. return 0
  47. updated = 0
  48. dry_printed = 0
  49. while True:
  50. rows = q.limit(batch_size).all()
  51. if not rows:
  52. break
  53. for row in rows:
  54. try:
  55. name = getattr(row, name_attr) or ""
  56. abbr = build_name_pinyin_abbr(name)
  57. if not abbr:
  58. continue
  59. if dry_run:
  60. if dry_printed < 20:
  61. print(f" would set {row.id}: {abbr}")
  62. dry_printed += 1
  63. updated += 1
  64. else:
  65. row.name_pinyin_abbr = abbr
  66. updated += 1
  67. except Exception as exc: # noqa: BLE001 — batch continue
  68. print(f" skip {getattr(row, 'id', '?')}: {exc}")
  69. if dry_run:
  70. if updated > dry_printed:
  71. print(f" ... and {updated - dry_printed} more")
  72. break
  73. db.commit()
  74. # filled rows leave the empty filter; no offset needed
  75. print(f"{model.__tablename__}: updated={updated}")
  76. return updated
  77. def main() -> int:
  78. parser = argparse.ArgumentParser(description="Backfill CRM name_pinyin_abbr (empty only)")
  79. parser.add_argument("--dry-run", action="store_true", help="只统计/预览,不写库")
  80. parser.add_argument("--env-file", default=None, help="指定 dotenv 文件")
  81. parser.add_argument("--batch-size", type=int, default=200)
  82. args = parser.parse_args()
  83. load_settings_env(args.env_file)
  84. from app.core.database import SessionLocal
  85. from app.models.crm_models import CrmCustomer, CrmProduct
  86. db = SessionLocal()
  87. try:
  88. backfill_model(
  89. db, CrmProduct, "product_name", dry_run=args.dry_run, batch_size=args.batch_size
  90. )
  91. backfill_model(
  92. db, CrmCustomer, "customer_name", dry_run=args.dry_run, batch_size=args.batch_size
  93. )
  94. finally:
  95. db.close()
  96. return 0
  97. if __name__ == "__main__":
  98. raise SystemExit(main())