From 473343f548c272fc86c107e66a55cc513fd25562 Mon Sep 17 00:00:00 2001 From: Marsway Date: Wed, 4 Mar 2026 13:52:41 +0800 Subject: [PATCH] fixing --- extensions/sync_ehr_to_oa/job.py | 84 ++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 3 deletions(-) diff --git a/extensions/sync_ehr_to_oa/job.py b/extensions/sync_ehr_to_oa/job.py index 6c42225..11c82fc 100644 --- a/extensions/sync_ehr_to_oa/job.py +++ b/extensions/sync_ehr_to_oa/job.py @@ -59,6 +59,26 @@ def _to_bool_or_none(v: Any) -> bool | None: 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 _score(item: dict[str, Any]) -> str: 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 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) ehr = SyncEhrToOaApi(secret_params={"app_key": app_key, "app_secret": app_secret}) @@ -152,6 +175,7 @@ class SyncEhrToOaFormJob(BaseJob): # 3) 员工按工号归并(同工号保留“最新”记录) ehr_by_job_no: dict[str, dict[str, Any]] = {} + ehr_by_job_no_norm: dict[str, dict[str, Any]] = {} for item in emp_rows: if not isinstance(item, dict): continue @@ -163,12 +187,20 @@ class SyncEhrToOaFormJob(BaseJob): continue 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) + 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( - "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(org_rows), 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映射 exp_resp = seeyon.export_cap4_form_soap( @@ -259,6 +291,7 @@ class SyncEhrToOaFormJob(BaseJob): job_field_code = display_to_code["工号"] oa_id_by_job_no: dict[str, int] = {} + oa_id_by_job_no_norm: dict[str, int] = {} for row in rows: if not isinstance(row, dict): continue @@ -281,16 +314,43 @@ class SyncEhrToOaFormJob(BaseJob): except Exception: continue 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) 组装批量更新数据 data_list: list[dict[str, Any]] = [] not_found_in_oa = 0 + unmatched_samples: list[str] = [] for job_no, item in ehr_by_job_no.items(): 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: 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 + 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 {} 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": 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( { @@ -350,7 +414,8 @@ class SyncEhrToOaFormJob(BaseJob): ) if not data_list: 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 @@ -360,6 +425,19 @@ class SyncEhrToOaFormJob(BaseJob): do_trigger_bool = _to_bool_or_none(do_trigger) for i in range(0, len(data_list), 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( formCode=oa_form_code, loginName=oa_login_name,