当前位置: 首页 > article >正文

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域服务器中重要的知识点或属性描述:

  1. “CN”(Common Name,常用名),用于指定对象的具体名称
  2. “DC”(Domain Component,域组件),用来标识域的各个部分
  3. “Description”,可对对象进行详细的描述说明,在这里存放的为组织编码
  4. “adminDescription”,用于存放组织id
  5. “DistinguishedName”(可分辨名称),是 OU 在 AD 中的唯一标识,它描述了 OU 在域中的完整路径
  6. “mobile”,用于记录用户的手机号
  7. “department”,用于记录部门的ID
  8. “displayName”,用于记录用户的显示名称
  9. “info”,用于记录用户的ID
  10. “sn”,用于记录用户的姓
  11. “givenName”,用于记录用户的名
  12. “unicodePwd”,用于记录用户的密码,赋值时用十六进制
  13. “userAccountControl”,用于控制用户状态,正常账户为514,禁用账户为514
  14. “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、出现权限异常问题,先检查赋值是否正确


http://www.kler.cn/a/595422.html

相关文章:

  • 基于Neo4j的文物推荐系统
  • MySQL 客户端连不上(1045 错误)原因全解析
  • 爱普生SG2016CAN晶振优势和多领域应用
  • 在Fedora-Workstation-Live-x86_64-41-1.4中使用最新版本firefox和腾讯翻译插件让英文网页显示中文翻译
  • 【数据结构】C语言实现线索二叉树
  • 树莓集团南京产业园:战略定位背后的深度思考
  • 二分查找上下界问题的思考
  • EMC整改案例:某网络机顶盒网口辐射
  • CA 机构如何防止中间人攻击
  • 大模型——让Word插上AI的翅膀:如何把DeepSeek装进Word
  • NO.52十六届蓝桥杯备战|红⿊树|二叉搜索树|二叉平衡树|查找|插入|删除|构造(C++)
  • 深入理解traceroute命令及其原理
  • 卷积神经网络 - 进一步理解反向传播
  • SQL-登录漏洞-实现原理
  • Powershell、Windows Shell、CMD 的区别与联系
  • 新能源市场科技变革:用Python解码产业趋势与技术创新
  • shell编程基础介绍
  • 根据文件名称查询文件所在位置
  • (每日一道算法题)交易逆序对的总数
  • 微信小程序使用状态管理 - mobx-miniprogram