From d12cc71bd4892820945dab63acaa621f77bf1cac Mon Sep 17 00:00:00 2001 From: Marsway Date: Thu, 30 Apr 2026 15:13:44 +0800 Subject: [PATCH] update --- extensions/sync_ehr_to_ad/job.py | 86 ++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/extensions/sync_ehr_to_ad/job.py b/extensions/sync_ehr_to_ad/job.py index 7598c48..fd93a18 100644 --- a/extensions/sync_ehr_to_ad/job.py +++ b/extensions/sync_ehr_to_ad/job.py @@ -173,6 +173,50 @@ def _proxy_addresses(email: str, sam: str, alias_domain: str | None) -> list[str return out +def _target_values(value: Any) -> list[str]: + if isinstance(value, (list, tuple, set)): + return [str(x).strip() for x in value if x is not None and str(x).strip() != ""] + if value is None or str(value).strip() == "": + return [] + return [str(value).strip()] + + +def _ad_values(attrs: dict[str, Any], attr_name: str) -> list[str]: + raw = attrs.get(attr_name) + if raw is None: + lower_attr = attr_name.lower() + for key, value in attrs.items(): + if str(key).lower() == lower_attr: + raw = value + break + if isinstance(raw, (list, tuple, set)): + return [str(x).strip() for x in raw if x is not None and str(x).strip() != ""] + if raw is None or str(raw).strip() == "": + return [] + return [str(raw).strip()] + + +def _canonical_values(attr_name: str, values: list[str]) -> list[str]: + if attr_name in {"proxyAddresses"}: + return sorted(values) + if attr_name in {"manager", "mail", "sAMAccountName"}: + return sorted([v.lower() for v in values]) + return sorted(values) + + +def _diff_ad_attributes(ad_attrs: dict[str, Any], target_attrs: dict[str, Any]) -> dict[str, Any]: + diff: dict[str, Any] = {} + for attr_name, target_value in target_attrs.items(): + wanted = _target_values(target_value) + if not wanted: + continue + current = _ad_values(ad_attrs, attr_name) + if _canonical_values(attr_name, current) == _canonical_values(attr_name, wanted): + continue + diff[attr_name] = target_value + return diff + + def _location_from_workplace(workplace: str, mappings: dict[str, Any] | None = None) -> dict[str, Any]: text = str(workplace or "").strip() defaults: dict[str, dict[str, Any]] = { @@ -409,10 +453,36 @@ class SyncEhrToAdUserJob(BaseJob): processed = 0 updated = 0 + skipped_unchanged = 0 skipped_missing_sam = 0 skipped_not_found_ad = 0 failed = 0 manager_dn_cache: dict[str, str] = {} + ad_compare_attributes = [ + "sAMAccountName", + "givenName", + "sn", + "title", + "department", + "manager", + "proxyAddresses", + "co", + "c", + "countryCode", + "company", + "displayName", + "mail", + "employeeID", + "employeeType", + "mobile", + "physicalDeliveryOfficeName", + "postalCode", + "st", + "l", + "streetAddress", + ] + if department_code_attr: + ad_compare_attributes.append(department_code_attr) for sam_key, item in users_by_sam.items(): emp = item.get("employeeInfo") or {} @@ -429,7 +499,7 @@ class SyncEhrToAdUserJob(BaseJob): processed += 1 try: - ad_user = ad.find_user(sam) + ad_user = ad.find_user(sam, attributes=ad_compare_attributes) if not ad_user: skipped_not_found_ad += 1 logger.warning("AD 用户不存在,跳过:sAMAccountName=%s", sam) @@ -493,15 +563,22 @@ class SyncEhrToAdUserJob(BaseJob): if department_code_attr and department_code: attributes[department_code_attr] = department_code - changed = ad.modify_user(str(ad_user["dn"]), attributes, dry_run=dry_run) + diff_attributes = _diff_ad_attributes(ad_user.get("attributes") or {}, attributes) + if not diff_attributes: + skipped_unchanged += 1 + if verbose_trace: + logger.info("AD 用户信息一致,跳过更新:sam=%s dn=%s", sam, ad_user["dn"]) + continue + + changed = ad.modify_user(str(ad_user["dn"]), diff_attributes, dry_run=dry_run) if changed: updated += 1 if verbose_trace: logger.info( - "AD 用户同步完成:sam=%s dn=%s attrs=%s", + "AD 用户同步完成:sam=%s dn=%s changed_attrs=%s", sam, ad_user["dn"], - json.dumps({k: v for k, v in attributes.items() if v}, ensure_ascii=False, default=str), + json.dumps({k: v for k, v in diff_attributes.items() if v}, ensure_ascii=False, default=str), ) except Exception as e: # noqa: BLE001 failed += 1 @@ -514,6 +591,7 @@ class SyncEhrToAdUserJob(BaseJob): "ehr_current_users_with_ad_account": len(users_by_sam), "processed": processed, "updated": updated, + "skipped_unchanged": skipped_unchanged, "skipped_missing_sam": skipped_missing_sam, "skipped_not_found_ad": skipped_not_found_ad, "failed": failed,