diff --git a/extensions/sync_ehr_to_oa/job.py b/extensions/sync_ehr_to_oa/job.py index 2bafe89..6c42225 100644 --- a/extensions/sync_ehr_to_oa/job.py +++ b/extensions/sync_ehr_to_oa/job.py @@ -46,6 +46,19 @@ def _custom_prop_value(custom_props: Any, key: str | None) -> str: return str(raw or "").strip() +def _to_bool_or_none(v: Any) -> bool | None: + if v is None: + return None + if isinstance(v, bool): + return v + s = str(v).strip().lower() + if s in ("1", "true", "yes", "y", "on"): + return True + if s in ("0", "false", "no", "n", "off", ""): + return False + return bool(v) + + 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 {} @@ -150,15 +163,43 @@ 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) + logger.info( + "EHR 数据准备完成:employee_rows=%s organization_rows=%s distinct_job_numbers=%s", + len(emp_rows), + len(org_rows), + len(ehr_by_job_no), + ) # 4) 导出 OA 表单,建立字段映射 + 工号到记录ID映射 - exp_resp = seeyon.export_cap4_form_soap(templateCode=oa_template_code, senderLoginName=sender_login_name, rightId=oa_right_id) + exp_resp = seeyon.export_cap4_form_soap( + templateCode=oa_template_code, + senderLoginName=sender_login_name, + rightId=oa_right_id, + ) raw = exp_resp.text or "" - payload = json.loads(raw) if raw else {} + logger.info( + "OA export 返回:status=%s content_length=%s template=%s", + exp_resp.status_code, + len(raw), + oa_template_code, + ) + if raw: + logger.info("OA export 响应预览:%s", raw[:1000]) + try: + payload = json.loads(raw) if raw else {} + except Exception as e: # noqa: BLE001 + raise RuntimeError(f"OA export 响应不是有效 JSON: err={e!r} preview={raw[:500]!r}") from e + + export_code = payload.get("code") + export_message = payload.get("message") + if export_code not in (None, 0, "0"): + raise RuntimeError(f"OA export failed code={export_code!r} message={export_message!r}") outer = payload.get("data") or {} form = outer.get("data") or {} if not isinstance(form, dict): - raise RuntimeError("OA export invalid: data.data is not an object") + raise RuntimeError( + f"OA export invalid: data.data is not an object; payload_keys={list(payload.keys())[:20]}" + ) definition = form.get("definition") or {} fields = definition.get("fields") or [] @@ -209,6 +250,12 @@ class SyncEhrToOaFormJob(BaseJob): oa_master_table_name = str(master_tbl.get("name") or "").strip() if not oa_master_table_name: raise RuntimeError("public_cfg.oa_master_table_name is required (cannot infer from OA export)") + logger.info( + "OA 表单解析完成:template=%s master_table=%s form_rows=%s", + oa_template_code, + oa_master_table_name, + len(rows), + ) job_field_code = display_to_code["工号"] oa_id_by_job_no: dict[str, int] = {} @@ -234,6 +281,7 @@ 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)) # 5) 组装批量更新数据 data_list: list[dict[str, Any]] = [] @@ -295,11 +343,21 @@ class SyncEhrToOaFormJob(BaseJob): "subTables": [], } ) + logger.info( + "待更新数据准备完成:prepared_updates=%s not_found_in_oa=%s", + len(data_list), + not_found_in_oa, + ) + if not data_list: + raise RuntimeError( + "No updates prepared for OA batch-update (check jobNumber matching between EHR and OA, and form field mapping)" + ) # 6) 分批执行 batch-update success_count = 0 failed_count = 0 failed_data: dict[str, str] = {} + 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] resp = seeyon.batch_update_cap4_form_soap( @@ -308,7 +366,7 @@ class SyncEhrToOaFormJob(BaseJob): rightId=oa_right_id, dataList=chunk, uniqueFiled=[job_field_code], - doTrigger=bool(do_trigger) if do_trigger is not None else None, + doTrigger=do_trigger_bool, ) rj = resp.json() if resp.content else {} code = int(rj.get("code", -1))