云计算百科
云计算领域专业知识百科平台

HR人员和组织信息同步AD域服务器实战方法JAVA

HR人员和组织信息同步AD域服务器

    • 前期准备
    • AD域基础知识整理
    • HR同步AD的逻辑
    • 代码结构
    • 配置文件设置
    • 启动类
    • HR组织的Bean
    • HR人员Bean
    • 获取HR人员和组织信息的类
    • AD中处理组织和人员的类
      • 日志配置
    • POM.xml文件
    • 生成EXE文件
    • 服务器定时任务
    • 异常问题注意事项

前期准备

1、开发语言:Java 2、开发框架:无 3、日志框架:logback 4、服务器:windows2016(已部署了AD域,这里不过多介绍) 5、开发工具:idea、launch4j(用于部署服务器定时任务生成exe用) 6、AD域证书(客户端连接AD使用)

AD域基础知识整理

AD域服务器中重要的知识点或属性描述:

  • “CN”(Common Name,常用名),用于指定对象的具体名称
  • “DC”(Domain Component,域组件),用来标识域的各个部分
  • “Description”,可对对象进行详细的描述说明,在这里存放的为组织编码
  • “adminDescription”,用于存放组织id
  • “DistinguishedName”(可分辨名称),是 OU 在 AD 中的唯一标识,它描述了 OU 在域中的完整路径
  • “mobile”,用于记录用户的手机号
  • “department”,用于记录部门的ID
  • “displayName”,用于记录用户的显示名称
  • “info”,用于记录用户的ID
  • “sn”,用于记录用户的姓
  • “givenName”,用于记录用户的名
  • “unicodePwd”,用于记录用户的密码,赋值时用十六进制
  • “userAccountControl”,用于控制用户状态,正常账户为514,禁用账户为514
  • “pwdLastSet”,用于控制用户下次登陆时是否需要更改密码
  • HR同步AD的逻辑

    1、数据准备:将HR中的组织和人员信息建立一个Bean方法 2、连接与认证: ①连接HR系统,可以通过接口,也可通过导入外部jar包的方式(此文章用导入外部jar包的方式获取HR中的信息) ②建立AD的系统连接 3、根据HR的信息处理AD中的信息,先处理组织,再处理人员 4、记录日志并打印

    代码结构

    在这里插入图片描述

    配置文件设置

    记录AD和HR系统的各种信息

    public class AppConfig {
    // SHR Configuration
    public static final String SHR_URL = "HR系统地址";
    public static final String SHR_ORG_SERVICE = "HR系统获取组织服务";
    public static final String SHR_PERSON_SERVICE = "HR系统获取人员服务";

    // AD Configuration
    public static final String AD_URL = "AD域的地址";
    public static final String AD_ADMIN_DN = "";
    public static final String AD_ADMIN_PASSWORD = "管理员密码";
    public static final String AD_INIT_PASSWORD = "初始密码";
    public static final String AD_BASE_DN = "根OU";
    public static final String AD_ARCHIVED_GROUP = "封存人员组";

    // Status codes
    public static final String STATUS_DISABLED = "1";
    public static final String STATUS_ENABLED = "0";

    public static final String PERSON_STATUS_ENABLED = "1";
    public static final String PERSON_STATUS_DISABLED = "0";
    }

    启动类

    import java.util.List;

    public class HrAdSynchronizer {
    /*定义日志对象*/
    private static final Logger logger = LoggerFactory.getLogger(HrAdSynchronizer.class);

    /*定义HR对象*/
    private final ShrService shrService;

    /*定义AD对象*/
    private final AdService adService;

    /**
    * 日志记录方法
    */

    public HrAdSynchronizer() {
    // 确保日志目录存在并打印出实际路径
    String logDir = SyncUtils.ensureDirectoryExists("logs");
    System.out.println("日志目录: " + logDir);

    this.shrService = new ShrService();
    this.adService = new AdService();
    }

    /**
    * 执行方法
    */

    public void synchronize() {
    try {
    logger.info("开始SHR到AD的同步过程");

    // 同步组织结构(包含变更处理)
    /*获取HR中的组织信息*/
    List<ShrOrganization> organizations = shrService.getOrganizations();

    /*打印日志*/
    logger.info("从SHR获取到 {} 个组织", organizations.size());

    /*将HR中的组织信息同步至AD*/
    adService.syncOrganizations(organizations);

    // 同步人员信息(包含变更处理)
    /*获取HR中的人员信息*/
    List<ShrPerson> personnel = shrService.getPersonnel();

    /*打印日志*/
    logger.info("从SHR获取到 {} 个人员", personnel.size());

    /*将HR中的人员信息同步至AD*/
    adService.syncPersonnel(personnel);

    /*打印日志*/
    logger.info("同步过程成功完成");
    } catch (Exception e) {
    logger.error("同步过程发生错误: {}", e.getMessage(), e);
    } finally {
    adService.close();
    logger.info("同步过程结束");
    }
    }

    /**
    * 启动方法
    * @param args
    */

    public static void main(String[] args) {
    /*打印日志,标记功能程序*/
    logger.info("启动HR-AD同步程序");

    /*调用日志文件自动生成的方法,可注释*/
    HrAdSynchronizer synchronizer = new HrAdSynchronizer();

    /*调用执行方法*/
    synchronizer.synchronize();
    }
    }

    HR组织的Bean

    public class ShrOrganization {
    private String fnumber;
    private String name;
    private String easdeptId;
    private String superior;
    private String status;

    // Getters and setters
    public String getFnumber() {
    return fnumber;
    }

    public void setFnumber(String fnumber) {
    this.fnumber = fnumber;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

    public String getEasdeptId() {
    return easdeptId;
    }

    public void setEasdeptId(String easdeptId) {
    this.easdeptId = easdeptId;
    }

    public String getSuperior() {
    return superior;
    }

    public void setSuperior(String superior) {
    this.superior = superior;
    }

    public String getStatus() {
    return status;
    }

    public void setStatus(String status) {
    this.status = status;
    }

    @Override
    public String toString() {
    return "ShrOrganization{" +
    "fnumber='" + fnumber + '\\'' +
    ", name='" + name + '\\'' +
    ", easdeptId='" + easdeptId + '\\'' +
    ", superior='" + superior + '\\'' +
    ", status='" + status + '\\'' +
    '}';
    }
    }

    HR人员Bean

    public class ShrPerson {
    private String empTypeName;
    private String mobile;
    private String orgNumber;
    private String easuserId;
    private String supFnumber;
    private String supname;
    private String superior;
    private String status;
    private String username;
    private String deptId;

    // Getters and setters
    public String getEmpTypeName() {
    return empTypeName;
    }

    public void setEmpTypeName(String empTypeName) {
    this.empTypeName = empTypeName;
    }

    public String getMobile() {
    return mobile;
    }

    public void setMobile(String mobile) {
    this.mobile = mobile;
    }

    public String getOrgNumber() {
    return orgNumber;
    }

    public void setOrgNumber(String orgNumber) {
    this.orgNumber = orgNumber;
    }

    public String getEasuserId() {
    return easuserId;
    }

    public void setEasuserId(String easuserId) {
    this.easuserId = easuserId;
    }

    public String getSupFnumber() {
    return supFnumber;
    }

    public void setSupFnumber(String supFnumber) {
    this.supFnumber = supFnumber;
    }

    public String getSupname() {
    return supname;
    }

    public void setSupname(String supname) {
    this.supname = supname;
    }

    public String getSuperior() {
    return superior;
    }

    public void setSuperior(String superior) {
    this.superior = superior;
    }

    public String getStatus() {
    return status;
    }

    public void setStatus(String status) {
    this.status = status;
    }

    public String getUsername() {
    return username;
    }

    public void setUsername(String username) {
    this.username = username;
    }

    public String getDeptId() {
    return deptId;
    }

    public void setDeptId(String deptId) {
    this.deptId = deptId;
    }

    @Override
    public String toString() {
    return "ShrPerson{" +
    "empTypeName='" + empTypeName + '\\'' +
    ", mobile='" + mobile + '\\'' +
    ", orgNumber='" + orgNumber + '\\'' +
    ", easuserId='" + easuserId + '\\'' +
    ", supFnumber='" + supFnumber + '\\'' +
    ", supname='" + supname + '\\'' +
    ", superior='" + superior + '\\'' +
    ", status='" + status + '\\'' +
    ", username='" + username + '\\'' +
    ", deptId='" + deptId + '\\'' +
    '}';
    }
    }

    获取HR人员和组织信息的类

    import com.shr.api.SHRClient;
    import com.shr.api.Response;
    import com.sync.config.AppConfig;
    import com.sync.model.ShrOrganization;
    import com.sync.model.ShrPerson;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import com.alibaba.fastjson2.JSON;
    import com.alibaba.fastjson2.JSONArray;
    import com.alibaba.fastjson2.JSONObject;

    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;

    public class ShrService {
    private static final Logger logger = LoggerFactory.getLogger(ShrService.class);

    private final SHRClient shrClient;

    public ShrService() {
    logger.info("初始化SHR服务,连接到 {}", AppConfig.SHR_URL);
    this.shrClient = new SHRClient();
    }

    /**
    * 获取SHR中的组织列表
    * @return 返回list
    */

    public List<ShrOrganization> getOrganizations() {
    /*定义一个返回list对象*/
    List<ShrOrganization> organizations = new ArrayList<>();

    try {
    /*记录开始调用SHR组织日志*/
    logger.info("调用SHR组织服务: {}", AppConfig.SHR_ORG_SERVICE);

    /*定义请求参数*/
    Map<String, Object> param = new HashMap<>();

    /*发起请求*/
    Response response = shrClient.executeService(AppConfig.SHR_URL, AppConfig.SHR_ORG_SERVICE, param);

    /*请求失败处理*/
    if (response == null || response.getData() == null) {
    /*记录失败日志*/
    logger.error("从SHR获取组织数据失败,响应为空");

    /*返回失败结果*/
    return organizations;
    }

    /*解析JSON数据*/
    JSONArray orgArray = JSON.parseArray(response.getData().toString());

    /*记录json日志数量*/
    logger.debug("获取到原始组织数据: {} 条记录", orgArray.size());

    int enabledCount = 0;

    /*遍历组织json*/
    for (int i = 0; i < orgArray.size(); i++) {
    /*获取第i个对象*/
    JSONObject orgJson = orgArray.getJSONObject(i);

    /*获取组织状态*/
    String status = orgJson.getString("status");

    /*只处理启用状态(status=0)的组织*/
    if (AppConfig.STATUS_ENABLED.equals(status)) {
    /*定义组织对象*/
    ShrOrganization organization = new ShrOrganization();

    /*组织编码赋值*/
    organization.setFnumber(orgJson.getString("fnumber"));

    /*组织名称赋值*/
    organization.setName(orgJson.getString("name"));

    /*组织id赋值*/
    organization.setEasdeptId(orgJson.getString("easdept_id"));

    /*上级组织部门id赋值*/
    organization.setSuperior(orgJson.getString("superior"));

    /*组织状态赋值*/
    organization.setStatus(status);

    /*加入list中*/
    organizations.add(organization);

    /*记录解析日志*/
    logger.debug("解析启用组织: {}", organization);

    /*计数器+1*/
    enabledCount++;
    } else {
    logger.debug("跳过禁用组织: fnumber={}, name={}",
    orgJson.getString("fnumber"), orgJson.getString("name"));
    }
    }

    /*记录总的处理日志*/
    logger.info("成功解析 {} 个组织,其中启用状态的有 {} 个", orgArray.size(), enabledCount);
    } catch (Exception e) {
    logger.error("从SHR获取组织信息时发生错误: {}", e.getMessage(), e);
    }
    return organizations;
    }

    /**
    * 获取SHR中人员信息
    *
    * @return 返回人员List
    */

    public List<ShrPerson> getPersonnel() {
    /*定义一个List返回对象*/
    List<ShrPerson> personnel = new ArrayList<>();
    try {
    /*记录开始日志*/
    logger.info("调用SHR人员服务: {}", AppConfig.SHR_PERSON_SERVICE);

    /*定义请求参数*/
    Map<String, Object> param = new HashMap<>();

    /*发起请求*/
    Response response = shrClient.executeService(AppConfig.SHR_URL, AppConfig.SHR_PERSON_SERVICE, param);

    /*请求判空*/
    if (response == null || response.getData() == null) {
    /*记录失败日志*/
    logger.error("从SHR获取人员数据失败,响应为空");

    /*返回结果*/
    return personnel;
    }

    /*解析JSON数据*/
    JSONArray personArray = JSON.parseArray(response.getData().toString());

    /*记录人员数量日志*/
    logger.debug("获取到原始人员数据: {} 条记录", personArray.size());

    int enabledCount = 0;

    /*遍历json*/
    for (int i = 0; i < personArray.size(); i++) {
    /*获取json数据*/
    JSONObject personJson = personArray.getJSONObject(i);

    /*定义人员对象*/
    ShrPerson shrPerson = new ShrPerson();

    /*员工类型*/
    shrPerson.setEmpTypeName(personJson.getString("empType_name"));

    /*手机号*/
    shrPerson.setMobile(personJson.getString("mobile"));

    /*部门编码*/
    shrPerson.setOrgNumber(personJson.getString("org_number"));

    /*人员ID*/
    shrPerson.setEasuserId(personJson.getString("easuser_id"));

    /*上级部门编码*/
    shrPerson.setSupFnumber(personJson.getString("supFnumber"));

    /*上级部门名称*/
    shrPerson.setSupname(personJson.getString("supname"));

    /*上级部门ID*/
    shrPerson.setSuperior(personJson.getString("superior"));

    /*人员状态*/
    shrPerson.setStatus(personJson.getString("status"));

    /*人员名称*/
    shrPerson.setUsername(personJson.getString("username"));

    /*人员所在部门ID*/
    shrPerson.setDeptId(personJson.getString("dept_id"));

    /*只添加启用状态的人员*/
    if (AppConfig.PERSON_STATUS_ENABLED.equals(shrPerson.getStatus())) {
    /*加入list*/
    personnel.add(shrPerson);

    /*计数器+1*/
    enabledCount++;

    /*记录人员日志*/
    logger.debug("解析启用人员: {}", shrPerson);
    } else {
    /*记录跳过日志*/
    logger.debug("跳过禁用人员: easuserId={}, username={}, deptId={}",
    personJson.getString("easuser_id"),
    personJson.getString("username"),
    personJson.getString("dept_id"));
    }
    }
    /*记录启动状态人数*/
    logger.info("成功解析 {} 个人员,其中启用状态的有 {} 个", personArray.size(), enabledCount);
    } catch (Exception e) {
    logger.error("从SHR获取人员信息时发生错误: {}", e.getMessage(), e);
    }
    return personnel;
    }
    }

    AD中处理组织和人员的类

    import com.sync.config.AppConfig;
    import com.sync.model.ShrOrganization;
    import com.sync.model.ShrPerson;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;

    import javax.naming.Context;
    import javax.naming.NamingException;
    import javax.naming.directory.*;
    import javax.naming.ldap.Control;
    import javax.naming.ldap.InitialLdapContext;
    import javax.naming.ldap.LdapContext;
    import javax.naming.NamingEnumeration;
    import javax.naming.ldap.PagedResultsControl;
    import java.io.IOException;
    import java.util.*;

    public class AdService {
    /*日志对象*/
    private static final Logger logger = LoggerFactory.getLogger(AdService.class);

    /*特殊组织编码,这些组织需要跳过处理*/
    private static final String SPECIAL_ORG_CODE = "999";

    /*记录AD的连接*/
    private LdapContext ldapContext;

    /*缓存AD中的组织信息,用于变更检测*/
    private Map<String, String> orgIdToDnMap = new HashMap<>();

    /*同步到AD的组织对象*/
    private Map<String, Attributes> orgDnToAttrsMap = new HashMap<>();

    /*存储特殊组织的DN,这些组织不会被处理*/
    private Set<String> specialOrgDns = new HashSet<>();

    /*增加组织编码到组织名称的映射缓存*/
    private Map<String, String> orgNumberToNameMap = new HashMap<>();

    /*添加 DN 到组织名称的映射*/
    private Map<String, String> dnToOuNameMap = new HashMap<>();

    /*构造方法*/
    public AdService() {
    initContext();
    // 初始化时加载现有组织结构
    loadExistingOrganizations();
    }

    /*AD的连接初始化*/
    private void initContext() {
    try {
    logger.info("初始化AD连接,URL: {}", AppConfig.AD_URL);
    Hashtable<String, String> env = new Hashtable<>();
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
    env.put(Context.PROVIDER_URL, AppConfig.AD_URL);
    env.put(Context.SECURITY_AUTHENTICATION, "simple");
    env.put(Context.SECURITY_PRINCIPAL, AppConfig.AD_ADMIN_DN);
    env.put(Context.SECURITY_CREDENTIALS, AppConfig.AD_ADMIN_PASSWORD);
    env.put(Context.SECURITY_PROTOCOL, "ssl");

    ldapContext = new InitialLdapContext(env, null);
    logger.info("成功连接到Active Directory");
    } catch (NamingException e) {
    logger.error("连接Active Directory失败: {}", e.getMessage(), e);
    }
    }

    /**
    * 加载AD中已存在的组织结构到缓存
    */

    private void loadExistingOrganizations() {
    try {
    logger.info("加载AD中现有组织结构");

    /*定义搜索控制器*/
    SearchControls searchControls = new SearchControls();

    /*设置搜索深度*/
    searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);

    /*设置查询内容*/
    String[] returnedAtts = {"distinguishedName", "ou", "adminDescription", "description"};

    /*设置查询对象*/
    searchControls.setReturningAttributes(returnedAtts);

    /*设置过滤条件*/
    String searchFilter = "(objectClass=organizationalUnit)";

    /*执行查询*/
    NamingEnumeration<SearchResult> results = ldapContext.search(AppConfig.AD_BASE_DN, searchFilter, searchControls);

    int count = 0;
    int specialCount = 0;
    int noAdminDescCount = 0;

    /*遍历查询结果*/
    while (results.hasMoreElements()) {
    /*获取查询结果*/
    SearchResult result = results.next();

    /*获取dn*/
    String dn = result.getNameInNamespace();

    /*获取其余结果*/
    Attributes attrs = result.getAttributes();

    // 保存 DN 和 OU 名称的映射,以便后续使用
    if (attrs.get("ou") != null) {
    /*获取ou*/
    String ouName = attrs.get("ou").get().toString();

    /*将OU放入缓存*/
    dnToOuNameMap.put(dn, ouName);
    }

    // 检查是否是特殊组织
    boolean isSpecial = false;
    if (attrs.get("description") != null) {
    String description = attrs.get("description").get().toString();
    if (SPECIAL_ORG_CODE.equals(description)) {
    specialOrgDns.add(dn);
    isSpecial = true;
    specialCount++;
    logger.debug("识别到特殊组织(编码999): {}", dn);
    }
    }

    if (attrs.get("ou") != null) {
    String ouName = attrs.get("ou").get().toString();
    if (AppConfig.AD_ARCHIVED_GROUP.equals(ouName)) {
    specialOrgDns.add(dn);
    isSpecial = true;
    specialCount++;
    logger.debug("识别到特殊组织(封存人员组): {}", dn);
    }
    }

    // 如果不是特殊组织且有adminDescription,则添加到正常组织映射
    if (!isSpecial && attrs.get("adminDescription") != null) {
    String orgId = attrs.get("adminDescription").get().toString();
    orgIdToDnMap.put(orgId, dn);
    orgDnToAttrsMap.put(dn, attrs);
    count++;
    } else if (!isSpecial) {
    // 记录缺少adminDescription的组织
    noAdminDescCount++;
    orgDnToAttrsMap.put(dn, attrs);
    }
    }

    logger.info("已加载 {} 个组织到缓存, {} 个特殊组织被排除, {} 个组织缺少adminDescription",
    count, specialCount, noAdminDescCount);
    } catch (NamingException e) {
    logger.error("加载组织结构时发生错误: {}", e.getMessage(), e);
    }
    }

    /**
    * 同步组织到AD,处理变更情况
    */

    public void syncOrganizations(List<ShrOrganization> organizations) {
    logger.info("开始同步组织到AD,共 {} 个组织", organizations.size());

    try {
    // 首先构建组织编码到名称的映射,用于后续定位上级组织
    buildOrgNumberToNameMap(organizations);

    // 记录当前同步中处理过的组织ID,用于后续检测删除操作
    Set<String> processedOrgIds = new HashSet<>();

    /**
    *先处理缺少adminDescription但distinguishedName匹配的组织,执行一次后,默认先不执行
    */

    handleOrganizationsWithoutAdminDescription(organizations);

    // 按上级组织ID排序,确保先处理上级组织
    List<ShrOrganization> sortedOrgs = sortOrganizationsByHierarchy(organizations);

    for (ShrOrganization org : sortedOrgs) {
    // 跳过特殊组织编码
    if (SPECIAL_ORG_CODE.equals(org.getFnumber())) {
    logger.info("跳过特殊组织编码 {}: {}", org.getFnumber(), org.getName());
    continue;
    }

    // 跳过封存人员组
    if (AppConfig.AD_ARCHIVED_GROUP.equals(org.getName())) {
    logger.info("跳过封存人员组: {}", org.getName());
    continue;
    }

    String orgId = org.getEasdeptId();
    processedOrgIds.add(orgId);

    // 组织在AD中存在的DN
    String existingDn = orgIdToDnMap.get(orgId);
    //existingDn="OU=测试test,OU=集团数字化本部,OU=集团数字化部,OU=多维联合集团股份有限公司,OU=多维联合集团,OU=Domain Controllers,DC=duowei,DC=net,DC=cn";
    // 根据上级组织确定目标DN
    String targetDn = getTargetDnWithParent(org);

    //targetDn = "OU=测试test,OU=集团数字化技术部,OU=集团数字化部,OU=多维联合集团股份有限公司,OU=多维联合集团,OU=Domain Controllers,DC=duowei,DC=net,DC=cn";

    // 检查组织状态
    if (AppConfig.STATUS_DISABLED.equals(org.getStatus())) {
    if (existingDn != null) {
    logger.info("组织 {} (ID: {}) 在SHR中被禁用,标记为禁用", org.getName(), orgId);
    markOrganizationAsDisabled(existingDn, org);
    }
    continue;
    }

    // 处理三种情况:新建、更新属性、重命名(移动)
    if (existingDn == null) {
    System.err.println(existingDn);
    // 新建组织
    createNewOrganization(targetDn, org);
    } else if (!existingDn.equals(targetDn)) {
    System.err.println(existingDn);
    // 组织名称或层级变更,需要重命名/移动
    renameOrganization(existingDn, targetDn, org);
    } else {
    // 组织名称和层级未变,但可能需要更新其他属性
    updateOrganizationAttributes(existingDn, org);
    }
    }

    // 处理在SHR中不存在但在AD中存在的组织(删除或禁用)
    handleDeletedOrganizations(processedOrgIds);

    logger.info("组织同步完成");
    } catch (Exception e) {
    logger.error("同步组织到AD时发生错误: {}", e.getMessage(), e);
    }
    }

    /**
    * 处理缺少adminDescription但distinguishedName匹配的组织
    */

    private void handleOrganizationsWithoutAdminDescription(List<ShrOrganization> organizations) {
    logger.info("检查缺少adminDescription但DN匹配的组织");
    int fixedCount = 0;

    for (ShrOrganization org : organizations) {
    String targetDn = getTargetDnWithParent(org);
    String orgId = org.getEasdeptId();

    // 如果organizationId不在映射中,但DN存在于AD中
    if (!orgIdToDnMap.containsKey(orgId) && orgDnToAttrsMap.containsKey(targetDn)) {
    logger.info("发现缺少adminDescription的组织,DN: {}, 组织ID: {}", targetDn, orgId);

    try {
    // 添加adminDescription属性
    ModificationItem[] mods = new ModificationItem[1];
    mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
    new BasicAttribute("adminDescription", orgId));

    ldapContext.modifyAttributes(targetDn, mods);

    // 更新缓存
    orgIdToDnMap.put(orgId, targetDn);
    Attributes attrs = orgDnToAttrsMap.get(targetDn);
    attrs.put("adminDescription", orgId);

    logger.info("成功添加adminDescription属性到组织: {}", targetDn);
    fixedCount++;
    } catch (NamingException e) {
    logger.error("添加adminDescription属性时发生错误: {}", e.getMessage(), e);
    }
    }
    }

    if (fixedCount > 0) {
    logger.info("共修复 {} 个缺少adminDescription的组织", fixedCount);
    }
    }

    /**
    * 构建组织编码到名称的映射
    */

    private void buildOrgNumberToNameMap(List<ShrOrganization> organizations) {
    orgNumberToNameMap.clear();
    for (ShrOrganization org : organizations) {
    if (org.getFnumber() != null && org.getName() != null) {
    orgNumberToNameMap.put(org.getEasdeptId(), org.getName());
    }
    }
    logger.debug("构建了 {} 个组织编码到名称的映射", orgNumberToNameMap.size());
    }

    /**
    * 按层级关系排序组织,确保先处理上级组织
    */

    private List<ShrOrganization> sortOrganizationsByHierarchy(List<ShrOrganization> organizations) {
    List<ShrOrganization> sorted = new ArrayList<>(organizations);

    // 首先处理没有上级的组织,然后处理有上级的组织
    sorted.sort((o1, o2) -> {
    boolean o1HasParent = o1.getSuperior() != null && !o1.getSuperior().isEmpty();
    boolean o2HasParent = o2.getSuperior() != null && !o2.getSuperior().isEmpty();

    if (!o1HasParent && o2HasParent) return 1;
    if (o1HasParent && !o2HasParent) return 1;
    return 0;
    });

    return sorted;
    }

    /**
    * 根据上级组织获取目标DN
    */

    private String getTargetDnWithParent(ShrOrganization org) {
    // 额外添加查找逻辑
    String dn = findExistingDnByOuName(org.getName());
    if (dn != null) {
    return dn;
    }

    // 原有的逻辑作为后备
    if (org.getSuperior() == null || org.getSuperior().isEmpty()) {
    // 没有上级组织,直接放在基础DN下
    return "OU=" + org.getName() + "," + AppConfig.AD_BASE_DN;
    }

    // 查找上级组织名称
    String parentNumber = org.getSuperior();
    String parentName = orgNumberToNameMap.get(parentNumber);

    if (parentName == null) {
    logger.warn("找不到上级组织 {},组织 {} 将直接放在基础DN下", parentNumber, org.getName());
    return "OU=" + org.getName() + "," + AppConfig.AD_BASE_DN;
    }

    // 检查上级组织是否在AD中存在
    String parentDN = findOrganizationDnByName(parentName);

    if (parentDN != null) {
    // 上级组织存在,将当前组织放在上级组织下
    return "OU=" + org.getName() + "," + parentDN;
    } else {
    logger.warn("上级组织 {} 在AD中不存在,组织 {} 将直接放在基础DN下", parentName, org.getName());
    return "OU=" + org.getName() + "," + AppConfig.AD_BASE_DN;
    }
    }

    /**
    * 根据组织名称查找DN
    */

    private String findOrganizationDnByName(String orgName) {
    try {
    SearchControls searchControls = new SearchControls();
    searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
    searchControls.setReturningAttributes(new String[]{"distinguishedName"});

    String searchFilter = "(&(objectClass=organizationalUnit)(ou=" + orgName + "))";
    NamingEnumeration<SearchResult> results = ldapContext.search(AppConfig.AD_BASE_DN, searchFilter, searchControls);

    if (results.hasMoreElements()) {
    SearchResult result = results.next();
    return result.getNameInNamespace();
    }
    } catch (NamingException e) {
    logger.error("查找组织 {} 时发生错误: {}", orgName, e.getMessage(), e);
    }

    return null;
    }

    /**
    * 根据OU名称查找可能存在的DN
    */

    private String findExistingDnByOuName(String ouName) {
    for (Map.Entry<String, String> entry : dnToOuNameMap.entrySet()) {
    if (ouName.equals(entry.getValue())) {
    return entry.getKey();
    }
    }
    return null;
    }

    /**
    * 创建新组织(不包含封存)
    */

    private void createNewOrganization(String orgDn, ShrOrganization org) throws NamingException {
    logger.info("创建新组织: {} (ID: {})", org.getName(), org.getFnumber());

    Attributes attrs = new BasicAttributes();
    Attribute objClass = new BasicAttribute("objectClass");
    objClass.add("top");
    objClass.add("organizationalUnit");
    attrs.put(objClass);

    attrs.put("ou", org.getName());
    attrs.put("description", org.getFnumber());
    attrs.put("adminDescription", org.getEasdeptId());

    ldapContext.createSubcontext(orgDn, attrs);

    // 更新缓存
    orgIdToDnMap.put(org.getFnumber(), orgDn);
    orgDnToAttrsMap.put(orgDn, attrs);

    logger.info("成功创建组织: {}", org.getName());
    }

    /**
    * 创建封存组织
    * @param orgDn
    * @throws NamingException
    */

    private void createFCNewOrganization(String orgDn) throws NamingException {
    logger.info("创建封存人员组");

    Attributes attrs = new BasicAttributes();
    Attribute objClass = new BasicAttribute("objectClass");
    objClass.add("top");
    objClass.add("organizationalUnit");
    attrs.put(objClass);

    attrs.put("ou", "封存人员组");
    attrs.put("description", "000");
    attrs.put("adminDescription", "000");

    ldapContext.createSubcontext(orgDn, attrs);

    logger.info("成功创建封存人员组");
    }

    /**
    * 更新组织属性
    */

    private void updateOrganizationAttributes(String orgDn, ShrOrganization org) throws NamingException {
    logger.info("更新组织属性: {} (ID: {})", org.getName(), org.getFnumber());

    Attributes existingAttrs = orgDnToAttrsMap.get(orgDn);
    boolean hasChanges = false;

    List<ModificationItem> mods = new ArrayList<>();

    // 检查description是否需要更新(只存放组织编码)
    String currentDesc = existingAttrs.get("description") != null ?
    existingAttrs.get("description").get().toString() : null;
    String newDesc = org.getFnumber();

    if (currentDesc == null || !currentDesc.equals(newDesc)) {
    mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
    new BasicAttribute("description", newDesc)));
    hasChanges = true;
    }

    // 检查adminDescription是否需要更新
    String currentAdminDesc = existingAttrs.get("adminDescription") != null ?
    existingAttrs.get("adminDescription").get().toString() : null;

    if (currentAdminDesc == null || !currentAdminDesc.equals(org.getEasdeptId())) {
    mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
    new BasicAttribute("adminDescription", org.getEasdeptId())));
    hasChanges = true;
    }

    if (hasChanges) {
    ldapContext.modifyAttributes(orgDn, mods.toArray(new ModificationItem[0]));
    logger.info("已更新组织 {} 的属性", org.getName());

    // 更新缓存
    orgDnToAttrsMap.put(orgDn, ldapContext.getAttributes(orgDn));
    } else {
    logger.debug("组织 {} 的属性无需更新", org.getName());
    }
    }

    /**
    * 重命名/移动组织
    */

    private void renameOrganization(String oldDn, String newDn, ShrOrganization org) throws NamingException {
    logger.info("重命名/移动组织: 从 {} 到 {}", oldDn, newDn);

    // 执行重命名
    ldapContext.rename(oldDn, newDn);

    // 更新缓存
    orgIdToDnMap.put(org.getEasdeptId(), newDn);
    orgDnToAttrsMap.remove(oldDn);
    orgDnToAttrsMap.put(newDn, ldapContext.getAttributes(newDn));

    // 重命名后可能需要更新属性
    updateOrganizationAttributes(newDn, org);

    // 更新缓存
    orgIdToDnMap.put(org.getEasdeptId(), newDn);
    orgDnToAttrsMap.remove(oldDn);
    orgDnToAttrsMap.put(newDn, ldapContext.getAttributes(newDn));

    logger.info("成功重命名/移动组织 {}", org.getName());
    }

    /**
    * 标记组织为禁用
    */

    private void markOrganizationAsDisabled(String orgDn, ShrOrganization org) throws NamingException {
    logger.info("标记组织为禁用: {} (ID: {})", org.getName(), org.getFnumber());

    Attributes existingAttrs = orgDnToAttrsMap.get(orgDn);

    // 在description前添加"[已禁用]"标记,但保留组织编码
    String currentDesc = existingAttrs.get("description") != null ?
    existingAttrs.get("description").get().toString() : org.getFnumber();

    if (!currentDesc.startsWith("[已禁用]")) {
    ModificationItem[] mods = new ModificationItem[1];
    mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
    new BasicAttribute("description", "[已禁用] " + org.getFnumber()));

    ldapContext.modifyAttributes(orgDn, mods);

    // 更新缓存
    orgDnToAttrsMap.put(orgDn, ldapContext.getAttributes(orgDn));
    }

    logger.info("组织 {} 已标记为禁用", org.getName());
    }

    /**
    * 处理已删除的组织
    */

    private void handleDeletedOrganizations(Set<String> processedOrgIds) throws NamingException {
    logger.info("处理在SHR中不存在的组织");

    for (String orgId : orgIdToDnMap.keySet()) {
    if (!processedOrgIds.contains(orgId)) {
    String orgDn = orgIdToDnMap.get(orgId);

    // 跳过特殊组织
    if (specialOrgDns.contains(orgDn)) {
    logger.info("跳过特殊组织的删除处理: {}", orgDn);
    continue;
    }

    // 获取现有属性
    Attributes attrs = orgDnToAttrsMap.get(orgDn);
    String description = attrs.get("description") != null ?
    attrs.get("description").get().toString() : "";

    // 如果是特殊编码,跳过
    if (SPECIAL_ORG_CODE.equals(description)) {
    logger.info("跳过特殊编码组织的删除处理: {}", orgDn);
    continue;
    }

    // 如果描述中没有已删除标记,添加标记
    if (!description.startsWith("[已删除]")) {
    ModificationItem[] mods = new ModificationItem[1];
    mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
    new BasicAttribute("description", "[已删除] " + description));

    ldapContext.modifyAttributes(orgDn, mods);
    logger.info("标记组织为已删除: {}", orgDn);

    // 更新缓存
    orgDnToAttrsMap.put(orgDn, ldapContext.getAttributes(orgDn));
    }
    }
    }
    }

    /**
    * 同步人员到AD,处理各种变更情况
    */

    public void syncPersonnel(List<ShrPerson> personnel) {
    logger.info("开始同步人员到AD,共 {} 个人员", personnel.size());

    try {
    // 确保封存组存在
    String archiveGroupDN = "OU=" + AppConfig.AD_ARCHIVED_GROUP + "," + AppConfig.AD_BASE_DN;
    if (!checkIfEntryExists(archiveGroupDN)) {
    logger.info("封存人员组不存在,开始创建");
    createFCNewOrganization(archiveGroupDN);
    }

    // 加载AD中现有用户
    Map<String, UserAdInfo> existingUsers = loadExistingUsers();
    logger.info("已加载 {} 个AD用户到缓存", existingUsers.size());

    // 记录处理过的用户ID,用于后续检测删除操作
    Set<String> processedUserIds = new HashSet<>();
    int createdCount = 0;
    int movedCount = 0;
    int updatedCount = 0;
    int disabledCount = 0;
    int skippedCount = 0;

    // 同步用户
    for (ShrPerson person : personnel) {
    try {
    ///*测试*/
    //if(!person.getUsername().equals("宋汝东")){
    // continue;
    //}

    // 1. 基本检查
    if (person.getEasuserId() == null || person.getEasuserId().isEmpty()) {
    logger.warn("跳过无ID的用户: {}", person);
    skippedCount++;
    continue;
    }

    if (person.getUsername() == null || person.getUsername().isEmpty()) {
    logger.warn("跳过无用户名的用户: {}", person.getEasuserId());
    skippedCount++;
    continue;
    }

    // 2. 检查员工类型
    if (!isValidEmployeeType(person.getEmpTypeName())) {
    logger.debug("跳过非目标类型员工: {} (类型: {})",
    person.getUsername(), person.getEmpTypeName());
    skippedCount++;
    continue;
    }

    String userId = person.getEasuserId();
    processedUserIds.add(userId);

    // 3. 员工在AD中的信息
    UserAdInfo userInfo = existingUsers.get(userId);
    boolean exists = (userInfo != null);

    // 4. 处理禁用用户
    if (AppConfig.PERSON_STATUS_DISABLED.equals(person.getStatus())) {
    if (exists) {
    logger.info("用户 {} 在SHR中被禁用,移至封存组并禁用", person.getUsername());
    disableAndArchiveUser(userInfo.getDn(), archiveGroupDN, person);
    disabledCount++;
    }
    continue;
    }

    // 5. 确定用户所属组织DN
    String orgDN = findOrgDnByDeptId(person.getDeptId());
    if (orgDN == null) {
    logger.warn("找不到用户 {} 所属组织(deptId={}), 将使用默认组织",
    person.getUsername(), person.getDeptId());
    orgDN = AppConfig.AD_BASE_DN;
    }

    // 6. 生成目标DN – 使用用户名而不是ID
    String targetUserDN = "CN=" + person.getUsername() + "," + orgDN;

    //if(person.getUsername().equals("田振强")){
    // System.out.println(111111);
    //}

    // 7. 处理不同情况
    if (!exists) {
    //System.err.println(person.getUsername());
    // 用户不存在 – 新建用户
    createNewUser(targetUserDN, person);
    createdCount++;
    } else if (!userInfo.getDn().equals(targetUserDN)) {
    //System.err.println(person.getUsername());
    // 用户存在但DN不同 – 移动用户
    moveUser(userInfo.getDn(), targetUserDN, person);
    movedCount++;
    } else {
    //System.err.println(person.getUsername());
    // 用户存在且DN一致 – 更新属性
    updateUserAttributes(userInfo.getDn(), person);
    updatedCount++;
    }

    } catch (Exception e) {
    logger.error("处理用户 {} 时发生错误: {}", person.getUsername(), e.getMessage(), e);
    }
    }

    // 8. 处理已删除的用户
    int deletedCount = handleDeletedUsers(existingUsers, processedUserIds, archiveGroupDN);

    logger.info("人员同步完成 – 新建: {}, 移动: {}, 更新: {}, 禁用: {}, 删除: {}, 跳过: {}",
    createdCount, movedCount, updatedCount, disabledCount, deletedCount, skippedCount);

    //logger.info("人员同步完成 – 新建: {}, 移动: {}, 更新: {}, 禁用: {}, 删除: {}, 跳过: {}",
    // createdCount, movedCount, updatedCount, disabledCount, skippedCount);
    } catch (NamingException e) {
    logger.error("同步人员到AD时发生错误: {}", e.getMessage(), e);
    } catch (IOException e) {
    throw new RuntimeException(e);
    }
    }

    /**
    * 判断是否为有效的员工类型
    */

    private boolean isValidEmployeeType(String empTypeName) {
    if (empTypeName == null) return false;

    // 只处理正式员工、试用员工、实习的人员
    return empTypeName.contains("正式") ||
    empTypeName.contains("试用") ||
    empTypeName.contains("实习");
    }

    /**
    * 加载AD中现有用户到缓存
    */

    private Map<String, UserAdInfo> loadExistingUsers() throws NamingException, IOException {
    Map<String, UserAdInfo> userMap = new HashMap<>();

    SearchControls searchControls = new SearchControls();
    searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
    String[] returnedAtts = {"distinguishedName", "info", "userAccountControl", "cn"};
    searchControls.setReturningAttributes(returnedAtts);

    String searchFilter = "(&(objectClass=user))";
    ldapContext.setRequestControls(new Control[]{new PagedResultsControl(10000, Control.NONCRITICAL)});

    NamingEnumeration<SearchResult> results = ldapContext.search(AppConfig.AD_BASE_DN, searchFilter, searchControls);

    while (results.hasMoreElements()) {
    SearchResult result = results.next();
    String dn = result.getNameInNamespace();
    Attributes attrs = result.getAttributes();

    // 使用info属性(对应easuserId)作为用户ID
    if (attrs.get("info") != null) {
    String userId = attrs.get("info").get().toString();

    // 获取用户账户控制属性,判断是否禁用
    boolean disabled = false;
    if (attrs.get("userAccountControl") != null) {
    int uac = Integer.parseInt(attrs.get("userAccountControl").get().toString());
    disabled = (uac & 2) != 0; // 账户禁用标志是第2位
    }

    // 获取用户显示名称
    String displayName = "";
    if (attrs.get("cn") != null) {
    displayName = attrs.get("cn").get().toString();
    }

    userMap.put(userId, new UserAdInfo(userId, dn, disabled, displayName));
    }
    }

    //System.err.println(personCount);

    return userMap;
    }

    /**
    * 创建新用户
    */

    private void createNewUser(String userDN, ShrPerson person) throws NamingException {
    logger.info("创建新用户: {} (ID: {})", person.getUsername(), person.getEasuserId());

    Attributes attrs = new BasicAttributes();
    Attribute objClass = new BasicAttribute("objectClass");
    objClass.add("top");
    objClass.add("person");
    objClass.add("organizationalPerson");
    objClass.add("user");
    attrs.put(objClass);

    // CN已经包含在DN中,使用用户名
    attrs.put("cn", person.getUsername());

    // 使用手机号作为登录名
    if (person.getMobile() != null && !person.getMobile().isEmpty()) {
    attrs.put("sAMAccountName", person.getMobile());
    attrs.put("userPrincipalName", person.getMobile() + "@duowei.net.cn");
    } else {
    // 如果没有手机号,回退到使用用户ID
    logger.warn("用户 {} 没有手机号,将使用ID作为登录名", person.getUsername());
    attrs.put("sAMAccountName", person.getEasuserId());
    attrs.put("userPrincipalName", person.getEasuserId() + "@duowei.net.cn");
    }

    // 设置显示名称
    attrs.put("displayName", person.getUsername());

    // 将easuserId存入info属性
    attrs.put("info", person.getEasuserId());

    // 设置姓和名
    // 假设中文名格式为"姓+名",取第一个字为姓,其余为名
    if (person.getUsername() != null && !person.getUsername().isEmpty()) {
    String fullName = person.getUsername();
    if (fullName.length() > 1) {
    // 取第一个字为姓
    String lastName = fullName.substring(0, 1);
    // 取剩余部分为名
    String firstName = fullName.substring(1);
    attrs.put("sn", lastName);
    attrs.put("givenName", firstName);
    } else {
    // 如果只有一个字,则全部作为姓
    attrs.put("sn", fullName);
    }
    }

    // 其他属性
    if (person.getMobile() != null) {
    attrs.put("mobile", person.getMobile());
    }

    if (person.getDeptId() != null) {
    attrs.put("department", person.getDeptId());
    }

    // 设置密码
    byte[] unicodePwd = generatePassword(AppConfig.AD_INIT_PASSWORD);
    attrs.put(new BasicAttribute("unicodePwd", unicodePwd));

    // 用户控制标志: 正常账户 + 密码不过期
    int userAccountControl = 512 | 65536;
    attrs.put(new BasicAttribute("userAccountControl", String.valueOf(userAccountControl)));

    // 要求下次登录更改密码
    attrs.put(new BasicAttribute("pwdLastSet", "0"));

    // 创建用户
    ldapContext.createSubcontext(userDN, attrs);
    }

    /**
    * 移动用户到新位置
    */

    private void moveUser(String currentDN, String targetDN, ShrPerson person) throws NamingException {
    logger.info("移动用户: {} 从 {} 到 {}", person.getUsername(), currentDN, targetDN);

    try {
    // 执行重命名操作移动用户
    ldapContext.rename(currentDN, targetDN);

    // 移动后更新属性
    updateUserAttributes(targetDN, person);
    } catch (NamingException e) {
    logger.error("移动用户 {} 时发生错误: {}", person.getUsername(), e.getMessage());
    throw e;
    }
    }

    /**
    * 更新用户属性
    */

    private void updateUserAttributes(String userDN, ShrPerson person) throws NamingException {
    logger.debug("更新用户属性: {}", person.getUsername());

    List<ModificationItem> mods = new ArrayList<>();

    // 更新手机号
    if (person.getMobile() != null) {
    mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
    new BasicAttribute("mobile", person.getMobile())));
    }

    // 更新部门ID
    if (person.getDeptId() != null) {
    mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
    new BasicAttribute("department", person.getDeptId())));
    }

    /*更新登录名为手机号*/
    if(person.getMobile() != null){
    mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
    new BasicAttribute("sAMAccountName", person.getMobile())));
    }

    // 更新info属性(easuserId)
    mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
    new BasicAttribute("info", person.getEasuserId())));

    // 确保账户处于启用状态
    mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
    new BasicAttribute("userAccountControl", "512")));

    // 应用修改
    if (!mods.isEmpty()) {
    ModificationItem[] modsArray = mods.toArray(new ModificationItem[0]);
    ldapContext.modifyAttributes(userDN, modsArray);
    }
    }

    /**
    * 禁用用户并移动到归档组
    */

    private void disableAndArchiveUser(String userDN, String archiveGroupDN, ShrPerson person) throws NamingException {
    logger.info("禁用并归档用户: {}", person.getUsername());

    try {
    // 首先禁用用户
    ModificationItem[] disableMods = new ModificationItem[1];
    disableMods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
    new BasicAttribute("userAccountControl", "514")); // 514 = 禁用账户

    ldapContext.modifyAttributes(userDN, disableMods);

    // 然后移动到归档组
    String userName = person.getUsername();
    String newDN = "CN=" + userName + "," + archiveGroupDN;

    ldapContext.rename(userDN, newDN);
    } catch (NamingException e) {
    logger.error("禁用并归档用户 {} 时发生错误: {}", person.getUsername(), e.getMessage());
    throw e;
    }
    }

    /**
    * 处理已删除的用户
    */

    private int handleDeletedUsers(Map<String, UserAdInfo> existingUsers, Set<String> processedUserIds,
    String archiveGroupDN) throws NamingException {
    logger.info("处理已删除用户");
    int count = 0;

    for (UserAdInfo userInfo : existingUsers.values()) {
    String userId = userInfo.getUserId();

    // 如果用户未在当前处理列表中,且不在归档组,则归档
    if (!processedUserIds.contains(userId) && !isInArchiveGroup(userInfo.getDn(), archiveGroupDN)) {
    logger.info("用户ID {} 在SHR中不存在,移至封存组并禁用", userId);

    try {
    disableUser(userInfo.getDn());
    moveUserToArchiveGroup(userInfo.getDn(), archiveGroupDN);
    count++;
    } catch (NamingException e) {
    logger.error("处理已删除用户 {} 时发生错误: {}", userId, e.getMessage());
    }
    }
    }

    return count;
    }

    /**
    * 检查用户是否已在归档组中
    */

    private boolean isInArchiveGroup(String userDN, String archiveGroupDN) {
    return userDN.endsWith(archiveGroupDN);
    }

    /**
    * 用户AD信息类
    */

    private static class UserAdInfo {
    private final String userId;
    private final String dn;
    private final boolean disabled;
    private final String displayName;

    public UserAdInfo(String userId, String dn, boolean disabled, String displayName) {
    this.userId = userId;
    this.dn = dn;
    this.disabled = disabled;
    this.displayName = displayName;
    }

    public String getUserId() {
    return userId;
    }

    public String getDn() {
    return dn;
    }

    public boolean isDisabled() {
    return disabled;
    }

    public String getDisplayName() {
    return displayName;
    }
    }

    public void close() {
    try {
    if (ldapContext != null) {
    ldapContext.close();
    logger.info("关闭LDAP连接");
    }
    } catch (NamingException e) {
    logger.error("关闭LDAP连接时发生错误: {}", e.getMessage(), e);
    }
    }

    /**
    * 根据部门ID查找组织DN
    */

    private String findOrgDnByDeptId(String deptId) {
    if (deptId == null || deptId.isEmpty()) {
    return null;
    }

    return orgIdToDnMap.get(deptId);
    }

    /**
    * 从组织 DN 中提取组织名称
    */

    private String getOrgNameFromDN(String dn) {
    if (dn == null || dn.isEmpty()) {
    return "未知组织";
    }

    try {
    // DN 格式通常是 "OU=组织名称,其他部分"
    // 提取第一个 OU= 后面的内容,直到下一个逗号
    if (dn.contains("OU=")) {
    int start = dn.indexOf("OU=") + 3; // OU= 后面的位置
    int end = dn.indexOf(",", start);
    if (end > start) {
    return dn.substring(start, end);
    } else {
    return dn.substring(start);
    }
    }

    // 如果没有找到 OU=,尝试从 dnToOuNameMap 获取
    if (dnToOuNameMap.containsKey(dn)) {
    return dnToOuNameMap.get(dn);
    }
    } catch (Exception e) {
    logger.warn("无法从DN提取组织名称: {}", dn);
    }

    return "未知组织";
    }

    /**
    * 检查指定DN的条目是否存在
    */

    private boolean checkIfEntryExists(String dn) {
    try {
    ldapContext.lookup(dn);
    return true;
    } catch (NamingException e) {
    return false;
    }
    }

    /**
    * 生成AD密码
    * AD密码需要以特定格式提供,使用Unicode编码
    */

    private byte[] generatePassword(String password) {
    // 将密码转换为AD要求的Unicode字节格式
    String quotedPassword = "\\"" + password + "\\"";
    char[] unicodePwd = quotedPassword.toCharArray();
    byte[] pwdBytes = new byte[unicodePwd.length * 2];

    // 转换为Unicode格式
    for (int i = 0; i < unicodePwd.length; i++) {
    pwdBytes[i * 2] = (byte) (unicodePwd[i] & 0xff);
    pwdBytes[i * 2 + 1] = (byte) (unicodePwd[i] >> 8);
    }

    return pwdBytes;
    }

    /**
    * 禁用用户账户
    */

    private void disableUser(String userDN) throws NamingException {
    logger.info("禁用用户: {}", userDN);

    // 用户账户控制: 禁用账户 (514)
    ModificationItem[] mods = new ModificationItem[1];
    mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
    new BasicAttribute("userAccountControl", "514"));

    ldapContext.modifyAttributes(userDN, mods);
    }

    /**
    * 将用户移动到封存组
    */

    private void moveUserToArchiveGroup(String userDN, String archiveGroupDN) throws NamingException {
    logger.info("移动用户到封存组: {} -> {}", userDN, archiveGroupDN);

    // 获取用户DN中的CN部分
    String cn = "";
    if (userDN.startsWith("CN=")) {
    int endIndex = userDN.indexOf(',');
    if (endIndex > 0) {
    cn = userDN.substring(0, endIndex);
    } else {
    cn = userDN;
    }
    } else {
    // 如果不是以CN=开头,使用整个DN
    cn = "CN=" + getDnFirstComponent(userDN);
    }

    String newDN = cn + "," + archiveGroupDN;

    // 执行移动操作
    ldapContext.rename(userDN, newDN);
    }

    /**
    * 从DN中提取第一个组件
    */

    private String getDnFirstComponent(String dn) {
    if (dn == null || dn.isEmpty()) {
    return "";
    }

    // DN格式可能是 "CN=名称,OU=组织,…"
    if (dn.contains("=")) {
    int startIndex = dn.indexOf('=') + 1;
    int endIndex = dn.indexOf(',', startIndex);
    if (endIndex > startIndex) {
    return dn.substring(startIndex, endIndex);
    } else {
    return dn.substring(startIndex);
    }
    }

    return dn;
    }
    }

    日志配置

    <configuration>
    <property name="LOG_PATH" value="D:/ADsync/logs" />
    <property name="FILE_NAME" value="AdSync" />

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
    <!– 日志文件命名规则 –>
    <fileNamePattern>D:/ADsync/logs/AdSync.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
    <!– 单个日志文件最大大小 –>
    <maxFileSize>10MB</maxFileSize>
    <!– 保留最近 30 天的日志 –>
    <maxHistory>30</maxHistory>
    <!– 总日志文件大小限制 –>
    <totalSizeCap>1GB</totalSizeCap>
    </rollingPolicy>
    <encoder>
    <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} – %msg%n</pattern>
    </encoder>
    </appender>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
    <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} – %msg%n</pattern>
    </encoder>
    </appender>

    <root level="INFO">
    <appender-ref ref="FILE" />
    <appender-ref ref="CONSOLE" />
    </root>
    </configuration>

    POM.xml文件

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>com</groupId>
    <artifactId>hr-ad-synchronizer</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

    <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
    <!– HTTP客户端 –>
    <dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
    </dependency>

    <!– JSON处理 –>
    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.51</version>
    </dependency>

    <!– 日志框架 –>
    <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.36</version>
    </dependency>
    <dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.11</version>
    <scope>runtime</scope>
    </dependency>

    <!– Apache Axis相关 –>
    <dependency>
    <groupId>org.apache.axis</groupId>
    <artifactId>axis</artifactId>
    <version>1.4</version>
    </dependency>
    <dependency>
    <groupId>commons-discovery</groupId>
    <artifactId>commons-discovery</artifactId>
    <version>0.5</version>
    </dependency>
    <dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.1.1</version>
    </dependency>
    <dependency>
    <groupId>wsdl4j</groupId>
    <artifactId>wsdl4j</artifactId>
    <version>1.6.2</version>
    </dependency>

    <!– SHR API依赖 –>
    <dependency>
    <groupId>com.shr</groupId>
    <artifactId>api</artifactId>
    <version>1.0</version>
    </dependency>
    </dependencies>

    <build>
    <finalName>hr-ad-sync</finalName>
    <plugins>
    <!– 编译插件 –>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
    <source>1.8</source>
    <target>1.8</target>
    <encoding>UTF-8</encoding>
    </configuration>
    </plugin>

    <!– 使用 assembly 插件,它更简单且可靠 –>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.3.0</version>
    <configuration>
    <descriptorRefs>
    <descriptorRef>jar-with-dependencies</descriptorRef>
    </descriptorRefs>
    <archive>
    <manifest>
    <mainClass>com.sync.HrAdSynchronizer</mainClass>
    </manifest>
    </archive>
    </configuration>
    <executions>
    <execution>
    <id>make-assembly</id>
    <phase>package</phase>
    <goals>
    <goal>single</goal>
    </goals>
    </execution>
    </executions>
    </plugin>
    </plugins>
    </build>
    </project>

    生成EXE文件

    1、通过MAVEN打jar包 2、下载launch4j 3、通过launch4j生成exe文件:https://blog.csdn.net/qq_41804823/article/details/145967426

    服务器定时任务

    1、打开服务器管理 在这里插入图片描述 2、点击右上角“工具”,打开任务计划程序 在这里插入图片描述 3、新增任务计划程序库 在这里插入图片描述

    异常问题注意事项

    1、测试时,增加基础OU限制 2、出现权限异常问题,先检查赋值是否正确

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » HR人员和组织信息同步AD域服务器实战方法JAVA
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!