diff --git a/app/integrations/seeyon.py b/app/integrations/seeyon.py index da1d1f9..9e6b170 100644 --- a/app/integrations/seeyon.py +++ b/app/integrations/seeyon.py @@ -1,6 +1,7 @@ from __future__ import annotations import logging +from urllib.parse import quote from typing import Any import httpx @@ -164,3 +165,32 @@ class SeeyonClient(BaseClient): json=body, headers={"Content-Type": "application/json"}, ) + + def get_org_members_by_code(self, *, code: str, pageNo: int = 0, pageSize: int = 20) -> list[dict[str, Any]]: + """ + 按人员编码查询 OA 人员信息: + GET /seeyon/rest/orgMembers/code/{code}?pageNo={pageNo}&pageSize={pageSize} + """ + c = str(code or "").strip() + if not c: + raise ValueError("code is required") + if pageNo < 0: + pageNo = 0 + if pageSize <= 0: + pageSize = 20 + path = f"/seeyon/rest/orgMembers/code/{quote(c, safe='')}" + resp = self.request("GET", path, params={"pageNo": pageNo, "pageSize": pageSize}) + payload = resp.json() if resp.content else {} + if isinstance(payload, list): + return [x for x in payload if isinstance(x, dict)] + if isinstance(payload, dict): + data = payload.get("data") + if isinstance(data, list): + return [x for x in data if isinstance(x, dict)] + if isinstance(data, dict): + items = data.get("content") + if isinstance(items, list): + return [x for x in items if isinstance(x, dict)] + return [data] + return [payload] + return [] diff --git a/extensions/sync_ehr_to_oa/job.py b/extensions/sync_ehr_to_oa/job.py index 12580c2..eea645f 100644 --- a/extensions/sync_ehr_to_oa/job.py +++ b/extensions/sync_ehr_to_oa/job.py @@ -40,6 +40,12 @@ def _cell_value(cell: Any) -> str: return str(cell or "").strip() +def _cell_show_value(cell: Any) -> str: + if isinstance(cell, dict): + return str(cell.get("showValue") or "").strip() + return "" + + def _date_only(s: Any) -> str: v = str(s or "").strip() if not v: @@ -148,6 +154,20 @@ def _extract_staff_code(staff_profile: dict[str, Any]) -> str: ).strip() +def _pick_best_member_by_code(members: list[dict[str, Any]]) -> dict[str, Any] | None: + if not members: + return None + # 优先:可用且在职 + for m in members: + if bool(m.get("isValid", True)) and int(m.get("state", 1) or 1) == 1: + return m + # 次优:enabled + for m in members: + if bool(m.get("enabled", True)): + return m + return members[0] + + def _extract_oa_row_id_and_fields(row: dict[str, Any]) -> tuple[int | None, dict[str, Any]]: """ 兼容不同 OA export 返回结构,提取: @@ -391,12 +411,28 @@ class SyncEhrToOaFormJob(BaseJob): code = _extract_staff_code(profile) if code: user_id_to_staff_code[int(uid)] = code + # 3.4) 将 HRBP/汇报人工号转换为 OA 人员ID(member 字段要求) + need_member_codes = list({c for c in user_id_to_staff_code.values() if str(c or "").strip()}) + code_to_member: dict[str, dict[str, str]] = {} + for code in need_member_codes: + try: + members = seeyon.get_org_members_by_code(code=code, pageNo=0, pageSize=20) + best = _pick_best_member_by_code(members) + if not best: + continue + member_id = str(best.get("id") or "").strip() + member_name = str(best.get("name") or best.get("loginName") or "").strip() + if member_id: + code_to_member[code] = {"id": member_id, "name": member_name} + except Exception as e: # noqa: BLE001 + logger.warning("OA 人员查询失败:code=%s err=%r", code, e) logger.info( - "人员工号反查完成:staff_profiles=%s reportings=%s hrbp_ids=%s resolved_staff_codes=%s", + "人员工号反查完成:staff_profiles=%s reportings=%s hrbp_ids=%s resolved_staff_codes=%s resolved_member_ids=%s", len(staff_profile_by_user_id), len(reporting_user_ids), len(hrbp_user_ids), len(user_id_to_staff_code), + len(code_to_member), ) if verbose_trace: for job_no in list(ehr_by_job_no.keys()): @@ -427,13 +463,19 @@ class SyncEhrToOaFormJob(BaseJob): leave_date = _date_only(rec.get("lastWorkDate")) id_number = str(emp.get("iDNumber") or "") hrbp_uid = _to_int_safe(rec.get(_EHR_HRBP_ID_KEY) or _custom_prop_value(rec.get("customProperties"), _EHR_HRBP_ID_KEY)) - hrbp = str(user_id_to_staff_code.get(hrbp_uid) or "") + hrbp_code = str(user_id_to_staff_code.get(hrbp_uid) or "") + hrbp_member = code_to_member.get(hrbp_code, {}) + hrbp = str(hrbp_member.get("id") or "") + hrbp_show = str(hrbp_member.get("name") or hrbp_code) manager_uid = _extract_reporting_user_id(staff_profile_by_user_id.get(user_id, {})) - manager = str(user_id_to_staff_code.get(manager_uid) or "") + manager_code = str(user_id_to_staff_code.get(manager_uid) or "") + manager_member = code_to_member.get(manager_code, {}) + manager = str(manager_member.get("id") or "") + manager_show = str(manager_member.get("name") or manager_code) is_leaving = "是" if _date_only(rec.get("lastWorkDate")) else "否" domain_account = _custom_prop_value(emp.get("customProperties"), domain_custom_key) or str(emp.get("_Name") or "") logger.info( - "EHR 字段预览:job_no=%s company=%s name=%s rd_attr=%s place=%s entry_date=%s leave_date=%s id_number=%s hrbp=%s manager=%s is_leaving=%s domain_account=%s", + "EHR 字段预览:job_no=%s company=%s name=%s rd_attr=%s place=%s entry_date=%s leave_date=%s id_number=%s hrbp_id=%s hrbp_show=%s manager_id=%s manager_show=%s is_leaving=%s domain_account=%s", job_no, company, name, @@ -443,7 +485,9 @@ class SyncEhrToOaFormJob(BaseJob): leave_date, id_number, hrbp, + hrbp_show, manager, + manager_show, is_leaving, domain_account, ) @@ -652,9 +696,15 @@ class SyncEhrToOaFormJob(BaseJob): leave_date = _date_only(rec.get("lastWorkDate")) id_number = str(emp.get("iDNumber") or "") hrbp_uid = _to_int_safe(rec.get(_EHR_HRBP_ID_KEY) or _custom_prop_value(rec.get("customProperties"), _EHR_HRBP_ID_KEY)) - hrbp = str(user_id_to_staff_code.get(hrbp_uid) or "") + hrbp_code = str(user_id_to_staff_code.get(hrbp_uid) or "") + hrbp_member = code_to_member.get(hrbp_code, {}) + hrbp = str(hrbp_member.get("id") or "") + hrbp_show = str(hrbp_member.get("name") or hrbp_code) manager_uid = _extract_reporting_user_id(staff_profile_by_user_id.get(user_id, {})) - manager = str(user_id_to_staff_code.get(manager_uid) or "") + manager_code = str(user_id_to_staff_code.get(manager_uid) or "") + manager_member = code_to_member.get(manager_code, {}) + manager = str(manager_member.get("id") or "") + manager_show = str(manager_member.get("name") or manager_code) is_leaving = "是" if _date_only(rec.get("lastWorkDate")) else "否" domain_account = _custom_prop_value(emp.get("customProperties"), domain_custom_key) or str(emp.get("_Name") or "") @@ -668,6 +718,8 @@ class SyncEhrToOaFormJob(BaseJob): id_number = _prefer_non_empty(id_number, _cell_value(existing_field_map.get(display_to_code["身份证号"]))) hrbp = _prefer_non_empty(hrbp, _cell_value(existing_field_map.get(display_to_code["HRBP"]))) manager = _prefer_non_empty(manager, _cell_value(existing_field_map.get(display_to_code["汇报人"]))) + hrbp_show = _prefer_non_empty(hrbp_show, _cell_show_value(existing_field_map.get(display_to_code["HRBP"]))) + manager_show = _prefer_non_empty(manager_show, _cell_show_value(existing_field_map.get(display_to_code["汇报人"]))) is_leaving = _prefer_non_empty(is_leaving, _cell_value(existing_field_map.get(display_to_code["在离职"]))) domain_account = _prefer_non_empty(domain_account, _cell_value(existing_field_map.get(display_to_code["域账号"]))) @@ -679,8 +731,8 @@ class SyncEhrToOaFormJob(BaseJob): {"name": display_to_code["入职日期"], "value": entry_date, "showValue": entry_date}, {"name": display_to_code["离职日期"], "value": leave_date, "showValue": leave_date}, {"name": display_to_code["身份证号"], "value": id_number, "showValue": id_number}, - {"name": display_to_code["HRBP"], "value": hrbp, "showValue": hrbp}, - {"name": display_to_code["汇报人"], "value": manager, "showValue": manager}, + {"name": display_to_code["HRBP"], "value": hrbp, "showValue": hrbp_show}, + {"name": display_to_code["汇报人"], "value": manager, "showValue": manager_show}, {"name": display_to_code["在离职"], "value": is_leaving, "showValue": is_leaving}, {"name": display_to_code["域账号"], "value": domain_account, "showValue": domain_account}, ]