This commit is contained in:
Marsway 2026-03-04 13:52:41 +08:00
parent ecc79128f6
commit 473343f548
1 changed files with 81 additions and 3 deletions

View File

@ -59,6 +59,26 @@ def _to_bool_or_none(v: Any) -> bool | None:
return bool(v) return bool(v)
def _normalize_job_no(v: Any) -> str:
"""
工号标准化
- 去首尾空白去内部空格
- 数值型字符串如 123.0 -> 123常见于表单数字字段
- 统一大写便于大小写不敏感匹配
"""
s = str(v or "").strip()
if not s:
return ""
s = s.replace(" ", "")
try:
if "." in s and s.endswith(".0"):
i = int(float(s))
s = str(i)
except Exception:
pass
return s.upper()
def _choose_better_record(current: dict[str, Any], candidate: dict[str, Any]) -> dict[str, Any]: def _choose_better_record(current: dict[str, Any], candidate: dict[str, Any]) -> dict[str, Any]:
def _score(item: dict[str, Any]) -> str: def _score(item: dict[str, Any]) -> str:
record = item.get("recordInfo") or {} record = item.get("recordInfo") or {}
@ -127,6 +147,9 @@ class SyncEhrToOaFormJob(BaseJob):
rd_attr_custom_key = str(params.get("rd_attr_custom_key") or "").strip() or None rd_attr_custom_key = str(params.get("rd_attr_custom_key") or "").strip() or None
domain_custom_key = str(params.get("domain_account_custom_key") or "").strip() or None domain_custom_key = str(params.get("domain_account_custom_key") or "").strip() or None
verbose_trace = _to_bool_or_none(params.get("verbose_trace"))
if verbose_trace is None:
verbose_trace = True
seeyon = SeeyonClient(base_url=oa_base_url, rest_user=rest_user, rest_password=rest_password, loginName=login_name) seeyon = SeeyonClient(base_url=oa_base_url, rest_user=rest_user, rest_password=rest_password, loginName=login_name)
ehr = SyncEhrToOaApi(secret_params={"app_key": app_key, "app_secret": app_secret}) ehr = SyncEhrToOaApi(secret_params={"app_key": app_key, "app_secret": app_secret})
@ -152,6 +175,7 @@ class SyncEhrToOaFormJob(BaseJob):
# 3) 员工按工号归并(同工号保留“最新”记录) # 3) 员工按工号归并(同工号保留“最新”记录)
ehr_by_job_no: dict[str, dict[str, Any]] = {} ehr_by_job_no: dict[str, dict[str, Any]] = {}
ehr_by_job_no_norm: dict[str, dict[str, Any]] = {}
for item in emp_rows: for item in emp_rows:
if not isinstance(item, dict): if not isinstance(item, dict):
continue continue
@ -163,12 +187,20 @@ class SyncEhrToOaFormJob(BaseJob):
continue continue
existing = ehr_by_job_no.get(job_no) existing = ehr_by_job_no.get(job_no)
ehr_by_job_no[job_no] = item if existing is None else _choose_better_record(existing, item) ehr_by_job_no[job_no] = item if existing is None else _choose_better_record(existing, item)
job_no_norm = _normalize_job_no(job_no)
if job_no_norm:
ex2 = ehr_by_job_no_norm.get(job_no_norm)
ehr_by_job_no_norm[job_no_norm] = item if ex2 is None else _choose_better_record(ex2, item)
logger.info( logger.info(
"EHR 数据准备完成employee_rows=%s organization_rows=%s distinct_job_numbers=%s", "EHR 数据准备完成employee_rows=%s organization_rows=%s distinct_job_numbers=%s distinct_job_numbers_norm=%s",
len(emp_rows), len(emp_rows),
len(org_rows), len(org_rows),
len(ehr_by_job_no), len(ehr_by_job_no),
len(ehr_by_job_no_norm),
) )
if verbose_trace:
for job_no in list(ehr_by_job_no.keys()):
logger.info("EHR 工号明细raw=%s norm=%s", job_no, _normalize_job_no(job_no))
# 4) 导出 OA 表单,建立字段映射 + 工号到记录ID映射 # 4) 导出 OA 表单,建立字段映射 + 工号到记录ID映射
exp_resp = seeyon.export_cap4_form_soap( exp_resp = seeyon.export_cap4_form_soap(
@ -259,6 +291,7 @@ class SyncEhrToOaFormJob(BaseJob):
job_field_code = display_to_code["工号"] job_field_code = display_to_code["工号"]
oa_id_by_job_no: dict[str, int] = {} oa_id_by_job_no: dict[str, int] = {}
oa_id_by_job_no_norm: dict[str, int] = {}
for row in rows: for row in rows:
if not isinstance(row, dict): if not isinstance(row, dict):
continue continue
@ -281,16 +314,43 @@ class SyncEhrToOaFormJob(BaseJob):
except Exception: except Exception:
continue continue
oa_id_by_job_no[job_no] = row_id oa_id_by_job_no[job_no] = row_id
logger.info("OA 工号索引完成indexed_job_numbers=%s", len(oa_id_by_job_no)) job_no_norm = _normalize_job_no(job_no)
if job_no_norm:
oa_id_by_job_no_norm[job_no_norm] = row_id
logger.info(
"OA 工号索引完成indexed_job_numbers=%s indexed_job_numbers_norm=%s",
len(oa_id_by_job_no),
len(oa_id_by_job_no_norm),
)
if verbose_trace:
for job_no, row_id in list(oa_id_by_job_no.items()):
logger.info("OA 工号索引明细raw=%s norm=%s row_id=%s", job_no, _normalize_job_no(job_no), row_id)
# 5) 组装批量更新数据 # 5) 组装批量更新数据
data_list: list[dict[str, Any]] = [] data_list: list[dict[str, Any]] = []
not_found_in_oa = 0 not_found_in_oa = 0
unmatched_samples: list[str] = []
for job_no, item in ehr_by_job_no.items(): for job_no, item in ehr_by_job_no.items():
oa_record_id = oa_id_by_job_no.get(job_no) oa_record_id = oa_id_by_job_no.get(job_no)
matched_by = "raw"
if oa_record_id is None:
oa_record_id = oa_id_by_job_no_norm.get(_normalize_job_no(job_no))
matched_by = "normalized"
if oa_record_id is None: if oa_record_id is None:
not_found_in_oa += 1 not_found_in_oa += 1
if len(unmatched_samples) < 20:
unmatched_samples.append(job_no)
if verbose_trace:
logger.info("匹配失败job_no=%s norm=%s", job_no, _normalize_job_no(job_no))
continue continue
if verbose_trace:
logger.info(
"匹配成功job_no=%s norm=%s row_id=%s matched_by=%s",
job_no,
_normalize_job_no(job_no),
oa_record_id,
matched_by,
)
emp = item.get("employeeInfo") or {} emp = item.get("employeeInfo") or {}
rec = item.get("recordInfo") or {} rec = item.get("recordInfo") or {}
@ -329,6 +389,10 @@ class SyncEhrToOaFormJob(BaseJob):
{"name": display_to_code["在离职"], "value": is_leaving, "showValue": is_leaving}, {"name": display_to_code["在离职"], "value": is_leaving, "showValue": is_leaving},
{"name": display_to_code["域账号"], "value": domain_account, "showValue": domain_account}, {"name": display_to_code["域账号"], "value": domain_account, "showValue": domain_account},
] ]
if verbose_trace:
logger.info("字段映射job_no=%s row_id=%s", job_no, oa_record_id)
for fld in fields_payload:
logger.info("字段映射明细job_no=%s field=%s value=%s", job_no, fld["name"], fld["value"])
data_list.append( data_list.append(
{ {
@ -350,7 +414,8 @@ class SyncEhrToOaFormJob(BaseJob):
) )
if not data_list: if not data_list:
raise RuntimeError( raise RuntimeError(
"No updates prepared for OA batch-update (check jobNumber matching between EHR and OA, and form field mapping)" "No updates prepared for OA batch-update (check jobNumber matching between EHR and OA, and form field mapping). "
f"unmatched_sample={unmatched_samples}"
) )
# 6) 分批执行 batch-update # 6) 分批执行 batch-update
@ -360,6 +425,19 @@ class SyncEhrToOaFormJob(BaseJob):
do_trigger_bool = _to_bool_or_none(do_trigger) do_trigger_bool = _to_bool_or_none(do_trigger)
for i in range(0, len(data_list), batch_size): for i in range(0, len(data_list), batch_size):
chunk = data_list[i : i + batch_size] chunk = data_list[i : i + batch_size]
if verbose_trace:
logger.info("批量更新尝试chunk_index=%s chunk_size=%s", i // batch_size + 1, len(chunk))
for row in chunk:
try:
record = (((row or {}).get("masterTable") or {}).get("record") or {})
row_id = record.get("id")
fields = record.get("fields") or []
logger.info("批量更新行row_id=%s fields_count=%s", row_id, len(fields))
for fld in fields:
if isinstance(fld, dict):
logger.info("批量更新字段row_id=%s field=%s value=%s", row_id, fld.get("name"), fld.get("value"))
except Exception:
logger.info("批量更新行日志输出失败,已忽略")
resp = seeyon.batch_update_cap4_form_soap( resp = seeyon.batch_update_cap4_form_soap(
formCode=oa_form_code, formCode=oa_form_code,
loginName=oa_login_name, loginName=oa_login_name,