diff --git a/extensions/sync_ehr_to_ad/README.md b/extensions/sync_ehr_to_ad/README.md index 1c7451f..3a88c93 100644 --- a/extensions/sync_ehr_to_ad/README.md +++ b/extensions/sync_ehr_to_ad/README.md @@ -18,6 +18,7 @@ - `displayName` 取 `employeeInfo.customProperties.extDDNIC_606508_303466862`。 - `department` 写入“EHR 部门编码 + EHR 部门名称”,例如 `V000076 IT`。 - `streetAddress` 默认取 `translateProperties.extgzddxx1_606508_892394263Text`。 +- `st`、`l`、`physicalDeliveryOfficeName` 默认按 `extgzddxx1_606508_892394263` 的原始值做映射。 ## public_cfg 示例 @@ -68,9 +69,23 @@ - `ldap_verify_tls`: 是否校验证书,默认 `true`。 - `proxy_alias_domain`: 生成 `smtp:@domain` 别名时使用的域名。 - `department_code_ad_attribute`: 部门编码写入的 AD 属性,默认 `departmentNumber`。 +- `work_location_value_key`: 工作地点原始值字段编码,默认 `extgzddxx1_606508_892394263`,用于映射 AD 省、市、办公室。 - `street_address_key`: 具体地址字段编码,默认 `extgzddxx1_606508_892394263Text`,写入 AD 的 `streetAddress`;取不到时会兜底尝试 `WorkSpacevalue` 和 `extgzddxx_606508_618643707`。 - `default_company`: 固定公司名;不传时尝试取 EHR 根组织名称。 -- `location_mappings`: 工作地点到 AD 国家、省、市字段的映射。 +- `location_mappings`: 工作地点原始值到 AD 国家、省、市、办公室字段的映射,可覆盖内置映射。 + +内置工作地点映射: + +| EHR 原始值 | AD st | AD l | AD Office | +| --- | --- | --- | --- | +| `qjjdizcpjk` | 上海 | 上海 | 上海张江 | +| `j1lw6k13ya` | 陕西 | 西安 | 西安环普 | +| `uzwlfke8vd` | 广州 | 深圳 | 深圳田厦 | +| `mrtkjjhoxx` | 上海 | 上海 | 上海徐汇 | +| `t21uyq5qvx` | 北京 | 北京 | 北京 | +| `nhu45qlh80` | 四川 | 成都 | 成都 | +| `5sntzs5dlr` | 加拿大 | 加拿大 | 多伦多 | +| `1` | 上海 | 上海 | 上海闵行 | ## 指定用户同步示例 diff --git a/extensions/sync_ehr_to_ad/job.py b/extensions/sync_ehr_to_ad/job.py index 66fbba2..8a48dbb 100644 --- a/extensions/sync_ehr_to_ad/job.py +++ b/extensions/sync_ehr_to_ad/job.py @@ -12,6 +12,7 @@ from extensions.sync_ehr_to_ad.api import ActiveDirectoryClient, SyncEhrToAdApi logger = logging.getLogger("connecthub.extensions.sync_ehr_to_ad") _EHR_AD_ACCOUNT_KEY = "extADAccountName_606508_511687157" +_EHR_WORK_LOCATION_VALUE_KEY = "extgzddxx1_606508_892394263" _EHR_WORK_LOCATION_TEXT_KEY = "extgzddxx1_606508_892394263Text" _EHR_STREET_ADDRESS_KEY = _EHR_WORK_LOCATION_TEXT_KEY _EHR_STREET_ADDRESS_FALLBACK_KEYS = ("WorkSpacevalue", "extgzddxx_606508_618643707") @@ -102,6 +103,30 @@ def _field_value(item: dict[str, Any], key: str) -> str: return "" +def _field_raw_value(item: dict[str, Any], key: str) -> str: + emp = item.get("employeeInfo") or {} + rec = item.get("recordInfo") or {} + for node in (emp, rec): + if not isinstance(node, dict): + continue + s = _scalar_value(node.get(key)) + if s: + return s + s = _custom_prop_value(node.get("customProperties"), key) + if s: + return s + for child in node.values(): + if not isinstance(child, dict): + continue + s = _scalar_value(child.get(key)) + if s: + return s + s = _custom_prop_value(child.get("customProperties"), key) + if s: + return s + return "" + + def _field_translate_or_value(item: dict[str, Any], key: str) -> str: emp = item.get("employeeInfo") or {} rec = item.get("recordInfo") or {} @@ -266,6 +291,82 @@ def _location_from_workplace(workplace: str, mappings: dict[str, Any] | None = N return {} +def _location_from_workspace_value(value: str, mappings: dict[str, Any] | None = None) -> dict[str, Any]: + code = str(value or "").strip() + defaults: dict[str, dict[str, Any]] = { + "qjjdizcpjk": { + "co": "China", + "c": "CN", + "countryCode": 156, + "st": "上海", + "l": "上海", + "office": "上海张江", + }, + "j1lw6k13ya": { + "co": "China", + "c": "CN", + "countryCode": 156, + "st": "陕西", + "l": "西安", + "office": "西安环普", + }, + "uyglfkg8vd": { + "co": "China", + "c": "CN", + "countryCode": 156, + "st": "广州", + "l": "深圳", + "office": "深圳田厦", + }, + "mrtkjjhoxx": { + "co": "China", + "c": "CN", + "countryCode": 156, + "st": "上海", + "l": "上海", + "office": "上海徐汇", + }, + "t21uyq5qvx": { + "co": "China", + "c": "CN", + "countryCode": 156, + "st": "北京", + "l": "北京", + "office": "北京", + }, + "nhu45qlh80": { + "co": "China", + "c": "CN", + "countryCode": 156, + "st": "四川", + "l": "成都", + "office": "成都", + }, + "5sntzg5dlr": { + "co": "Canada", + "c": "CA", + "countryCode": 124, + "st": "加拿大", + "l": "加拿大", + "office": "多伦多", + }, + "1": { + "co": "China", + "c": "CN", + "countryCode": 156, + "st": "上海", + "l": "上海", + "office": "上海闵行", + }, + } + merged = dict(defaults) + if isinstance(mappings, dict): + for k, v in mappings.items(): + if isinstance(v, dict): + merged[str(k).strip()] = v + return dict(merged.get(code, {})) + + def _org_name(org: dict[str, Any]) -> str: for key in ("name", "Name", "shortName", "ShortName"): s = str(org.get(key) or "").strip() @@ -390,6 +491,7 @@ class SyncEhrToAdUserJob(BaseJob): max_users = int(params.get("max_users") or 0) connect_timeout_s = int(params.get("ldap_connect_timeout_s") or 10) domain_account_key = str(params.get("domain_account_custom_key") or "").strip() or _EHR_AD_ACCOUNT_KEY + work_location_value_key = str(params.get("work_location_value_key") or "").strip() or _EHR_WORK_LOCATION_VALUE_KEY work_location_text_key = str(params.get("work_location_text_key") or "").strip() or _EHR_WORK_LOCATION_TEXT_KEY street_address_key = str(params.get("street_address_key") or "").strip() or _EHR_STREET_ADDRESS_KEY proxy_alias_domain = str(params.get("proxy_alias_domain") or "").strip() or None @@ -566,6 +668,7 @@ class SyncEhrToAdUserJob(BaseJob): job_number = str(rec.get("jobNumber") or rec.get("JobNumber") or "").strip() mobile = _field_value(item, "MobilePhone") or _extract_mobile_phone(emp) office = _field_translate_or_value(item, "Place") + workplace_value = _field_raw_value(item, work_location_value_key) workplace_text = _field_translate_or_value(item, work_location_text_key) street_address = _field_value(item, street_address_key) if not street_address: @@ -583,7 +686,20 @@ class SyncEhrToAdUserJob(BaseJob): street_address, ) company = default_company or _root_org_name(org, org_by_oid) - location_attrs = _location_from_workplace(workplace_text or office, location_mappings) + location_attrs = _location_from_workspace_value(workplace_value, location_mappings) or _location_from_workplace( + workplace_text or office, location_mappings + ) + office = str(location_attrs.get("office") or office or "").strip() + if verbose_trace: + logger.info( + "AD 办公地点映射:sam=%s workspace_key=%s workspace_value=%r province=%r city=%r office=%r", + sam, + work_location_value_key, + workplace_value, + location_attrs.get("st"), + location_attrs.get("l"), + office, + ) manager_dn = "" manager_uid = _to_int_safe(rec.get("pOIdEmpAdmin") or rec.get("POIdEmpAdmin"))