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 Configurationpublic static final String SHR_URL = "HR系統地址";public static final String SHR_ORG_SERVICE = "HR系統獲取組織服務";public static final String SHR_PERSON_SERVICE = "HR系統獲取人員服務";// AD Configurationpublic 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 codespublic 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 setterspublic 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;}@Overridepublic 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 setterspublic 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;}@Overridepublic 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中存在的DNString existingDn = orgIdToDnMap.get(orgId);//existingDn="OU=測試test,OU=集團數字化本部,OU=集團數字化部,OU=多維聯合集團股份有限公司,OU=多維聯合集團,OU=Domain Controllers,DC=duowei,DC=net,DC=cn";// 根據上級組織確定目標DNString 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. 確定用戶所屬組織DNString orgDN = findOrgDnByDeptId(person.getDeptId());if (orgDN == null) {logger.warn("找不到用戶 {} 所屬組織(deptId={}), 將使用默認組織",person.getUsername(), person.getDeptId());orgDN = AppConfig.AD_BASE_DN;}// 6. 生成目標DN - 使用用戶名而不是IDString 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)作為用戶IDif (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 {// 如果沒有手機號,回退到使用用戶IDlogger.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())));}// 更新部門IDif (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=開頭,使用整個DNcn = "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.pswp.cn/pingmian/73127.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/73127.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/73127.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

修改服務器windows遠程桌面默認端口號

修改服務器windows遠程桌面默認端口號 在Windows服務器上修改遠程桌面協議&#xff08;RDP&#xff09;的默認端口&#xff08;3389&#xff09;可以增強服務器的安全性&#xff0c;減少被惡意掃描和攻擊的風險。以下是修改遠程端口的詳細步驟&#xff1a; 按 Win R 打開運行…

MuJoCo 仿真 Panda 機械臂!末端位置實時追蹤 + 可視化(含縮放交互)

視頻講解&#xff1a; MuJoCo 仿真 Panda 機械臂&#xff01;末端位置實時追蹤 可視化&#xff08;含縮放交互&#xff09; 倉庫地址&#xff1a;GitHub - LitchiCheng/mujoco-learning 本期介紹下&#xff0c;mujoco_py這個庫很老了&#xff0c;最新的版本可以通過mujoco的p…

vue-splice方法

一、代碼解析 語法結構 splice(index, deleteCount, newElement) 是 JavaScript 數組的變異方法&#xff0c;其參數含義為&#xff1a; ? index&#xff1a;操作的起始位置&#xff08;索引&#xff09;。 ? 1&#xff1a;刪除的元素數量&#xff08;此處刪除 1 個元素&#…

在Mac M1/M2芯片上完美安裝DeepCTR庫:避坑指南與實戰驗證

讓推薦算法在Apple Silicon上全速運行 概述 作為推薦系統領域的最經常用的明星庫&#xff0c;DeepCTR集成了CTR預估、多任務學習等前沿模型實現。但在Apple Silicon架構的Mac設備上&#xff0c;安裝過程常因ARM架構適配、依賴庫版本沖突等問題受阻。本文通過20次環境搭建實測…

spring boot 攔截器

1、創建ServletConfig配置類 package com.pn.config;import com.pn.filter.LoginFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.web.servlet.Fil…

論文閱讀筆記:Denoising Diffusion Probabilistic Models (2)

接論文閱讀筆記&#xff1a;Denoising Diffusion Probabilistic Models (1) 3、論文推理過程 擴散模型的流程如下圖所示&#xff0c;可以看出 q ( x 0 , 1 , 2 ? , T ? 1 , T ) q(x^{0,1,2\cdots ,T-1, T}) q(x0,1,2?,T?1,T)為正向加噪音過程&#xff0c; p ( x 0 , 1 , …

【大模型基礎_毛玉仁】3.5 Prompt相關應用

目錄 3.5 相關應用3.5.1 基于大語言模型的Agent3.5.2 數據合成3.5.3 Text-to-SQL3.5.4 GPTs 3.5 相關應用 Prompt工程應用廣泛&#xff0c;能提升大語言模型處理基礎及復雜任務的能力&#xff0c;在構建Agent、數據合成、Text-to-SQL轉換和設計個性化GPTs等方面不可或缺。 . …

Deepseek訓練成AI圖片生成機器人

目錄 內容安全層 語義理解層 提示詞工程層 圖像生成層 交付系統 訓練好的指令(復制就可以) 內容安全層 理論支撐:基于深度語義理解的混合過濾系統 敏感詞檢測:采用BERT+CRF混合模型,建立三級敏感詞庫(顯性/隱性/文化禁忌),通過注意力機制捕捉上下文關聯風險 倫…

深入理解 Linux ALSA 音頻架構:從入門到驅動開發

文章目錄 一、什么是 ALSA?二、ALSA 系統架構全景圖核心組件詳解:三、用戶空間開發實戰1. PCM 音頻流操作流程2. 高級配置(asound.conf)四、內核驅動開發指南1. 驅動初始化模板2. DMA 緩沖區管理五、高級主題1. 插件系統原理2. 調試技巧3. 實時音頻優化六、現代 ALSA 發展七…

