This commit is contained in:
Marsway 2026-04-30 18:18:05 +08:00
parent 2992a2631c
commit 853ffcf5d0
2 changed files with 27 additions and 87 deletions

View File

@ -65,7 +65,7 @@
- `ldap_verify_tls`: 是否校验证书,默认 `true` - `ldap_verify_tls`: 是否校验证书,默认 `true`
- `proxy_alias_domain`: 生成 `smtp:<sAMAccountName>@domain` 别名时使用的域名。 - `proxy_alias_domain`: 生成 `smtp:<sAMAccountName>@domain` 别名时使用的域名。
- `department_code_ad_attribute`: 部门编码写入的 AD 属性,默认 `departmentNumber` - `department_code_ad_attribute`: 部门编码写入的 AD 属性,默认 `departmentNumber`
- `street_address_key`: 具体地址字段编码,默认 `extgzddxx_606508_618643707`,写入 AD 的 `streetAddress`。 - `street_address_key`: 具体地址字段编码,默认 `WorkSpacevalue`,写入 AD 的 `streetAddress`;取不到时会兜底尝试 `extgzddxx_606508_618643707`。
- `default_company`: 固定公司名;不传时尝试取 EHR 根组织名称。 - `default_company`: 固定公司名;不传时尝试取 EHR 根组织名称。
- `location_mappings`: 工作地点到 AD 国家、省、市字段的映射。 - `location_mappings`: 工作地点到 AD 国家、省、市字段的映射。

View File

@ -13,7 +13,8 @@ logger = logging.getLogger("connecthub.extensions.sync_ehr_to_ad")
_EHR_AD_ACCOUNT_KEY = "extADAccountName_606508_511687157" _EHR_AD_ACCOUNT_KEY = "extADAccountName_606508_511687157"
_EHR_WORK_LOCATION_TEXT_KEY = "extgzddxx1_606508_892394263Text" _EHR_WORK_LOCATION_TEXT_KEY = "extgzddxx1_606508_892394263Text"
_EHR_STREET_ADDRESS_KEY = "extgzddxx_606508_618643707" _EHR_STREET_ADDRESS_KEY = "WorkSpacevalue"
_EHR_STREET_ADDRESS_FALLBACK_KEYS = ("extgzddxx_606508_618643707",)
def _to_bool_or_none(v: Any) -> bool | None: def _to_bool_or_none(v: Any) -> bool | None:
@ -38,17 +39,12 @@ def _to_int_safe(v: Any) -> int:
def _scalar_value(raw: Any) -> str: def _scalar_value(raw: Any) -> str:
if isinstance(raw, dict): if isinstance(raw, dict):
val = None val = raw.get("value")
for value_key in ("value", "showValue", "text", "Text", "displayValue", "DisplayValue"): if val is None or str(val).strip() == "":
val = raw.get(value_key) val = raw.get("showValue")
if val is not None and str(val).strip() != "":
break
if isinstance(val, (dict, list, tuple, set)):
return _scalar_value(val)
return str(val or "").strip() return str(val or "").strip()
if isinstance(raw, (list, tuple, set)): if isinstance(raw, (list, tuple, set)):
values = [_scalar_value(x) for x in raw] return "\n".join([str(x).strip() for x in raw if x is not None and str(x).strip() != ""])
return "\n".join([x for x in values if x])
return str(raw or "").strip() return str(raw or "").strip()
@ -75,76 +71,6 @@ def _translate_value(node: dict[str, Any], key: str | None) -> str:
return "" return ""
def _candidate_keys(key: str | None) -> list[str]:
if not key:
return []
candidates = [key, f"{key}Text"]
if key.endswith("Text"):
candidates.insert(0, key)
candidates.append(key[:-4])
out: list[str] = []
seen: set[str] = set()
for candidate in candidates:
s = str(candidate or "").strip()
if s and s not in seen:
seen.add(s)
out.append(s)
return out
def _deep_field_value(node: Any, key: str | None) -> str:
candidates = set(_candidate_keys(key))
if not candidates:
return ""
if isinstance(node, dict):
for candidate in candidates:
if candidate in node:
s = _scalar_value(node.get(candidate))
if s:
return s
for value in node.values():
s = _deep_field_value(value, key)
if s:
return s
elif isinstance(node, list):
for value in node:
s = _deep_field_value(value, key)
if s:
return s
return ""
def _collect_matching_keys(node: Any, keyword: str, *, limit: int = 30) -> list[str]:
matches: list[str] = []
seen: set[str] = set()
needle = str(keyword or "").lower()
if not needle:
return matches
def walk(current: Any) -> None:
if len(matches) >= limit:
return
if isinstance(current, dict):
for k, v in current.items():
key = str(k)
if needle in key.lower() and key not in seen:
seen.add(key)
matches.append(key)
if len(matches) >= limit:
return
walk(v)
if len(matches) >= limit:
return
elif isinstance(current, list):
for v in current:
walk(v)
if len(matches) >= limit:
return
walk(node)
return matches
def _field_value(item: dict[str, Any], key: str) -> str: def _field_value(item: dict[str, Any], key: str) -> str:
emp = item.get("employeeInfo") or {} emp = item.get("employeeInfo") or {}
rec = item.get("recordInfo") or {} rec = item.get("recordInfo") or {}
@ -160,9 +86,18 @@ def _field_value(item: dict[str, Any], key: str) -> str:
s = _custom_prop_value(node.get("customProperties"), key) s = _custom_prop_value(node.get("customProperties"), key)
if s: if s:
return s return s
s = _deep_field_value(node, key) for child in node.values():
if s: if not isinstance(child, dict):
return s continue
s = _scalar_value(child.get(key))
if s:
return s
s = _translate_value(child, key)
if s:
return s
s = _custom_prop_value(child.get("customProperties"), key)
if s:
return s
return "" return ""
@ -620,14 +555,19 @@ class SyncEhrToAdUserJob(BaseJob):
office = _field_translate_or_value(item, "Place") office = _field_translate_or_value(item, "Place")
workplace_text = _field_translate_or_value(item, work_location_text_key) workplace_text = _field_translate_or_value(item, work_location_text_key)
street_address = _field_value(item, street_address_key) street_address = _field_value(item, street_address_key)
if not street_address:
for fallback_key in _EHR_STREET_ADDRESS_FALLBACK_KEYS:
if fallback_key == street_address_key:
continue
street_address = _field_value(item, fallback_key)
if street_address:
break
if verbose_trace: if verbose_trace:
address_candidate_keys = _collect_matching_keys(item, "extgzddxx") if not street_address else []
logger.info( logger.info(
"AD 地址字段解析sam=%s street_address_key=%s streetAddress=%r candidate_keys=%s", "AD 地址字段解析sam=%s street_address_key=%s streetAddress=%r",
sam, sam,
street_address_key, street_address_key,
street_address, street_address,
address_candidate_keys,
) )
company = default_company or _root_org_name(org, org_by_oid) 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_workplace(workplace_text or office, location_mappings)