oa_confirmation_kit_sync.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. """转正套件:审批通过后回写发起人 sys_user。"""
  2. from __future__ import annotations
  3. import json
  4. import logging
  5. import re
  6. from datetime import date, datetime
  7. from typing import Any, Dict, List, Optional
  8. from sqlalchemy.orm import Session
  9. from app.models.models import OaAppVersion, OaInstance, SysUser
  10. logger = logging.getLogger(__name__)
  11. WRITEBACK_KEYS = {
  12. "hireDate": "hire_date",
  13. "probationMonths": "probation_months",
  14. "name": "nick_name",
  15. "email": "email",
  16. "dept": "dept_id",
  17. "phone": "phone_no",
  18. }
  19. _PHONE_RE = re.compile(r"^1[3-9]\d{9}$")
  20. def _loads(raw: Any, default: Any = None) -> Any:
  21. if raw is None or raw == "":
  22. return default
  23. if isinstance(raw, (dict, list)):
  24. return raw
  25. try:
  26. return json.loads(raw)
  27. except Exception:
  28. return default
  29. def _extract_rules(form_schema: Any) -> List[Dict[str, Any]]:
  30. if form_schema is None:
  31. return []
  32. if isinstance(form_schema, list):
  33. return [r for r in form_schema if isinstance(r, dict)]
  34. if isinstance(form_schema, dict):
  35. rules = form_schema.get("rule") or form_schema.get("rules") or form_schema.get("fields")
  36. if isinstance(rules, list):
  37. return [r for r in rules if isinstance(r, dict)]
  38. if form_schema.get("type") or form_schema.get("field"):
  39. return [form_schema]
  40. return []
  41. def _walk(rules: List[Dict[str, Any]], out: List[Dict[str, Any]]) -> None:
  42. for rule in rules:
  43. out.append(rule)
  44. children = rule.get("children") or (rule.get("props") or {}).get("rule") or []
  45. if isinstance(children, list):
  46. _walk([c for c in children if isinstance(c, dict)], out)
  47. def _kit_field_map(kit: Dict[str, Any]) -> Dict[str, str]:
  48. """semantic -> form field id"""
  49. props = kit.get("props") if isinstance(kit.get("props"), dict) else {}
  50. mapping: Dict[str, str] = {}
  51. prop_keys = {
  52. "hireDateField": "hireDate",
  53. "probationMonthsField": "probationMonths",
  54. "confirmationDateField": "confirmationDate",
  55. "postField": "post",
  56. "jobLevelField": "jobLevel",
  57. "nameField": "name",
  58. "emailField": "email",
  59. "deptField": "dept",
  60. "phoneField": "phone",
  61. }
  62. for pk, sem in prop_keys.items():
  63. fid = props.get(pk)
  64. if fid:
  65. mapping[sem] = str(fid)
  66. children = kit.get("children") or props.get("rule") or []
  67. if isinstance(children, list):
  68. for c in children:
  69. if not isinstance(c, dict) or not c.get("field"):
  70. continue
  71. cprops = c.get("props") if isinstance(c.get("props"), dict) else {}
  72. sem = str(cprops.get("_fc_kit_field") or "").strip()
  73. if sem and sem not in mapping:
  74. mapping[sem] = str(c["field"])
  75. return mapping
  76. def _parse_hire_date(value: Any) -> Optional[date]:
  77. if value is None or value == "":
  78. return None
  79. if isinstance(value, datetime):
  80. return value.date()
  81. if isinstance(value, date):
  82. return value
  83. s = str(value).strip()[:10]
  84. try:
  85. return datetime.strptime(s, "%Y-%m-%d").date()
  86. except ValueError:
  87. return None
  88. def _dept_id(value: Any) -> Optional[str]:
  89. if value is None or value == "":
  90. return None
  91. if isinstance(value, dict):
  92. sid = str(value.get("id") or "").strip()
  93. return sid or None
  94. if isinstance(value, list) and value:
  95. return _dept_id(value[0])
  96. sid = str(value).strip()
  97. return sid or None
  98. def _is_empty(value: Any) -> bool:
  99. if value is None or value == "":
  100. return True
  101. if isinstance(value, list) and not value:
  102. return True
  103. if isinstance(value, dict) and not str(value.get("id") or "").strip():
  104. return True
  105. return False
  106. def extract_writeback_values(schema: Any, form_data: Dict[str, Any]) -> Dict[str, Any]:
  107. """从 schema+form_data 提取可回写列值(列名 -> 值)。空字段不进入结果。"""
  108. flat: List[Dict[str, Any]] = []
  109. _walk(_extract_rules(schema), flat)
  110. out: Dict[str, Any] = {}
  111. for rule in flat:
  112. if str(rule.get("type") or "") != "confirmationKit":
  113. continue
  114. fmap = _kit_field_map(rule)
  115. for sem, col in WRITEBACK_KEYS.items():
  116. field = fmap.get(sem)
  117. if not field:
  118. continue
  119. raw = form_data.get(field)
  120. if _is_empty(raw):
  121. continue
  122. if col == "hire_date":
  123. parsed = _parse_hire_date(raw)
  124. if parsed is None:
  125. logger.warning("confirmation kit skip invalid hire_date=%r", raw)
  126. continue
  127. out[col] = parsed
  128. elif col == "probation_months":
  129. try:
  130. months = int(raw)
  131. except (TypeError, ValueError):
  132. logger.warning("confirmation kit skip invalid probation_months=%r", raw)
  133. continue
  134. if months < 0:
  135. continue
  136. out[col] = months
  137. elif col == "dept_id":
  138. did = _dept_id(raw)
  139. if not did:
  140. continue
  141. out[col] = did
  142. elif col == "phone_no":
  143. phone = str(raw).strip()
  144. if not _PHONE_RE.match(phone):
  145. logger.warning("confirmation kit skip invalid phone=%r", phone)
  146. continue
  147. out[col] = phone
  148. elif col == "email":
  149. email = str(raw).strip()
  150. if "@" not in email:
  151. logger.warning("confirmation kit skip invalid email=%r", email)
  152. continue
  153. out[col] = email
  154. elif col == "nick_name":
  155. name = str(raw).strip()
  156. if not name:
  157. continue
  158. out[col] = name
  159. return out
  160. def apply_confirmation_kit_user_sync(db: Session, inst: OaInstance) -> None:
  161. """扫描 confirmationKit,有值则更新发起人 sys_user;异常只打日志。"""
  162. try:
  163. if not inst or not getattr(inst, "id", None):
  164. return
  165. form_data = _loads(getattr(inst, "form_data_json", None), {}) or {}
  166. if not isinstance(form_data, dict):
  167. form_data = {}
  168. ver = (
  169. db.query(OaAppVersion)
  170. .filter(OaAppVersion.id == inst.app_version_id, OaAppVersion.del_flag == 0)
  171. .first()
  172. )
  173. schema = _loads(getattr(ver, "form_schema_json", None) if ver else None, {}) or {}
  174. values = extract_writeback_values(schema, form_data)
  175. if not values:
  176. return
  177. user = (
  178. db.query(SysUser)
  179. .filter(
  180. SysUser.id == inst.initiator_user_id,
  181. SysUser.tenant_no == inst.tenant_no,
  182. SysUser.del_flag == 0,
  183. )
  184. .first()
  185. )
  186. if not user:
  187. logger.warning(
  188. "confirmation kit sync: initiator not found id=%s tenant=%s",
  189. inst.initiator_user_id,
  190. inst.tenant_no,
  191. )
  192. return
  193. for col, val in values.items():
  194. setattr(user, col, val)
  195. user.update_time = datetime.now()
  196. db.add(user)
  197. except Exception:
  198. logger.exception(
  199. "confirmation kit user sync failed instance=%s", getattr(inst, "id", None)
  200. )