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

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

2025-04-26 14:17:40 1 阅读

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.logfileNamePattern>
            
            <maxFileSize>10MBmaxFileSize>
            
            <maxHistory>30maxHistory>
            
            <totalSizeCap>1GBtotalSizeCap>
        rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%npattern>
        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%npattern>
        encoder>
    appender>

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

POM.xml文件


<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.0modelVersion>

    <groupId>comgroupId>
    <artifactId>hr-ad-synchronizerartifactId>
    <version>1.0version>
    <packaging>jarpackaging>

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

    <dependencies>
        
        <dependency>
            <groupId>org.apache.httpcomponentsgroupId>
            <artifactId>httpclientartifactId>
            <version>4.5.13version>
        dependency>

        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>2.0.51version>
        dependency>

        
        <dependency>
            <groupId>org.slf4jgroupId>
            <artifactId>slf4j-apiartifactId>
            <version>1.7.36version>
        dependency>
        <dependency>
            <groupId>ch.qos.logbackgroupId>
            <artifactId>logback-classicartifactId>
            <version>1.2.11version>
            <scope>runtimescope>
        dependency>

        
        <dependency>
            <groupId>org.apache.axisgroupId>
            <artifactId>axisartifactId>
            <version>1.4version>
        dependency>
        <dependency>
            <groupId>commons-discoverygroupId>
            <artifactId>commons-discoveryartifactId>
            <version>0.5version>
        dependency>
        <dependency>
            <groupId>commons-logginggroupId>
            <artifactId>commons-loggingartifactId>
            <version>1.1.1version>
        dependency>
        <dependency>
            <groupId>wsdl4jgroupId>
            <artifactId>wsdl4jartifactId>
            <version>1.6.2version>
        dependency>

        
        <dependency>
            <groupId>com.shrgroupId>
            <artifactId>apiartifactId>
            <version>1.0version>
        dependency>
    dependencies>

    <build>
        <finalName>hr-ad-syncfinalName>
        <plugins>
            
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-compiler-pluginartifactId>
                <version>3.8.1version>
                <configuration>
                    <source>1.8source>
                    <target>1.8target>
                    <encoding>UTF-8encoding>
                configuration>
            plugin>

            
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-assembly-pluginartifactId>
                <version>3.3.0version>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependenciesdescriptorRef>
                    descriptorRefs>
                    <archive>
                        <manifest>
                            <mainClass>com.sync.HrAdSynchronizermainClass>
                        manifest>
                    archive>
                configuration>
                <executions>
                    <execution>
                        <id>make-assemblyid>
                        <phase>packagephase>
                        <goals>
                            <goal>singlegoal>
                        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、出现权限异常问题,先检查赋值是否正确

本文地址:https://www.vps345.com/2005.html

搜索文章

Tags