探秘海螺 AI 視頻與計算機視覺算法的奇妙融合

目錄 開篇&#xff1a;數字浪潮下的視頻新變革 藍耘 Maas 平臺與海螺 AI 視頻&#xff1a;嶄露頭角的視頻創作利器 圖片生成視頻&#xff1a;化靜為動的魔法 文本生成視頻&#xff1a;文字到畫面的奇妙轉換 注冊與登錄 計算機視覺算法&#xff1a;海螺 AI 視頻的核心驅動力…

SOFABoot-10-聊一聊 sofatboot 的十個問題

前言 大家好&#xff0c;我是老馬。 sofastack 其實出來很久了&#xff0c;第一次應該是在 2022 年左右開始關注&#xff0c;但是一直沒有深入研究。 最近想學習一下 SOFA 對于生態的設計和思考。 sofaboot 系列 SOFABoot-00-sofaboot 概覽 SOFABoot-01-螞蟻金服開源的 s…

【數據分享】我國鄉鎮(街道)行政區劃數據(免費獲取/Shp格式)

行政區劃邊界矢量數據是我們在各項研究中最常用的數據。之前我們分享過2024年我國省市縣行政區劃矢量數據&#xff08;可查看之前的文章獲悉詳情&#xff09;&#xff0c;很多小伙伴拿到數據后咨詢有沒有精細到鄉鎮&#xff08;街道&#xff09;的行政區劃矢量數據&#xff01;…

同一個局域網的話 如何訪問另一臺電腦的ip

在局域網內訪問另一臺電腦&#xff0c;可以通過以下幾種常見的方法來實現&#xff1a; ?直接通過IP地址訪問?&#xff1a; 首先&#xff0c;確保兩臺電腦都連接在同一個局域網內。獲取目標電腦的IP地址&#xff0c;這可以通過在目標電腦上打開命令提示符&#xff08;Windows系…

2、基本操作-

學習之前–查看docker服務的狀態 sudo systemctl status docker sudo systemctl start docker restart 配置國內鏡像加速【重要】 選擇阿里云鏡像加速&#xff1a; https://help.aliyun.com/zh/acr/user-guide/accelerate-the-pulls-of-docker-official-images sudo mkdir …

LINUX基礎 [二] - 進程概念

目錄 前言 什么是進程 如何管理進程 描述進程 組織進程 如何查看進程 通過 ps 命令查看進程 通過 ls / proc 命令查看進程 通過系統調用 獲取進程標示符 前言 在學習了【Linux系統編程】中的 ? 操作系統 和 馮諾依曼體系結構 之后&#xff0c;我們已經對系統應該有…

什么是PHP偽協議

PHP偽協議是一種特殊的URL格式&#xff0c;允許開發者以不同于傳統文件路徑訪問和操作資源。以下是一些常見的PHP偽協議及其詳細介紹&#xff1a; 常見的PHP偽協議 1. **file://** - **用途**&#xff1a;訪問本地文件系統。 - **示例**&#xff1a;file:///path/to/file.txt。…

股指期貨貼水波動,影響哪些投資策略?

先來說說“貼水”。簡單來說&#xff0c;貼水就是股指期貨的價格比現貨價格低。比如&#xff0c;滬深300指數現在是4000點&#xff0c;但股指期貨合約的價格只有3950點&#xff0c;這就叫貼水。貼水的大小會影響很多投資策略的收益&#xff0c;接下來我們就來看看具體的影響。 …

算法·動態規劃·入門

動態規劃的概念 狀態&#xff1a;也就是DP數組的定義 狀態轉移 dp五部曲的理解 見&#xff1a;代碼隨想錄 優先確定&#xff1a;狀態的定義&#xff0c;狀態轉移的房產 根據狀態轉移方程確定&#xff1a;遍歷順序&#xff0c;初始化 狀態壓縮 本質上就是變量個數減少&am…

在刀刃上發力:如何精準把握計劃關鍵節點

關鍵路徑分析是項目管理中的一種重要方法&#xff0c;它通過在甘特圖中識別出項目中最長、最關鍵的路徑&#xff0c;來確定項目的最短完成時間。 關鍵路徑上的任務都是項目成功的關鍵因素&#xff0c;任何延誤都可能導致整個項目的延期。關鍵路徑分析對于項目管理者來說至關重要…

第二天 開始Unity Shader的學習之旅之熟悉頂點著色器和片元著色器

Shader初學者的學習筆記 第二天 開始Unity Shader的學習之旅之熟悉頂點著色器和片元著色器 文章目錄 Shader初學者的學習筆記前言一、頂點/片元著色器的基本結構① Shader "Unity Shaders Book/Chapter 5/ Simple Shader"② SubShader③ CGPROGRAM和ENDCG④ 指明頂點…