backfill_party_customer_ids.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. #!/usr/bin/env python3
  2. """回填当事人父亲/子女/缺失母亲客户 ID。可重复执行。
  3. 用法(在 backend 目录、已配置 .env):
  4. python -m scripts.backfill_party_customer_ids
  5. python -m scripts.backfill_party_customer_ids --dry-run
  6. """
  7. from __future__ import annotations
  8. import argparse
  9. import os
  10. import sys
  11. from datetime import datetime
  12. from typing import Any, Dict, Optional, Tuple
  13. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  14. from app.core.database import SessionLocal
  15. from app.crud import collection_protocol as cp_svc
  16. from app.models.models import BizProtocolPartyCustomer
  17. from app.models.research_donation import BizResearchDonationParty
  18. from app.services.party_customer_sync import sync_party_customers
  19. def _party_to_sync_dict(party: Any) -> Dict[str, Any]:
  20. return {
  21. "customer_id": (party.customer_id or "").strip(),
  22. "father_customer_id": (getattr(party, "father_customer_id", None) or "").strip(),
  23. "newborn_customer_id": (getattr(party, "newborn_customer_id", None) or "").strip(),
  24. "mother_name": (party.mother_name or "").strip(),
  25. "mother_phone": (party.mother_phone or "").strip(),
  26. "mother_id_card": (party.mother_id_card or "").strip(),
  27. "father_name": (party.father_name or "").strip(),
  28. "father_phone": (party.father_phone or "").strip(),
  29. "father_id_card": (party.father_id_card or "").strip(),
  30. "newborn_name": (party.newborn_name or "").strip(),
  31. "newborn_id_card": (party.newborn_id_card or "").strip(),
  32. "newborn_sex": (party.newborn_sex or "").strip(),
  33. }
  34. def _ids(party: Any) -> Tuple[str, str, str]:
  35. return (
  36. (party.customer_id or "").strip(),
  37. (getattr(party, "father_customer_id", None) or "").strip(),
  38. (getattr(party, "newborn_customer_id", None) or "").strip(),
  39. )
  40. def _needs_sync(party: Any) -> bool:
  41. mother_name = (party.mother_name or "").strip()
  42. father_name = (party.father_name or "").strip()
  43. newborn_name = (party.newborn_name or "").strip()
  44. cid, fid, nid = _ids(party)
  45. if mother_name and not cid:
  46. return True
  47. if father_name and not fid:
  48. return True
  49. if newborn_name and not nid:
  50. return True
  51. # 有姓名也允许再跑 sync,以便补证撞库;但默认只回填缺 ID 的,保持幂等少噪音
  52. return False
  53. def _apply_ids(party: Any, synced: Dict[str, Any], user_id: Optional[str]) -> bool:
  54. before = _ids(party)
  55. after = (
  56. (synced.get("customer_id") or "").strip(),
  57. (synced.get("father_customer_id") or "").strip(),
  58. (synced.get("newborn_customer_id") or "").strip(),
  59. )
  60. if before == after:
  61. return False
  62. party.customer_id = after[0] or None
  63. party.father_customer_id = after[1] or None
  64. party.newborn_customer_id = after[2] or None
  65. party.update_by = user_id
  66. party.update_time = datetime.now()
  67. return True
  68. def backfill_storage(db, *, dry_run: bool, user_id: str) -> Tuple[int, int]:
  69. updated = 0
  70. skipped = 0
  71. rows = (
  72. db.query(BizProtocolPartyCustomer)
  73. .filter(BizProtocolPartyCustomer.del_flag == 0)
  74. .order_by(BizProtocolPartyCustomer.create_time.asc())
  75. .all()
  76. )
  77. for party in rows:
  78. if not _needs_sync(party):
  79. skipped += 1
  80. continue
  81. synced = sync_party_customers(
  82. db,
  83. _party_to_sync_dict(party),
  84. tenant_no=party.tenant_no,
  85. user_id=user_id,
  86. )
  87. before = _ids(party)
  88. after = (
  89. (synced.get("customer_id") or "").strip(),
  90. (synced.get("father_customer_id") or "").strip(),
  91. (synced.get("newborn_customer_id") or "").strip(),
  92. )
  93. if before == after:
  94. skipped += 1
  95. continue
  96. if dry_run:
  97. updated += 1
  98. print(f"[dry-run] storage party {party.id} {before} -> {after}")
  99. continue
  100. cp_svc.upsert_party_customer(
  101. db,
  102. party.collection_protocol_id,
  103. party.tenant_no,
  104. user_id,
  105. **synced,
  106. )
  107. updated += 1
  108. return updated, skipped
  109. def backfill_donation(db, *, dry_run: bool, user_id: str) -> Tuple[int, int]:
  110. updated = 0
  111. skipped = 0
  112. rows = (
  113. db.query(BizResearchDonationParty)
  114. .filter(BizResearchDonationParty.del_flag == 0)
  115. .order_by(BizResearchDonationParty.create_time.asc())
  116. .all()
  117. )
  118. for party in rows:
  119. if not _needs_sync(party):
  120. skipped += 1
  121. continue
  122. synced = sync_party_customers(
  123. db,
  124. _party_to_sync_dict(party),
  125. tenant_no=party.tenant_no,
  126. user_id=user_id,
  127. )
  128. if dry_run:
  129. before = _ids(party)
  130. after = (
  131. (synced.get("customer_id") or "").strip(),
  132. (synced.get("father_customer_id") or "").strip(),
  133. (synced.get("newborn_customer_id") or "").strip(),
  134. )
  135. if before != after:
  136. updated += 1
  137. print(
  138. f"[dry-run] donation party {party.id} "
  139. f"{before} -> {after}"
  140. )
  141. else:
  142. skipped += 1
  143. continue
  144. if _apply_ids(party, synced, user_id):
  145. db.add(party)
  146. db.commit()
  147. updated += 1
  148. else:
  149. skipped += 1
  150. return updated, skipped
  151. def main() -> None:
  152. parser = argparse.ArgumentParser(description="回填当事人三角色客户 ID")
  153. parser.add_argument("--dry-run", action="store_true", help="只打印变更,不写库")
  154. parser.add_argument("--user-id", default="backfill", help="写入 update_by / create_by")
  155. args = parser.parse_args()
  156. db = SessionLocal()
  157. try:
  158. s_upd, s_skip = backfill_storage(db, dry_run=args.dry_run, user_id=args.user_id)
  159. d_upd, d_skip = backfill_donation(db, dry_run=args.dry_run, user_id=args.user_id)
  160. print(
  161. f"storage: updated={s_upd} skipped={s_skip}; "
  162. f"donation: updated={d_upd} skipped={d_skip}"
  163. )
  164. if args.dry_run:
  165. print("dry-run, no commit")
  166. else:
  167. print("done")
  168. finally:
  169. db.close()
  170. if __name__ == "__main__":
  171. main()