PV计算 带宽计算 流量带宽 服务器带宽 上行带宽 上行速率 什么是上行带宽? CC攻击 攻击怎么办 流量攻击 DDOS攻击 服务器被攻击怎么办 源IP 服务器 linux 运维 游戏 云计算 ssh deepseek Ollama 模型联网 API CherryStudio 进程 操作系统 进程控制 Ubuntu python MCP 数据库 centos oracle 关系型 安全 分布式 llama 算法 opencv 自然语言处理 神经网络 语言模型 javascript 前端 chrome edge react.js 前端面试题 node.js 持续部署 Dell R750XS 科技 ai java 人工智能 个人开发 harmonyos 华为 开发语言 typescript 计算机网络 ubuntu 阿里云 网络 网络安全 网络协议 经验分享 tcp/ip ollama llm php android c++ c语言 YOLO 深度学习 pytorch nuxt3 vue3 spring sql KingBase fastapi mcp mcp-proxy mcp-inspector fastapi-mcp agent sse 银河麒麟 kylin v10 麒麟 v10 spring boot websocket docker 实时音视频 filezilla 无法连接服务器 连接被服务器拒绝 vsftpd 331/530 自动化 蓝耘科技 元生代平台工作流 ComfyUI adb nginx 监控 自动化运维 pycharm conda pillow django flask web3.py kubernetes 容器 学习方法 程序人生 笔记 C 环境变量 进程地址空间 gitlab numpy json html5 firefox macos RTSP xop RTP RTSPServer 推流 视频 kvm 无桌面 命令行 zotero WebDAV 同步失败 代理模式 IIS .net core Hosting Bundle .NET Framework vs2022 学习 html FunASR ASR 佛山戴尔服务器维修 佛山三水服务器维修 golang go file server http server web server windows 集成学习 集成测试 远程工作 权限 ssl rust mysql ffmpeg 音视频 oceanbase rc.local 开机自启 systemd 麒麟 eureka web安全 云原生 AIGC AI MNN DeepSeek Qwen 机器学习 chatgpt 大模型 llama3 Chatglm 开源大模型 fpga开发 媒体 kylin 深度优先 图论 并集查找 换根法 树上倍增 pppoe radius http ESP32 microsoft arm开发 架构 mongodb Dify 博客 java-ee udp react next.js 部署 部署next.js 机器人 ide AI agent prometheus 监控k8s集群 集群内prometheus vue.js audio vue音乐播放器 vue播放音频文件 Audio音频播放器自定义样式 播放暂停进度条音量调节快进快退 自定义audio覆盖默认样式 远程登录 telnet qt 多线程服务器 TCP服务器 qt项目 qt项目实战 qt教程 bash 小程序 YOLOv8 NPU Atlas800 A300I pro 国标28181 视频监控 监控接入 语音广播 流程 SIP SDP Docker Hub docker pull 镜像源 daemon.json Linux 编辑器 uni-app 宝塔面板 同步 备份 建站 安全威胁分析 jenkins gitee AI编程 vscode 1.86 豆瓣 追剧助手 迅雷 nas 微信 内存 目标检测 计算机视觉 excel 服务器繁忙 备选 网站 api 调用 示例 银河麒麟桌面操作系统 Kylin OS 国产化 后端 tomcat postman mock mock server 模拟服务器 mock服务器 Postman内置变量 Postman随机数据 LDAP https jvm 面试 aws googlecloud IIS服务器 IIS性能 日志监控 maven intellij idea 腾讯云 word图片自动上传 word一键转存 复制word图片 复制word图文 复制word公式 粘贴word图文 粘贴word公式 sqlite openssl 密码学 3d 数学建模 网络结构图 MQTT mosquitto 消息队列 智能路由器 外网访问 内网穿透 端口映射 根服务器 spring cloud intellij-idea kafka hibernate shell 统信 国产操作系统 虚拟机安装 redis 游戏程序 sqlserver vscode android studio Cursor ftp GaN HEMT 氮化镓 单粒子烧毁 辐射损伤 辐照效应 github git 运维开发 apache 孤岛惊魂4 爬虫 数据集 pygame 小游戏 五子棋 安全架构 virtualenv arm debian pdf 华为认证 网络工程师 交换机 开源 Linux网络编程 DeepSeek-R1 API接口 Headless Linux live555 rtsp rtp visualstudio 僵尸进程 嵌入式硬件 驱动开发 硬件工程 嵌入式实习 电脑 交互 vim 重启 排查 系统重启 日志 原因 ecmascript nextjs reactjs flash-attention 报错 代码调试 ipdb ux 多线程 单片机 搜索引擎 HTML audio 控件组件 vue3 audio音乐播放器 Audio标签自定义样式默认 vue3播放音频文件音效音乐 自定义audio播放器样式 播放暂停调整声音大小下载文件 MI300x 开发环境 SSL证书 svn stm32 string模拟实现 深拷贝 浅拷贝 经典的string类问题 三个swap 串口服务器 c# Docker Compose docker compose docker-compose Reactor 设计模式 性能优化 C++ 能力提升 面试宝典 技术 IT信息化 创意 社区 直播推流 TRAE Flask FastAPI Waitress Gunicorn uWSGI Uvicorn 银河麒麟操作系统 rpc 远程过程调用 Windows环境 C语言 物联网 flutter Hyper-V WinRM TrustedHosts JAVA IDEA Java matlab asi_bench ci/cd idm 联想开天P90Z装win10 Deepseek mount挂载磁盘 wrong fs type LVM挂载磁盘 Centos7.9 ecm bpm C++软件实战问题排查经验分享 0xfeeefeee 0xcdcdcdcd 动态库加载失败 程序启动失败 程序运行权限 标准用户权限与管理员权限 ddos MCP server C/S LLM jdk gpu算力 向日葵 agi ansible playbook 华为云 springsecurity6 oauth2 授权服务器 前后端分离 医疗APP开发 app开发 safari pip Mac 系统 系统架构 无人机 历史版本 下载 安装 cursor X11 Xming eclipse can 线程池 系统安全 openEuler jar 中间件 负载均衡 王者荣耀 Wi-Fi 超融合 信息与通信 微信小程序 wsl minio devops springboot ssh远程登录 Spring Security 我的世界 我的世界联机 数码 大数据 linux上传下载 ruoyi 前端框架 健康医疗 互联网医院 远程连接 rdp 实验 我的世界服务器搭建 linux 命令 sed 命令 云电竞 云电脑 todesk ArkUI 多端开发 智慧分发 应用生态 鸿蒙OS rabbitmq zabbix BMC IPMI 带外管理 职场和发展 yum apt jetty undertow UOS 统信操作系统 中兴光猫 换光猫 网络桥接 自己换光猫 ESXi Dell HPE 联想 浪潮 缓存 ISO镜像作为本地源 温湿度数据上传到服务器 Arduino HTTP yaml Ultralytics 可视化 list 模拟实现 ip ui 备份SQL Server数据库 数据库备份 傲梅企业备份网络版 单元测试 功能测试 selenium etl fd 文件描述符 智能手机 NAS Termux Samba express xss 换源 国内源 Debian 宝塔面板访问不了 宝塔面板网站访问不了 宝塔面板怎么配置网站能访问 宝塔面板配置ip访问 宝塔面板配置域名访问教程 宝塔面板配置教程 Qwen2.5-coder 离线部署 jupyter unix 矩阵 cocoapods xcode dify hugo IMX317 MIPI H265 VCU SenseVoice 远程桌面 gaussdb rtsp服务器 rtsp server android rtsp服务 安卓rtsp服务器 移动端rtsp服务 大牛直播SDK elasticsearch wsl2 kind AI写作 AI作画 QQ 聊天室 工业4.0 ocr 测试工具 思科模拟器 思科 Cisco IM即时通讯 企业微信 剪切板对通 HTML FORMAT visual studio code Linux的基础指令 Radius 数据分析 ios muduo 个人博客 小智AI服务端 xiaozhi TTS selete 高级IO 其他 银河麒麟服务器操作系统 系统激活 hadoop 命令 windwos防火墙 defender防火墙 win防火墙白名单 防火墙白名单效果 防火墙只允许指定应用上网 防火墙允许指定上网其它禁止 课程设计 软件测试 proxy模式 图像处理 EMQX 通信协议 HAProxy 策略模式 单例模式 弹性计算 虚拟化 KVM 计算虚拟化 弹性裸金属 微信分享 鸿蒙 Image wxopensdk 鸿蒙系统 k8s 漏洞 显示管理器 lightdm gdm CPU 主板 电源 网卡 文件系统 路径解析 程序 编程 性能分析 阻塞队列 生产者消费者模型 服务器崩坏原因 华为od HarmonyOS Next laravel Linux无人智慧超市 LInux多线程服务器 QT项目 LInux项目 单片机项目 vue css less 毕昇JDK ollama下载加速 grafana junit SEO unity unity3d 网络穿透 云服务器 实战案例 Windows 小艺 Pura X 实时互动 SSH Xterminal 流水线 脚本式流水线 efficientVIT YOLOv8替换主干网络 TOLOv8 半虚拟化 硬件虚拟化 Hypervisor 田俊楠 反向代理 可信计算技术 网络攻击模型 致远OA OA服务器 服务器磁盘扩容 .netcore okhttp CORS 跨域 游戏机 n8n 工作流 workflow Netty 即时通信 NIO HTTP 服务器控制 ESP32 DeepSeek 数据结构 Linux awk awk函数 awk结构 awk内置变量 awk参数 awk脚本 awk详解 信号处理 vasp安装 WSL2 查询数据库服务IP地址 SQL Server 语音识别 AutoDL redhat VMware安装Ubuntu Ubuntu安装k8s HCIE 数通 VR手套 数据手套 动捕手套 动捕数据手套 双系统 GRUB引导 Linux技巧 1024程序员节 计算机 程序员 iperf3 带宽测试 输入法 W5500 OLED u8g2 dubbo gateway Clion Nova ResharperC++引擎 Centos7 远程开发 ShenTong 业界资讯 客户端 SVN Server tortoise svn r语言 数据挖掘 数据可视化 烟花代码 烟花 元旦 线程 gradle 自动驾驶 Typore AISphereButler 远程 执行 sshpass 操作 webrtc kamailio sip VoIP 大数据平台 Ubuntu 24 常用命令 Ubuntu 24 Ubuntu vi 异常处理 银河麒麟高级服务器 外接硬盘 Kylin 微服务 dns .net 低代码 回显服务器 UDP的API使用 vSphere vCenter dity make Java Applet URL操作 服务器建立 Socket编程 网络文件读取 AI大模型 大模型入门 大模型教程 OD机试真题 华为OD机试真题 服务器能耗统计 ukui 麒麟kylinos openeuler rust腐蚀 需求分析 规格说明书 安装教程 GPU环境配置 Ubuntu22 CUDA PyTorch Anaconda安装 web 升级 CVE-2024-7347 VPS 合成模型 扩散模型 图像生成 AI-native Docker Desktop 智能音箱 智能家居 etcd 数据安全 RBAC yolov8 jmeter 交叉编译 嵌入式 恒源云 dba tcp autodl 软件定义数据中心 sddc big data DevEco Studio HarmonyOS OpenHarmony 真机调试 XCC Lenovo 飞书 繁忙 解决办法 替代网站 汇总推荐 AI推理 CDN web3 游戏引擎 抗锯齿 GCC Linux环境 Ubuntu DeepSeek DeepSeek Ubuntu DeepSeek 本地部署 DeepSeek 知识库 DeepSeek 私有化知识库 本地部署 DeepSeek DeepSeek 私有化部署 postgresql embedding echarts 传统数据库升级 银行 大语言模型 LLMs 单一职责原则 gcc g++ g++13 IPMITOOL 硬件管理 oneapi open webui IMM sdkman 硬件架构 软件工程 LORA NLP 计算机外设 ardunio BLE gitea asp.net大文件上传 asp.net大文件上传下载 asp.net大文件上传源码 ASP.NET断点续传 asp.net上传文件夹 asp.net上传大文件 .net core断点续传 iis 移动云 云服务 lio-sam SLAM token sas 鲲鹏 FTP 服务器 相机 zookeeper nfs SSL 域名 虚拟局域网 rsyslog mysql离线安装 ubuntu22.04 mysql8.0 源码 毕业设计 onlyoffice 图形化界面 Node-Red 编程工具 流编程 在线office 服务器主板 AI芯片 tcpdump WSL win11 无法解析服务器的名称或地址 hive Hive环境搭建 hive3环境 Hive远程模式 webgl armbian u-boot 显卡驱动 webstorm Trae IDE AI 原生集成开发环境 Trae AI 考研 EasyConnect Cline Kali Linux 黑客 渗透测试 信息收集 tensorflow 黑客技术 VMware安装mocOS VMware macOS系统安装 chrome 浏览器下载 chrome 下载安装 mac 谷歌浏览器下载 项目部署到linux服务器 项目部署过程 本地部署 asm 微信小程序域名配置 微信小程序服务器域名 微信小程序合法域名 小程序配置业务域名 微信小程序需要域名吗 微信小程序添加域名 视觉检测 vscode1.86 1.86版本 ssh远程连接 SSE open Euler dde deepin 统信UOS db LLM Web APP Streamlit MySql Kali 虚拟机 网工 opensearch helm xpath定位元素 虚幻 ssrf 失效的访问控制 openwrt epoll P2P HDLC sysctl.conf vm.nr_hugepages adobe elk Python 网络编程 聊天服务器 套接字 TCP Socket 群晖 bug 魔百盒刷机 移动魔百盒 机顶盒ROM springboot远程调试 java项目远程debug docker远程debug java项目远程调试 springboot远程 ruby 游戏服务器 TrinityCore 魔兽世界 Ubuntu 24.04.1 轻量级服务器 python3.11 dash 正则表达式 Agent 文件分享 Linux24.04 实习 odoo 服务器动作 Server action Linux PID 环境迁移 c deepseek r1 高效日志打印 串口通信日志 服务器日志 系统状态监控日志 异常记录日志 毕设 wps 安卓 sentinel 金仓数据库 2025 征文 数据库平替用金仓 iphone ipython linux安装配置 三级等保 服务器审计日志备份 FTP服务器 rnn KylinV10 麒麟操作系统 Vmware make命令 makefile文件 微信公众平台 bootstrap 软考 5G 3GPP 卫星通信 wireshark linux驱动开发 aarch64 编译安装 HPC seatunnel DigitalOcean GPU服务器购买 GPU服务器哪里有 GPU服务器 镜像 iBMC UltraISO 多个客户端访问 IO多路复用 TCP相关API navicat WebUI DeepSeek V3 bonding 链路聚合 树莓派 VNC ROS 开机自启动 压力测试 rag ragflow ragflow 源码启动 tailscale derp derper 中转 大文件分片上传断点续传及进度条 如何批量上传超大文件并显示进度 axios大文件切片上传详细教 node服务器合并切片 vue3大文件上传报错提示错误 大文件秒传跨域报错cors 防火墙 NAT转发 NAT Server Unity Dedicated Server Host Client 无头主机 stm32项目 netty Python基础 Python教程 Python技巧 frp MacOS录屏软件 mamba Vmamba 腾讯云大模型知识引擎 windows日志 log4j Minecraft thingsboard rclone AList webdav fnOS 端口测试 H3C iDRAC R720xd freebsd glibc npm 常用命令 文本命令 目录命令 XFS xfs文件系统损坏 I_O error es bcompare Beyond Compare HiCar CarLife+ CarPlay QT RK3588 模拟器 教程 加解密 Yakit yaklang iot k8s资源监控 annotations自动化 自动化监控 监控service 监控jvm dell服务器 昇腾 npu css3 服务器无法访问 ip地址无法访问 无法访问宝塔面板 宝塔面板打不开 SysBench 基准测试 流量运营 bat Jellyfin YOLOv12 mybatis 沙盒 word TrueLicense firewalld 多路转接 服务器配置 生物信息学 执法记录仪 智能安全帽 smarteye GPU USB网络共享 嵌入式Linux IPC Playwright 自动化测试 DNS crosstool-ng nvidia RAID RAID技术 磁盘 存储 EMUI 回退 降级 gnu UDP docker部署翻译组件 docker部署deepl docker搭建deepl java对接deepl 翻译组件使用 IO模型 kali 共享文件夹 cnn vmware 卡死 硬件 设备 PCI-Express 自动化任务管理 llama.cpp ip命令 新增网卡 新增IP 启动网卡 grub 版本升级 扩容 服务器时间 rpa 浏览器开发 AI浏览器 ssh漏洞 ssh9.9p2 CVE-2025-23419 游戏开发 Erlang OTP gen_server 热代码交换 事务语义 大模型推理 大模型学习 yum源切换 更换国内yum源 PX4 音乐服务器 Navidrome 音流 AI代码编辑器 springcloud .net mvc断点续传 Linux find grep ping++ 灵办AI rime 链表 dns是什么 如何设置电脑dns dns应该如何设置 ue5 vr ceph DeepSeek行业应用 Heroku 网站部署 在线预览 xlsx xls文件 在浏览器直接打开解析xls表格 前端实现vue3打开excel 文件地址url或接口文档流二进 uni-file-picker 拍摄从相册选择 uni.uploadFile H5上传图片 微信小程序上传图片 camera Arduino 电子信息 状态模式 软件需求 threejs 3D 服务器管理 配置教程 服务器安装 网站管理 edge浏览器 远程控制 rustdesk 剧本 元服务 应用上架 Docker引擎已经停止 Docker无法使用 WSL进度一直是0 镜像加速地址 perf 测试用例 DBeaver 数据仓库 kerberos cuda 程序员创富 nlp VS Code trae 算力 微信开放平台 微信公众号配置 openstack Xen deekseek 知识库 李心怡 hexo TCP协议 composer Linux的权限 多层架构 解耦 产测工具框架 IMX6ULL 管理框架 AD 域管理 语法 网站搭建 serv00 trea idea 具身智能 强化学习 clickhouse docker部署Python 社交电子 Logstash 日志采集 高效远程协作 TrustViewer体验 跨设备操作便利 智能远程控制 热榜 磁盘镜像 服务器镜像 服务器实时复制 实时文件备份 分析解读 开发 milvus 物联网开发 信号 SWAT 配置文件 服务管理 网络共享 软链接 硬链接 langchain 直流充电桩 充电桩 风扇控制软件 minecraft DenseNet ubuntu24.04.1 裸金属服务器 弹性裸金属服务器 p2p micropython esp32 mqtt CrewAI prompt MacMini 迷你主机 mini Apple 离线部署dify 键盘 宠物 免费学习 宠物领养 宠物平台 Nuxt.js 分布式训练 增强现实 沉浸式体验 应用场景 技术实现 案例分析 AR 代理服务器 AD域 fast 大模型应用 SSH 服务 SSH Server OpenSSH Server OpenSSH 嵌入式系统开发 pgpool eNSP 企业网络规划 华为eNSP 网络规划 SSH 密钥生成 SSH 公钥 私钥 生成 CH340 串口驱动 CH341 uart 485 ubuntu24 vivado24 ABAP 边缘计算 智能硬件 outlook 虚幻引擎 Windsurf DocFlow IPv4 子网掩码 公网IP 私有IP gpt conda配置 conda镜像源 命名管道 客户端与服务端通信 存储维护 NetApp存储 EMC存储 7z k8s集群资源管理 云原生开发 MS Materials av1 电视盒子 chfs ubuntu 16.04 RAGFLOW 模拟退火算法 EtherNet/IP串口网关 EIP转RS485 EIP转Modbus EtherNet/IP网关协议 EIP转RS485网关 EIP串口服务器 自动化编程 远程服务 项目部署 code-server 技能大赛 flink 信息可视化 网页设计 华为机试 ai小智 语音助手 ai小智配网 ai小智教程 esp32语音助手 diy语音助手 数据库系统 lsb_release /etc/issue /proc/version uname -r 查看ubuntu版本 C# MQTTS 双向认证 emqx keepalived sonoma 自动更新 ros2 moveit 机器人运动 大模型部署 xshell termius iterm2 neo4j 数据库开发 数据库架构 database chrome devtools chromedriver 框架搭建 ArcTS 登录 ArcUI GridItem 做raid 装系统 arkUI 服务网格 istio remote-ssh js searxng cudnn anaconda 网络药理学 生信 PPI String Cytoscape CytoHubba 西门子PLC 通讯 mcu 影刀 #影刀RPA# 火绒安全 内网服务器 内网代理 内网通信 服务器数据恢复 数据恢复 存储数据恢复 北亚数据恢复 oracle数据恢复 VM搭建win2012 win2012应急响应靶机搭建 攻击者获取服务器权限 上传wakaung病毒 应急响应并溯源 挖矿病毒处置 应急响应综合性靶场 序列化反序列化 办公自动化 自动化生成 pdf教程 RTMP 应用层 CentOS seleium firewall 雨云 NPS 鸿蒙开发 移动开发 uniapp figma 捆绑 链接 谷歌浏览器 youtube google gmail 图形渲染 wsgiref Web 服务器网关接口 opcua opcda KEPServer安装 黑苹果 skynet 产品经理 大模型微调 MDK 嵌入式开发工具 论文笔记 sublime text sequoiaDB arcgis 阿里云ECS 运维监控 wpf alias unalias 别名 VSCode mm-wiki搭建 linux搭建mm-wiki mm-wiki搭建与使用 mm-wiki使用 mm-wiki详解 pyautogui prometheus数据采集 prometheus数据模型 prometheus特点 GoogLeNet spark HistoryServer Spark YARN jobhistory AP配网 AK配网 小程序AP配网和AK配网教程 WIFI设备配网小程序UDP开 服务器部署ai模型 bot Docker Ark-TS语言 Anolis nginx安装 环境安装 linux插件下载 leetcode 推荐算法 hosts 代理 raid5数据恢复 磁盘阵列数据恢复 自定义客户端 SAS dock 加速 CLion 政务 分布式系统 监控运维 Prometheus Grafana 混合开发 JDK 僵尸世界大战 游戏服务器搭建 regedit 开机启动 Google pay Apple pay GIS 遥感 WebGIS AI Agent 字节智能运维 京东云 gpt-3 文心一言 docker run 数据卷挂载 交互模式 基础入门 大大通 第三代半导体 碳化硅 curl wget 端口 查看 ss ai工具 java-rocketmq v10 软件 ldap cmos 本地化部署 centos-root /dev/mapper yum clean all df -h / du -sh cd 目录切换 玩机技巧 软件分享 软件图标 内网环境 h.264 RustDesk自建服务器 rustdesk服务器 docker rustdesk 流式接口 人工智能生成内容 URL 私有化 金融 pyqt RAGFlow 本地知识库部署 DeepSeek R1 模型 网络用户购物行为分析可视化平台 大数据毕业设计 Kylin-Server ros win服务器架设 windows server 拓扑图 Ubuntu共享文件夹 共享目录 Linux共享文件夹 Open WebUI VLAN 企业网络 迁移指南 VMware创建虚拟机 网卡的名称修改 eth0 ens33 tidb GLIBC cpp-httplib linux环境变量 WebRTC easyui 信创 信创终端 中科方德 deep learning Ubuntu Server Ubuntu 22.04.5 QT 5.12.12 QT开发环境 Ubuntu18.04 知识图谱 飞牛nas fnos docker搭建nacos详解 docker部署nacos docker安装nacos 腾讯云搭建nacos centos7搭建nacos 搭建个人相关服务器 sqlite3 RoboVLM 通用机器人策略 VLA设计哲学 vlm fot robot 视觉语言动作模型 USB转串口 飞牛NAS 飞牛OS MacBook Pro xrdp harmonyOS面试题 SRS 流媒体 直播 邮件APP 免费软件 NFS uv 性能测试 雨云服务器 崖山数据库 YashanDB LInux 视频编解码 源码剖析 rtsp实现步骤 流媒体开发 wordpress 无法访问wordpess后台 打开网站页面错乱 linux宝塔面板 wordpress更换服务器 架构与原理 软负载 多进程 cpu 实时 使用 minicom 串口调试工具 相差8小时 UTC 时间 车载系统 nac 802.1 portal 远程看看 远程协助 CentOS Stream iftop 网络流量监控 swoole 粘包问题 risc-v vue-i18n 国际化多语言 vue2中英文切换详细教程 如何动态加载i18n语言包 把语言json放到服务器调用 前端调用api获取语言配置文件 干货分享 黑客工具 密码爆破 docker命令大全 数据管理 数据治理 数据编织 数据虚拟化 Deepseek-R1 私有化部署 推理模型 蓝桥杯 EtherCAT转Modbus ECT转Modbus协议 EtherCAT转485网关 ECT转Modbus串口网关 EtherCAT转485协议 ECT转Modbus网关 欧标 OCPP Invalid Host allowedHosts Cookie visual studio lua 宕机切换 服务器宕机 rocketmq triton 模型分析 怎么卸载MySQL MySQL怎么卸载干净 MySQL卸载重新安装教程 MySQL5.7卸载 Linux卸载MySQL8.0 如何卸载MySQL教程 MySQL卸载与安装 线性代数 电商平台 压测 ECS 域名服务 DHCP 符号链接 配置 深度求索 私域 ue4 着色器 音乐库 飞牛 实用教程 上传视频至服务器代码 vue3批量上传多个视频并预览 如何实现将本地视频上传到网页 element plu视频上传 ant design vue vue3本地上传视频及预览移除 匿名管道 transformer midjourney Attention Redis Desktop DOIT 四博智联 jina 大模型面经 状态管理的 UDP 服务器 Arduino RTOS AnythingLLM AnythingLLM安装 Xinference RAG 检索增强生成 文档解析 大模型垂直应用 PVE 系统开发 binder framework 源码环境 环境配置 Claude x64 SIGSEGV xmm0 matplotlib 磁盘监控 基础环境 mariadb ubuntu20.04 开机黑屏 Unity插件 技术共享 iventoy VmWare OpenEuler