#!/usr/bin/env python3 """回填当事人父亲/子女/缺失母亲客户 ID。可重复执行。 用法(在 backend 目录、已配置 .env): python -m scripts.backfill_party_customer_ids python -m scripts.backfill_party_customer_ids --dry-run """ from __future__ import annotations import argparse import os import sys from datetime import datetime from typing import Any, Dict, Optional, Tuple sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app.core.database import SessionLocal from app.crud import collection_protocol as cp_svc from app.models.models import BizProtocolPartyCustomer from app.models.research_donation import BizResearchDonationParty from app.services.party_customer_sync import sync_party_customers def _party_to_sync_dict(party: Any) -> Dict[str, Any]: return { "customer_id": (party.customer_id or "").strip(), "father_customer_id": (getattr(party, "father_customer_id", None) or "").strip(), "newborn_customer_id": (getattr(party, "newborn_customer_id", None) or "").strip(), "mother_name": (party.mother_name or "").strip(), "mother_phone": (party.mother_phone or "").strip(), "mother_id_card": (party.mother_id_card or "").strip(), "father_name": (party.father_name or "").strip(), "father_phone": (party.father_phone or "").strip(), "father_id_card": (party.father_id_card or "").strip(), "newborn_name": (party.newborn_name or "").strip(), "newborn_id_card": (party.newborn_id_card or "").strip(), "newborn_sex": (party.newborn_sex or "").strip(), } def _ids(party: Any) -> Tuple[str, str, str]: return ( (party.customer_id or "").strip(), (getattr(party, "father_customer_id", None) or "").strip(), (getattr(party, "newborn_customer_id", None) or "").strip(), ) def _needs_sync(party: Any) -> bool: mother_name = (party.mother_name or "").strip() father_name = (party.father_name or "").strip() newborn_name = (party.newborn_name or "").strip() cid, fid, nid = _ids(party) if mother_name and not cid: return True if father_name and not fid: return True if newborn_name and not nid: return True # 有姓名也允许再跑 sync,以便补证撞库;但默认只回填缺 ID 的,保持幂等少噪音 return False def _apply_ids(party: Any, synced: Dict[str, Any], user_id: Optional[str]) -> bool: before = _ids(party) after = ( (synced.get("customer_id") or "").strip(), (synced.get("father_customer_id") or "").strip(), (synced.get("newborn_customer_id") or "").strip(), ) if before == after: return False party.customer_id = after[0] or None party.father_customer_id = after[1] or None party.newborn_customer_id = after[2] or None party.update_by = user_id party.update_time = datetime.now() return True def backfill_storage(db, *, dry_run: bool, user_id: str) -> Tuple[int, int]: updated = 0 skipped = 0 rows = ( db.query(BizProtocolPartyCustomer) .filter(BizProtocolPartyCustomer.del_flag == 0) .order_by(BizProtocolPartyCustomer.create_time.asc()) .all() ) for party in rows: if not _needs_sync(party): skipped += 1 continue synced = sync_party_customers( db, _party_to_sync_dict(party), tenant_no=party.tenant_no, user_id=user_id, ) before = _ids(party) after = ( (synced.get("customer_id") or "").strip(), (synced.get("father_customer_id") or "").strip(), (synced.get("newborn_customer_id") or "").strip(), ) if before == after: skipped += 1 continue if dry_run: updated += 1 print(f"[dry-run] storage party {party.id} {before} -> {after}") continue cp_svc.upsert_party_customer( db, party.collection_protocol_id, party.tenant_no, user_id, **synced, ) updated += 1 return updated, skipped def backfill_donation(db, *, dry_run: bool, user_id: str) -> Tuple[int, int]: updated = 0 skipped = 0 rows = ( db.query(BizResearchDonationParty) .filter(BizResearchDonationParty.del_flag == 0) .order_by(BizResearchDonationParty.create_time.asc()) .all() ) for party in rows: if not _needs_sync(party): skipped += 1 continue synced = sync_party_customers( db, _party_to_sync_dict(party), tenant_no=party.tenant_no, user_id=user_id, ) if dry_run: before = _ids(party) after = ( (synced.get("customer_id") or "").strip(), (synced.get("father_customer_id") or "").strip(), (synced.get("newborn_customer_id") or "").strip(), ) if before != after: updated += 1 print( f"[dry-run] donation party {party.id} " f"{before} -> {after}" ) else: skipped += 1 continue if _apply_ids(party, synced, user_id): db.add(party) db.commit() updated += 1 else: skipped += 1 return updated, skipped def main() -> None: parser = argparse.ArgumentParser(description="回填当事人三角色客户 ID") parser.add_argument("--dry-run", action="store_true", help="只打印变更,不写库") parser.add_argument("--user-id", default="backfill", help="写入 update_by / create_by") args = parser.parse_args() db = SessionLocal() try: s_upd, s_skip = backfill_storage(db, dry_run=args.dry_run, user_id=args.user_id) d_upd, d_skip = backfill_donation(db, dry_run=args.dry_run, user_id=args.user_id) print( f"storage: updated={s_upd} skipped={s_skip}; " f"donation: updated={d_upd} skipped={d_skip}" ) if args.dry_run: print("dry-run, no commit") else: print("done") finally: db.close() if __name__ == "__main__": main()