party_customer_sync.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. """协议当事人三角色客户同步:新建、补证、撞库合并改挂。"""
  2. from __future__ import annotations
  3. from datetime import date
  4. from types import SimpleNamespace
  5. from typing import Any, Dict, Optional
  6. from sqlalchemy.orm import Session
  7. from app.crud import customer as customer_crud
  8. from app.models.models import BizCustomer, BizProtocolPartyCustomer
  9. from app.models.research_donation import BizResearchDonationParty
  10. ROLE_MOTHER = "mother"
  11. ROLE_FATHER = "father"
  12. ROLE_NEWBORN = "newborn"
  13. _ROLE_SPECS = (
  14. {
  15. "role": ROLE_MOTHER,
  16. "out_key": "customer_id",
  17. "id_keys": ("customer_id", "customerId"),
  18. "name_keys": ("mother_name", "motherName"),
  19. "phone_keys": ("mother_phone", "motherPhone"),
  20. "id_card_keys": ("mother_id_card", "motherIdCard"),
  21. "birth_ym_keys": ("mother_birth_ym", "motherBirthYm"),
  22. "ethnic_keys": ("mother_ethnic", "motherEthnic"),
  23. "email_keys": ("mother_email", "motherEmail"),
  24. "address_keys": ("mother_address", "motherAddress"),
  25. "sex_keys": (),
  26. "default_sex": "1",
  27. },
  28. {
  29. "role": ROLE_FATHER,
  30. "out_key": "father_customer_id",
  31. "id_keys": ("father_customer_id", "fatherCustomerId"),
  32. "name_keys": ("father_name", "fatherName"),
  33. "phone_keys": ("father_phone", "fatherPhone"),
  34. "id_card_keys": ("father_id_card", "fatherIdCard"),
  35. "birth_ym_keys": (),
  36. "ethnic_keys": (),
  37. "email_keys": (),
  38. "address_keys": (),
  39. "sex_keys": (),
  40. "default_sex": "0",
  41. },
  42. {
  43. "role": ROLE_NEWBORN,
  44. "out_key": "newborn_customer_id",
  45. "id_keys": ("newborn_customer_id", "newbornCustomerId"),
  46. "name_keys": ("newborn_name", "newbornName"),
  47. "phone_keys": ("newborn_phone", "newbornPhone"),
  48. "id_card_keys": ("newborn_id_card", "newbornIdCard"),
  49. "birth_ym_keys": (),
  50. "ethnic_keys": (),
  51. "email_keys": (),
  52. "address_keys": (),
  53. "sex_keys": ("newborn_sex", "newbornSex"),
  54. "default_sex": "2",
  55. },
  56. )
  57. _PROFILE_ATTRS = (
  58. "customer_name",
  59. "phone_no",
  60. "id_card_no",
  61. "sex",
  62. "age",
  63. "birth_ym",
  64. "ethnic",
  65. "email",
  66. "address",
  67. )
  68. def _is_empty(val: Any) -> bool:
  69. if val is None:
  70. return True
  71. if isinstance(val, str) and not val.strip():
  72. return True
  73. return False
  74. def _pick(party: Dict[str, Any], keys: tuple) -> str:
  75. for key in keys:
  76. if key not in party or party[key] is None:
  77. continue
  78. return str(party[key]).strip()
  79. return ""
  80. def _union_tags(left: Any, right: Any) -> str:
  81. seen = []
  82. for raw in (left, right):
  83. if _is_empty(raw):
  84. continue
  85. for part in str(raw).split(","):
  86. tag = part.strip()
  87. if tag and tag not in seen:
  88. seen.append(tag)
  89. return ",".join(seen)
  90. def _age_from_id_card(id_card: str) -> Optional[int]:
  91. s = (id_card or "").strip()
  92. if len(s) != 18:
  93. return None
  94. try:
  95. birth = date(int(s[6:10]), int(s[10:12]), int(s[12:14]))
  96. except ValueError:
  97. return None
  98. today = date.today()
  99. age = today.year - birth.year - ((today.month, today.day) < (birth.month, birth.day))
  100. if age < 0 or age > 150:
  101. return None
  102. return age
  103. def apply_role_profile_overwrite(target: Any, profile: Any) -> None:
  104. """协议非空字段覆盖客户主档;空串/None 跳过,不碰 tags/remark。"""
  105. for attr in _PROFILE_ATTRS:
  106. val = getattr(profile, attr, None)
  107. if _is_empty(val):
  108. continue
  109. if attr == "sex":
  110. setattr(target, attr, str(val).strip()[:1])
  111. elif attr == "birth_ym":
  112. setattr(target, attr, str(val).strip()[:6])
  113. elif attr == "age":
  114. setattr(target, attr, val)
  115. else:
  116. setattr(target, attr, str(val).strip())
  117. def merge_customer_fields(target: Any, source: Any) -> None:
  118. """目标为空才补;性别在空或未知(2)时补;标签逗号并集;姓名保留目标。"""
  119. if _is_empty(getattr(target, "phone_no", None)):
  120. src_phone = getattr(source, "phone_no", None)
  121. if not _is_empty(src_phone):
  122. target.phone_no = src_phone
  123. if _is_empty(getattr(target, "age", None)):
  124. src_age = getattr(source, "age", None)
  125. if not _is_empty(src_age):
  126. target.age = src_age
  127. target_sex = getattr(target, "sex", None)
  128. if _is_empty(target_sex) or str(target_sex).strip() == "2":
  129. src_sex = getattr(source, "sex", None)
  130. if not _is_empty(src_sex):
  131. target.sex = src_sex
  132. if _is_empty(getattr(target, "remark", None)):
  133. src_remark = getattr(source, "remark", None)
  134. if not _is_empty(src_remark):
  135. target.remark = src_remark
  136. for attr in ("birth_ym", "ethnic", "email", "address"):
  137. if _is_empty(getattr(target, attr, None)):
  138. src_val = getattr(source, attr, None)
  139. if not _is_empty(src_val):
  140. setattr(target, attr, src_val)
  141. target.customer_tags = _union_tags(
  142. getattr(target, "customer_tags", None),
  143. getattr(source, "customer_tags", None),
  144. )
  145. def find_customer_by_id_card(db: Session, id_card: str, tenant_no: str) -> Optional[BizCustomer]:
  146. ic = (id_card or "").strip()
  147. if not ic:
  148. return None
  149. return (
  150. db.query(BizCustomer)
  151. .filter(
  152. BizCustomer.id_card_no == ic,
  153. BizCustomer.tenant_no == tenant_no,
  154. BizCustomer.del_flag == 0,
  155. )
  156. .first()
  157. )
  158. def get_customer(db: Session, customer_id: str, tenant_no: Optional[str] = None) -> Optional[BizCustomer]:
  159. cid = (customer_id or "").strip()
  160. if not cid:
  161. return None
  162. return customer_crud.get_customer(db, cid, tenant_no=tenant_no)
  163. def repoint_customer_refs(db: Session, from_id: str, to_id: str, tenant_no: str) -> None:
  164. if not from_id or not to_id or from_id == to_id:
  165. return
  166. for model in (BizProtocolPartyCustomer, BizResearchDonationParty):
  167. for col_name in ("customer_id", "father_customer_id", "newborn_customer_id"):
  168. col = getattr(model, col_name)
  169. db.query(model).filter(
  170. col == from_id,
  171. model.del_flag == 0,
  172. model.tenant_no == tenant_no,
  173. ).update({col_name: to_id}, synchronize_session=False)
  174. def _build_role_profile(party: Dict[str, Any], spec: Dict[str, Any]) -> SimpleNamespace:
  175. name = _pick(party, spec["name_keys"])
  176. phone = _pick(party, spec["phone_keys"])
  177. id_card = _pick(party, spec["id_card_keys"])
  178. sex = _pick(party, spec["sex_keys"]) or spec["default_sex"]
  179. birth_ym = _pick(party, spec.get("birth_ym_keys") or ())
  180. ethnic = _pick(party, spec.get("ethnic_keys") or ())
  181. email = _pick(party, spec.get("email_keys") or ())
  182. address = _pick(party, spec.get("address_keys") or ())
  183. return SimpleNamespace(
  184. customer_name=name or None,
  185. phone_no=phone or None,
  186. id_card_no=id_card or None,
  187. sex=sex or None,
  188. age=_age_from_id_card(id_card),
  189. birth_ym=birth_ym or None,
  190. ethnic=ethnic or None,
  191. email=email or None,
  192. address=address or None,
  193. )
  194. def _create_role_customer(
  195. db: Session,
  196. *,
  197. profile: SimpleNamespace,
  198. tenant_no: str,
  199. user_id: str,
  200. commit: bool = True,
  201. create_dept_id: str = "",
  202. ) -> str:
  203. obj = customer_crud.create_customer(
  204. db,
  205. customer_name=profile.customer_name,
  206. sex=profile.sex or "2",
  207. id_card_no=profile.id_card_no or None,
  208. age=profile.age,
  209. birth_ym=profile.birth_ym,
  210. ethnic=profile.ethnic,
  211. email=profile.email,
  212. address=profile.address,
  213. phone_no=profile.phone_no or None,
  214. tenant_no=tenant_no,
  215. user_id=user_id,
  216. commit=commit,
  217. create_dept_id=create_dept_id,
  218. )
  219. return obj.id
  220. def _merge_and_repoint(
  221. db: Session,
  222. *,
  223. from_id: str,
  224. to_customer: Any,
  225. tenant_no: str,
  226. profile: Optional[SimpleNamespace] = None,
  227. commit: bool = True,
  228. ) -> str:
  229. source = get_customer(db, from_id, tenant_no=tenant_no)
  230. if source is not None:
  231. merge_customer_fields(to_customer, source)
  232. db.add(to_customer)
  233. if profile is not None:
  234. apply_role_profile_overwrite(to_customer, profile)
  235. db.add(to_customer)
  236. repoint_customer_refs(db, from_id, to_customer.id, tenant_no)
  237. customer_crud.soft_delete_customer(
  238. db, from_id, tenant_no=tenant_no, commit=commit
  239. )
  240. return to_customer.id
  241. def _sync_one_role(
  242. db: Session,
  243. party: Dict[str, Any],
  244. spec: Dict[str, Any],
  245. tenant_no: str,
  246. user_id: str,
  247. commit: bool = True,
  248. create_dept_id: str = "",
  249. ) -> str:
  250. profile = _build_role_profile(party, spec)
  251. if _is_empty(profile.customer_name):
  252. return ""
  253. id_card = profile.id_card_no or ""
  254. linked_id = _pick(party, spec["id_keys"])
  255. found = find_customer_by_id_card(db, id_card, tenant_no) if id_card else None
  256. if found is not None:
  257. if linked_id and linked_id != found.id:
  258. return _merge_and_repoint(
  259. db,
  260. from_id=linked_id,
  261. to_customer=found,
  262. tenant_no=tenant_no,
  263. profile=profile,
  264. commit=commit,
  265. )
  266. apply_role_profile_overwrite(found, profile)
  267. db.add(found)
  268. return found.id
  269. if linked_id:
  270. linked = get_customer(db, linked_id, tenant_no=tenant_no)
  271. if linked is not None:
  272. apply_role_profile_overwrite(linked, profile)
  273. db.add(linked)
  274. return linked.id
  275. return _create_role_customer(
  276. db,
  277. profile=profile,
  278. tenant_no=tenant_no,
  279. user_id=user_id,
  280. commit=commit,
  281. create_dept_id=create_dept_id,
  282. )
  283. def sync_party_customers(
  284. db: Session,
  285. party_dict: Dict[str, Any],
  286. tenant_no: str,
  287. user_id: str,
  288. *,
  289. commit: bool = True,
  290. create_dept_id: str = "",
  291. ) -> Dict[str, Any]:
  292. out = dict(party_dict or {})
  293. for spec in _ROLE_SPECS:
  294. role_id = _sync_one_role(
  295. db,
  296. out,
  297. spec,
  298. tenant_no,
  299. user_id,
  300. commit=commit,
  301. create_dept_id=create_dept_id,
  302. )
  303. for key in spec["id_keys"]:
  304. out[key] = role_id
  305. return out