HR人员和组织信息同步AD域服务器实战方法JAVA
HR人员和组织信息同步AD域服务器
- 前期准备
- AD域基础知识整理
- HR同步AD的逻辑
- 代码结构
- 配置文件设置
- 启动类
- HR组织的Bean
- HR人员Bean
- 获取HR人员和组织信息的类
- AD中处理组织和人员的类
- 日志配置
- POM.xml文件
- 生成EXE文件
- 服务器定时任务
- 异常问题注意事项
前期准备
1、开发语言:Java
2、开发框架:无
3、日志框架:logback
4、服务器:windows2016(已部署了AD域,这里不过多介绍)
5、开发工具:idea、launch4j(用于部署服务器定时任务生成exe用)
6、AD域证书(客户端连接AD使用)
AD域基础知识整理
AD域服务器中重要的知识点或属性描述:
- “CN”(Common Name,常用名),用于指定对象的具体名称
- “DC”(Domain Component,域组件),用来标识域的各个部分
- “Description”,可对对象进行详细的描述说明,在这里存放的为组织编码
- “adminDescription”,用于存放组织id
- “DistinguishedName”(可分辨名称),是 OU 在 AD 中的唯一标识,它描述了 OU 在域中的完整路径
- “mobile”,用于记录用户的手机号
- “department”,用于记录部门的ID
- “displayName”,用于记录用户的显示名称
- “info”,用于记录用户的ID
- “sn”,用于记录用户的姓
- “givenName”,用于记录用户的名
- “unicodePwd”,用于记录用户的密码,赋值时用十六进制
- “userAccountControl”,用于控制用户状态,正常账户为514,禁用账户为514
- “pwdLastSet”,用于控制用户下次登陆时是否需要更改密码
HR同步AD的逻辑
1、数据准备:将HR中的组织和人员信息建立一个Bean方法
2、连接与认证:
①连接HR系统,可以通过接口,也可通过导入外部jar包的方式(此文章用导入外部jar包的方式获取HR中的信息)
②建立AD的系统连接
3、根据HR的信息处理AD中的信息,先处理组织,再处理人员
4、记录日志并打印
代码结构
配置文件设置
记录AD和HR系统的各种信息
public class AppConfig {
// SHR Configuration
public static final String SHR_URL = "HR系统地址";
public static final String SHR_ORG_SERVICE = "HR系统获取组织服务";
public static final String SHR_PERSON_SERVICE = "HR系统获取人员服务";
// AD Configuration
public static final String AD_URL = "AD域的地址";
public static final String AD_ADMIN_DN = "";
public static final String AD_ADMIN_PASSWORD = "管理员密码";
public static final String AD_INIT_PASSWORD = "初始密码";
public static final String AD_BASE_DN = "根OU";
public static final String AD_ARCHIVED_GROUP = "封存人员组";
// Status codes
public static final String STATUS_DISABLED = "1";
public static final String STATUS_ENABLED = "0";
public static final String PERSON_STATUS_ENABLED = "1";
public static final String PERSON_STATUS_DISABLED = "0";
}
启动类
import java.util.List;
public class HrAdSynchronizer {
/*定义日志对象*/
private static final Logger logger = LoggerFactory.getLogger(HrAdSynchronizer.class);
/*定义HR对象*/
private final ShrService shrService;
/*定义AD对象*/
private final AdService adService;
/**
* 日志记录方法
*/
public HrAdSynchronizer() {
// 确保日志目录存在并打印出实际路径
String logDir = SyncUtils.ensureDirectoryExists("logs");
System.out.println("日志目录: " + logDir);
this.shrService = new ShrService();
this.adService = new AdService();
}
/**
* 执行方法
*/
public void synchronize() {
try {
logger.info("开始SHR到AD的同步过程");
// 同步组织结构(包含变更处理)
/*获取HR中的组织信息*/
List<ShrOrganization> organizations = shrService.getOrganizations();
/*打印日志*/
logger.info("从SHR获取到 {} 个组织", organizations.size());
/*将HR中的组织信息同步至AD*/
adService.syncOrganizations(organizations);
// 同步人员信息(包含变更处理)
/*获取HR中的人员信息*/
List<ShrPerson> personnel = shrService.getPersonnel();
/*打印日志*/
logger.info("从SHR获取到 {} 个人员", personnel.size());
/*将HR中的人员信息同步至AD*/
adService.syncPersonnel(personnel);
/*打印日志*/
logger.info("同步过程成功完成");
} catch (Exception e) {
logger.error("同步过程发生错误: {}", e.getMessage(), e);
} finally {
adService.close();
logger.info("同步过程结束");
}
}
/**
* 启动方法
* @param args
*/
public static void main(String[] args) {
/*打印日志,标记功能程序*/
logger.info("启动HR-AD同步程序");
/*调用日志文件自动生成的方法,可注释*/
HrAdSynchronizer synchronizer = new HrAdSynchronizer();
/*调用执行方法*/
synchronizer.synchronize();
}
}
HR组织的Bean
public class ShrOrganization {
private String fnumber;
private String name;
private String easdeptId;
private String superior;
private String status;
// Getters and setters
public String getFnumber() {
return fnumber;
}
public void setFnumber(String fnumber) {
this.fnumber = fnumber;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEasdeptId() {
return easdeptId;
}
public void setEasdeptId(String easdeptId) {
this.easdeptId = easdeptId;
}
public String getSuperior() {
return superior;
}
public void setSuperior(String superior) {
this.superior = superior;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
@Override
public String toString() {
return "ShrOrganization{" +
"fnumber='" + fnumber + '\'' +
", name='" + name + '\'' +
", easdeptId='" + easdeptId + '\'' +
", superior='" + superior + '\'' +
", status='" + status + '\'' +
'}';
}
}
HR人员Bean
public class ShrPerson {
private String empTypeName;
private String mobile;
private String orgNumber;
private String easuserId;
private String supFnumber;
private String supname;
private String superior;
private String status;
private String username;
private String deptId;
// Getters and setters
public String getEmpTypeName() {
return empTypeName;
}
public void setEmpTypeName(String empTypeName) {
this.empTypeName = empTypeName;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getOrgNumber() {
return orgNumber;
}
public void setOrgNumber(String orgNumber) {
this.orgNumber = orgNumber;
}
public String getEasuserId() {
return easuserId;
}
public void setEasuserId(String easuserId) {
this.easuserId = easuserId;
}
public String getSupFnumber() {
return supFnumber;
}
public void setSupFnumber(String supFnumber) {
this.supFnumber = supFnumber;
}
public String getSupname() {
return supname;
}
public void setSupname(String supname) {
this.supname = supname;
}
public String getSuperior() {
return superior;
}
public void setSuperior(String superior) {
this.superior = superior;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getDeptId() {
return deptId;
}
public void setDeptId(String deptId) {
this.deptId = deptId;
}
@Override
public String toString() {
return "ShrPerson{" +
"empTypeName='" + empTypeName + '\'' +
", mobile='" + mobile + '\'' +
", orgNumber='" + orgNumber + '\'' +
", easuserId='" + easuserId + '\'' +
", supFnumber='" + supFnumber + '\'' +
", supname='" + supname + '\'' +
", superior='" + superior + '\'' +
", status='" + status + '\'' +
", username='" + username + '\'' +
", deptId='" + deptId + '\'' +
'}';
}
}
获取HR人员和组织信息的类
import com.shr.api.SHRClient;
import com.shr.api.Response;
import com.sync.config.AppConfig;
import com.sync.model.ShrOrganization;
import com.sync.model.ShrPerson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ShrService {
private static final Logger logger = LoggerFactory.getLogger(ShrService.class);
private final SHRClient shrClient;
public ShrService() {
logger.info("初始化SHR服务,连接到 {}", AppConfig.SHR_URL);
this.shrClient = new SHRClient();
}
/**
* 获取SHR中的组织列表
* @return 返回list
*/
public List<ShrOrganization> getOrganizations() {
/*定义一个返回list对象*/
List<ShrOrganization> organizations = new ArrayList<>();
try {
/*记录开始调用SHR组织日志*/
logger.info("调用SHR组织服务: {}", AppConfig.SHR_ORG_SERVICE);
/*定义请求参数*/
Map<String, Object> param = new HashMap<>();
/*发起请求*/
Response response = shrClient.executeService(AppConfig.SHR_URL, AppConfig.SHR_ORG_SERVICE, param);
/*请求失败处理*/
if (response == null || response.getData() == null) {
/*记录失败日志*/
logger.error("从SHR获取组织数据失败,响应为空");
/*返回失败结果*/
return organizations;
}
/*解析JSON数据*/
JSONArray orgArray = JSON.parseArray(response.getData().toString());
/*记录json日志数量*/
logger.debug("获取到原始组织数据: {} 条记录", orgArray.size());
int enabledCount = 0;
/*遍历组织json*/
for (int i = 0; i < orgArray.size(); i++) {
/*获取第i个对象*/
JSONObject orgJson = orgArray.getJSONObject(i);
/*获取组织状态*/
String status = orgJson.getString("status");
/*只处理启用状态(status=0)的组织*/
if (AppConfig.STATUS_ENABLED.equals(status)) {
/*定义组织对象*/
ShrOrganization organization = new ShrOrganization();
/*组织编码赋值*/
organization.setFnumber(orgJson.getString("fnumber"));
/*组织名称赋值*/
organization.setName(orgJson.getString("name"));
/*组织id赋值*/
organization.setEasdeptId(orgJson.getString("easdept_id"));
/*上级组织部门id赋值*/
organization.setSuperior(orgJson.getString("superior"));
/*组织状态赋值*/
organization.setStatus(status);
/*加入list中*/
organizations.add(organization);
/*记录解析日志*/
logger.debug("解析启用组织: {}", organization);
/*计数器+1*/
enabledCount++;
} else {
logger.debug("跳过禁用组织: fnumber={}, name={}",
orgJson.getString("fnumber"), orgJson.getString("name"));
}
}
/*记录总的处理日志*/
logger.info("成功解析 {} 个组织,其中启用状态的有 {} 个", orgArray.size(), enabledCount);
} catch (Exception e) {
logger.error("从SHR获取组织信息时发生错误: {}", e.getMessage(), e);
}
return organizations;
}
/**
* 获取SHR中人员信息
*
* @return 返回人员List
*/
public List<ShrPerson> getPersonnel() {
/*定义一个List返回对象*/
List<ShrPerson> personnel = new ArrayList<>();
try {
/*记录开始日志*/
logger.info("调用SHR人员服务: {}", AppConfig.SHR_PERSON_SERVICE);
/*定义请求参数*/
Map<String, Object> param = new HashMap<>();
/*发起请求*/
Response response = shrClient.executeService(AppConfig.SHR_URL, AppConfig.SHR_PERSON_SERVICE, param);
/*请求判空*/
if (response == null || response.getData() == null) {
/*记录失败日志*/
logger.error("从SHR获取人员数据失败,响应为空");
/*返回结果*/
return personnel;
}
/*解析JSON数据*/
JSONArray personArray = JSON.parseArray(response.getData().toString());
/*记录人员数量日志*/
logger.debug("获取到原始人员数据: {} 条记录", personArray.size());
int enabledCount = 0;
/*遍历json*/
for (int i = 0; i < personArray.size(); i++) {
/*获取json数据*/
JSONObject personJson = personArray.getJSONObject(i);
/*定义人员对象*/
ShrPerson shrPerson = new ShrPerson();
/*员工类型*/
shrPerson.setEmpTypeName(personJson.getString("empType_name"));
/*手机号*/
shrPerson.setMobile(personJson.getString("mobile"));
/*部门编码*/
shrPerson.setOrgNumber(personJson.getString("org_number"));
/*人员ID*/
shrPerson.setEasuserId(personJson.getString("easuser_id"));
/*上级部门编码*/
shrPerson.setSupFnumber(personJson.getString("supFnumber"));
/*上级部门名称*/
shrPerson.setSupname(personJson.getString("supname"));
/*上级部门ID*/
shrPerson.setSuperior(personJson.getString("superior"));
/*人员状态*/
shrPerson.setStatus(personJson.getString("status"));
/*人员名称*/
shrPerson.setUsername(personJson.getString("username"));
/*人员所在部门ID*/
shrPerson.setDeptId(personJson.getString("dept_id"));
/*只添加启用状态的人员*/
if (AppConfig.PERSON_STATUS_ENABLED.equals(shrPerson.getStatus())) {
/*加入list*/
personnel.add(shrPerson);
/*计数器+1*/
enabledCount++;
/*记录人员日志*/
logger.debug("解析启用人员: {}", shrPerson);
} else {
/*记录跳过日志*/
logger.debug("跳过禁用人员: easuserId={}, username={}, deptId={}",
personJson.getString("easuser_id"),
personJson.getString("username"),
personJson.getString("dept_id"));
}
}
/*记录启动状态人数*/
logger.info("成功解析 {} 个人员,其中启用状态的有 {} 个", personArray.size(), enabledCount);
} catch (Exception e) {
logger.error("从SHR获取人员信息时发生错误: {}", e.getMessage(), e);
}
return personnel;
}
}
AD中处理组织和人员的类
import com.sync.config.AppConfig;
import com.sync.model.ShrOrganization;
import com.sync.model.ShrPerson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.*;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.NamingEnumeration;
import javax.naming.ldap.PagedResultsControl;
import java.io.IOException;
import java.util.*;
public class AdService {
/*日志对象*/
private static final Logger logger = LoggerFactory.getLogger(AdService.class);
/*特殊组织编码,这些组织需要跳过处理*/
private static final String SPECIAL_ORG_CODE = "999";
/*记录AD的连接*/
private LdapContext ldapContext;
/*缓存AD中的组织信息,用于变更检测*/
private Map<String, String> orgIdToDnMap = new HashMap<>();
/*同步到AD的组织对象*/
private Map<String, Attributes> orgDnToAttrsMap = new HashMap<>();
/*存储特殊组织的DN,这些组织不会被处理*/
private Set<String> specialOrgDns = new HashSet<>();
/*增加组织编码到组织名称的映射缓存*/
private Map<String, String> orgNumberToNameMap = new HashMap<>();
/*添加 DN 到组织名称的映射*/
private Map<String, String> dnToOuNameMap = new HashMap<>();
/*构造方法*/
public AdService() {
initContext();
// 初始化时加载现有组织结构
loadExistingOrganizations();
}
/*AD的连接初始化*/
private void initContext() {
try {
logger.info("初始化AD连接,URL: {}", AppConfig.AD_URL);
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, AppConfig.AD_URL);
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, AppConfig.AD_ADMIN_DN);
env.put(Context.SECURITY_CREDENTIALS, AppConfig.AD_ADMIN_PASSWORD);
env.put(Context.SECURITY_PROTOCOL, "ssl");
ldapContext = new InitialLdapContext(env, null);
logger.info("成功连接到Active Directory");
} catch (NamingException e) {
logger.error("连接Active Directory失败: {}", e.getMessage(), e);
}
}
/**
* 加载AD中已存在的组织结构到缓存
*/
private void loadExistingOrganizations() {
try {
logger.info("加载AD中现有组织结构");
/*定义搜索控制器*/
SearchControls searchControls = new SearchControls();
/*设置搜索深度*/
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
/*设置查询内容*/
String[] returnedAtts = {"distinguishedName", "ou", "adminDescription", "description"};
/*设置查询对象*/
searchControls.setReturningAttributes(returnedAtts);
/*设置过滤条件*/
String searchFilter = "(objectClass=organizationalUnit)";
/*执行查询*/
NamingEnumeration<SearchResult> results = ldapContext.search(AppConfig.AD_BASE_DN, searchFilter, searchControls);
int count = 0;
int specialCount = 0;
int noAdminDescCount = 0;
/*遍历查询结果*/
while (results.hasMoreElements()) {
/*获取查询结果*/
SearchResult result = results.next();
/*获取dn*/
String dn = result.getNameInNamespace();
/*获取其余结果*/
Attributes attrs = result.getAttributes();
// 保存 DN 和 OU 名称的映射,以便后续使用
if (attrs.get("ou") != null) {
/*获取ou*/
String ouName = attrs.get("ou").get().toString();
/*将OU放入缓存*/
dnToOuNameMap.put(dn, ouName);
}
// 检查是否是特殊组织
boolean isSpecial = false;
if (attrs.get("description") != null) {
String description = attrs.get("description").get().toString();
if (SPECIAL_ORG_CODE.equals(description)) {
specialOrgDns.add(dn);
isSpecial = true;
specialCount++;
logger.debug("识别到特殊组织(编码999): {}", dn);
}
}
if (attrs.get("ou") != null) {
String ouName = attrs.get("ou").get().toString();
if (AppConfig.AD_ARCHIVED_GROUP.equals(ouName)) {
specialOrgDns.add(dn);
isSpecial = true;
specialCount++;
logger.debug("识别到特殊组织(封存人员组): {}", dn);
}
}
// 如果不是特殊组织且有adminDescription,则添加到正常组织映射
if (!isSpecial && attrs.get("adminDescription") != null) {
String orgId = attrs.get("adminDescription").get().toString();
orgIdToDnMap.put(orgId, dn);
orgDnToAttrsMap.put(dn, attrs);
count++;
} else if (!isSpecial) {
// 记录缺少adminDescription的组织
noAdminDescCount++;
orgDnToAttrsMap.put(dn, attrs);
}
}
logger.info("已加载 {} 个组织到缓存, {} 个特殊组织被排除, {} 个组织缺少adminDescription",
count, specialCount, noAdminDescCount);
} catch (NamingException e) {
logger.error("加载组织结构时发生错误: {}", e.getMessage(), e);
}
}
/**
* 同步组织到AD,处理变更情况
*/
public void syncOrganizations(List<ShrOrganization> organizations) {
logger.info("开始同步组织到AD,共 {} 个组织", organizations.size());
try {
// 首先构建组织编码到名称的映射,用于后续定位上级组织
buildOrgNumberToNameMap(organizations);
// 记录当前同步中处理过的组织ID,用于后续检测删除操作
Set<String> processedOrgIds = new HashSet<>();
/**
*先处理缺少adminDescription但distinguishedName匹配的组织,执行一次后,默认先不执行
*/
handleOrganizationsWithoutAdminDescription(organizations);
// 按上级组织ID排序,确保先处理上级组织
List<ShrOrganization> sortedOrgs = sortOrganizationsByHierarchy(organizations);
for (ShrOrganization org : sortedOrgs) {
// 跳过特殊组织编码
if (SPECIAL_ORG_CODE.equals(org.getFnumber())) {
logger.info("跳过特殊组织编码 {}: {}", org.getFnumber(), org.getName());
continue;
}
// 跳过封存人员组
if (AppConfig.AD_ARCHIVED_GROUP.equals(org.getName())) {
logger.info("跳过封存人员组: {}", org.getName());
continue;
}
String orgId = org.getEasdeptId();
processedOrgIds.add(orgId);
// 组织在AD中存在的DN
String existingDn = orgIdToDnMap.get(orgId);
//existingDn="OU=测试test,OU=集团数字化本部,OU=集团数字化部,OU=多维联合集团股份有限公司,OU=多维联合集团,OU=Domain Controllers,DC=duowei,DC=net,DC=cn";
// 根据上级组织确定目标DN
String targetDn = getTargetDnWithParent(org);
//targetDn = "OU=测试test,OU=集团数字化技术部,OU=集团数字化部,OU=多维联合集团股份有限公司,OU=多维联合集团,OU=Domain Controllers,DC=duowei,DC=net,DC=cn";
// 检查组织状态
if (AppConfig.STATUS_DISABLED.equals(org.getStatus())) {
if (existingDn != null) {
logger.info("组织 {} (ID: {}) 在SHR中被禁用,标记为禁用", org.getName(), orgId);
markOrganizationAsDisabled(existingDn, org);
}
continue;
}
// 处理三种情况:新建、更新属性、重命名(移动)
if (existingDn == null) {
System.err.println(existingDn);
// 新建组织
createNewOrganization(targetDn, org);
} else if (!existingDn.equals(targetDn)) {
System.err.println(existingDn);
// 组织名称或层级变更,需要重命名/移动
renameOrganization(existingDn, targetDn, org);
} else {
// 组织名称和层级未变,但可能需要更新其他属性
updateOrganizationAttributes(existingDn, org);
}
}
// 处理在SHR中不存在但在AD中存在的组织(删除或禁用)
handleDeletedOrganizations(processedOrgIds);
logger.info("组织同步完成");
} catch (Exception e) {
logger.error("同步组织到AD时发生错误: {}", e.getMessage(), e);
}
}
/**
* 处理缺少adminDescription但distinguishedName匹配的组织
*/
private void handleOrganizationsWithoutAdminDescription(List<ShrOrganization> organizations) {
logger.info("检查缺少adminDescription但DN匹配的组织");
int fixedCount = 0;
for (ShrOrganization org : organizations) {
String targetDn = getTargetDnWithParent(org);
String orgId = org.getEasdeptId();
// 如果organizationId不在映射中,但DN存在于AD中
if (!orgIdToDnMap.containsKey(orgId) && orgDnToAttrsMap.containsKey(targetDn)) {
logger.info("发现缺少adminDescription的组织,DN: {}, 组织ID: {}", targetDn, orgId);
try {
// 添加adminDescription属性
ModificationItem[] mods = new ModificationItem[1];
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("adminDescription", orgId));
ldapContext.modifyAttributes(targetDn, mods);
// 更新缓存
orgIdToDnMap.put(orgId, targetDn);
Attributes attrs = orgDnToAttrsMap.get(targetDn);
attrs.put("adminDescription", orgId);
logger.info("成功添加adminDescription属性到组织: {}", targetDn);
fixedCount++;
} catch (NamingException e) {
logger.error("添加adminDescription属性时发生错误: {}", e.getMessage(), e);
}
}
}
if (fixedCount > 0) {
logger.info("共修复 {} 个缺少adminDescription的组织", fixedCount);
}
}
/**
* 构建组织编码到名称的映射
*/
private void buildOrgNumberToNameMap(List<ShrOrganization> organizations) {
orgNumberToNameMap.clear();
for (ShrOrganization org : organizations) {
if (org.getFnumber() != null && org.getName() != null) {
orgNumberToNameMap.put(org.getEasdeptId(), org.getName());
}
}
logger.debug("构建了 {} 个组织编码到名称的映射", orgNumberToNameMap.size());
}
/**
* 按层级关系排序组织,确保先处理上级组织
*/
private List<ShrOrganization> sortOrganizationsByHierarchy(List<ShrOrganization> organizations) {
List<ShrOrganization> sorted = new ArrayList<>(organizations);
// 首先处理没有上级的组织,然后处理有上级的组织
sorted.sort((o1, o2) -> {
boolean o1HasParent = o1.getSuperior() != null && !o1.getSuperior().isEmpty();
boolean o2HasParent = o2.getSuperior() != null && !o2.getSuperior().isEmpty();
if (!o1HasParent && o2HasParent) return -1;
if (o1HasParent && !o2HasParent) return 1;
return 0;
});
return sorted;
}
/**
* 根据上级组织获取目标DN
*/
private String getTargetDnWithParent(ShrOrganization org) {
// 额外添加查找逻辑
String dn = findExistingDnByOuName(org.getName());
if (dn != null) {
return dn;
}
// 原有的逻辑作为后备
if (org.getSuperior() == null || org.getSuperior().isEmpty()) {
// 没有上级组织,直接放在基础DN下
return "OU=" + org.getName() + "," + AppConfig.AD_BASE_DN;
}
// 查找上级组织名称
String parentNumber = org.getSuperior();
String parentName = orgNumberToNameMap.get(parentNumber);
if (parentName == null) {
logger.warn("找不到上级组织 {},组织 {} 将直接放在基础DN下", parentNumber, org.getName());
return "OU=" + org.getName() + "," + AppConfig.AD_BASE_DN;
}
// 检查上级组织是否在AD中存在
String parentDN = findOrganizationDnByName(parentName);
if (parentDN != null) {
// 上级组织存在,将当前组织放在上级组织下
return "OU=" + org.getName() + "," + parentDN;
} else {
logger.warn("上级组织 {} 在AD中不存在,组织 {} 将直接放在基础DN下", parentName, org.getName());
return "OU=" + org.getName() + "," + AppConfig.AD_BASE_DN;
}
}
/**
* 根据组织名称查找DN
*/
private String findOrganizationDnByName(String orgName) {
try {
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchControls.setReturningAttributes(new String[]{"distinguishedName"});
String searchFilter = "(&(objectClass=organizationalUnit)(ou=" + orgName + "))";
NamingEnumeration<SearchResult> results = ldapContext.search(AppConfig.AD_BASE_DN, searchFilter, searchControls);
if (results.hasMoreElements()) {
SearchResult result = results.next();
return result.getNameInNamespace();
}
} catch (NamingException e) {
logger.error("查找组织 {} 时发生错误: {}", orgName, e.getMessage(), e);
}
return null;
}
/**
* 根据OU名称查找可能存在的DN
*/
private String findExistingDnByOuName(String ouName) {
for (Map.Entry<String, String> entry : dnToOuNameMap.entrySet()) {
if (ouName.equals(entry.getValue())) {
return entry.getKey();
}
}
return null;
}
/**
* 创建新组织(不包含封存)
*/
private void createNewOrganization(String orgDn, ShrOrganization org) throws NamingException {
logger.info("创建新组织: {} (ID: {})", org.getName(), org.getFnumber());
Attributes attrs = new BasicAttributes();
Attribute objClass = new BasicAttribute("objectClass");
objClass.add("top");
objClass.add("organizationalUnit");
attrs.put(objClass);
attrs.put("ou", org.getName());
attrs.put("description", org.getFnumber());
attrs.put("adminDescription", org.getEasdeptId());
ldapContext.createSubcontext(orgDn, attrs);
// 更新缓存
orgIdToDnMap.put(org.getFnumber(), orgDn);
orgDnToAttrsMap.put(orgDn, attrs);
logger.info("成功创建组织: {}", org.getName());
}
/**
* 创建封存组织
* @param orgDn
* @throws NamingException
*/
private void createFCNewOrganization(String orgDn) throws NamingException {
logger.info("创建封存人员组");
Attributes attrs = new BasicAttributes();
Attribute objClass = new BasicAttribute("objectClass");
objClass.add("top");
objClass.add("organizationalUnit");
attrs.put(objClass);
attrs.put("ou", "封存人员组");
attrs.put("description", "000");
attrs.put("adminDescription", "000");
ldapContext.createSubcontext(orgDn, attrs);
logger.info("成功创建封存人员组");
}
/**
* 更新组织属性
*/
private void updateOrganizationAttributes(String orgDn, ShrOrganization org) throws NamingException {
logger.info("更新组织属性: {} (ID: {})", org.getName(), org.getFnumber());
Attributes existingAttrs = orgDnToAttrsMap.get(orgDn);
boolean hasChanges = false;
List<ModificationItem> mods = new ArrayList<>();
// 检查description是否需要更新(只存放组织编码)
String currentDesc = existingAttrs.get("description") != null ?
existingAttrs.get("description").get().toString() : null;
String newDesc = org.getFnumber();
if (currentDesc == null || !currentDesc.equals(newDesc)) {
mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("description", newDesc)));
hasChanges = true;
}
// 检查adminDescription是否需要更新
String currentAdminDesc = existingAttrs.get("adminDescription") != null ?
existingAttrs.get("adminDescription").get().toString() : null;
if (currentAdminDesc == null || !currentAdminDesc.equals(org.getEasdeptId())) {
mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("adminDescription", org.getEasdeptId())));
hasChanges = true;
}
if (hasChanges) {
ldapContext.modifyAttributes(orgDn, mods.toArray(new ModificationItem[0]));
logger.info("已更新组织 {} 的属性", org.getName());
// 更新缓存
orgDnToAttrsMap.put(orgDn, ldapContext.getAttributes(orgDn));
} else {
logger.debug("组织 {} 的属性无需更新", org.getName());
}
}
/**
* 重命名/移动组织
*/
private void renameOrganization(String oldDn, String newDn, ShrOrganization org) throws NamingException {
logger.info("重命名/移动组织: 从 {} 到 {}", oldDn, newDn);
// 执行重命名
ldapContext.rename(oldDn, newDn);
// 更新缓存
orgIdToDnMap.put(org.getEasdeptId(), newDn);
orgDnToAttrsMap.remove(oldDn);
orgDnToAttrsMap.put(newDn, ldapContext.getAttributes(newDn));
// 重命名后可能需要更新属性
updateOrganizationAttributes(newDn, org);
// 更新缓存
orgIdToDnMap.put(org.getEasdeptId(), newDn);
orgDnToAttrsMap.remove(oldDn);
orgDnToAttrsMap.put(newDn, ldapContext.getAttributes(newDn));
logger.info("成功重命名/移动组织 {}", org.getName());
}
/**
* 标记组织为禁用
*/
private void markOrganizationAsDisabled(String orgDn, ShrOrganization org) throws NamingException {
logger.info("标记组织为禁用: {} (ID: {})", org.getName(), org.getFnumber());
Attributes existingAttrs = orgDnToAttrsMap.get(orgDn);
// 在description前添加"[已禁用]"标记,但保留组织编码
String currentDesc = existingAttrs.get("description") != null ?
existingAttrs.get("description").get().toString() : org.getFnumber();
if (!currentDesc.startsWith("[已禁用]")) {
ModificationItem[] mods = new ModificationItem[1];
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("description", "[已禁用] " + org.getFnumber()));
ldapContext.modifyAttributes(orgDn, mods);
// 更新缓存
orgDnToAttrsMap.put(orgDn, ldapContext.getAttributes(orgDn));
}
logger.info("组织 {} 已标记为禁用", org.getName());
}
/**
* 处理已删除的组织
*/
private void handleDeletedOrganizations(Set<String> processedOrgIds) throws NamingException {
logger.info("处理在SHR中不存在的组织");
for (String orgId : orgIdToDnMap.keySet()) {
if (!processedOrgIds.contains(orgId)) {
String orgDn = orgIdToDnMap.get(orgId);
// 跳过特殊组织
if (specialOrgDns.contains(orgDn)) {
logger.info("跳过特殊组织的删除处理: {}", orgDn);
continue;
}
// 获取现有属性
Attributes attrs = orgDnToAttrsMap.get(orgDn);
String description = attrs.get("description") != null ?
attrs.get("description").get().toString() : "";
// 如果是特殊编码,跳过
if (SPECIAL_ORG_CODE.equals(description)) {
logger.info("跳过特殊编码组织的删除处理: {}", orgDn);
continue;
}
// 如果描述中没有已删除标记,添加标记
if (!description.startsWith("[已删除]")) {
ModificationItem[] mods = new ModificationItem[1];
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("description", "[已删除] " + description));
ldapContext.modifyAttributes(orgDn, mods);
logger.info("标记组织为已删除: {}", orgDn);
// 更新缓存
orgDnToAttrsMap.put(orgDn, ldapContext.getAttributes(orgDn));
}
}
}
}
/**
* 同步人员到AD,处理各种变更情况
*/
public void syncPersonnel(List<ShrPerson> personnel) {
logger.info("开始同步人员到AD,共 {} 个人员", personnel.size());
try {
// 确保封存组存在
String archiveGroupDN = "OU=" + AppConfig.AD_ARCHIVED_GROUP + "," + AppConfig.AD_BASE_DN;
if (!checkIfEntryExists(archiveGroupDN)) {
logger.info("封存人员组不存在,开始创建");
createFCNewOrganization(archiveGroupDN);
}
// 加载AD中现有用户
Map<String, UserAdInfo> existingUsers = loadExistingUsers();
logger.info("已加载 {} 个AD用户到缓存", existingUsers.size());
// 记录处理过的用户ID,用于后续检测删除操作
Set<String> processedUserIds = new HashSet<>();
int createdCount = 0;
int movedCount = 0;
int updatedCount = 0;
int disabledCount = 0;
int skippedCount = 0;
// 同步用户
for (ShrPerson person : personnel) {
try {
///*测试*/
//if(!person.getUsername().equals("宋汝东")){
// continue;
//}
// 1. 基本检查
if (person.getEasuserId() == null || person.getEasuserId().isEmpty()) {
logger.warn("跳过无ID的用户: {}", person);
skippedCount++;
continue;
}
if (person.getUsername() == null || person.getUsername().isEmpty()) {
logger.warn("跳过无用户名的用户: {}", person.getEasuserId());
skippedCount++;
continue;
}
// 2. 检查员工类型
if (!isValidEmployeeType(person.getEmpTypeName())) {
logger.debug("跳过非目标类型员工: {} (类型: {})",
person.getUsername(), person.getEmpTypeName());
skippedCount++;
continue;
}
String userId = person.getEasuserId();
processedUserIds.add(userId);
// 3. 员工在AD中的信息
UserAdInfo userInfo = existingUsers.get(userId);
boolean exists = (userInfo != null);
// 4. 处理禁用用户
if (AppConfig.PERSON_STATUS_DISABLED.equals(person.getStatus())) {
if (exists) {
logger.info("用户 {} 在SHR中被禁用,移至封存组并禁用", person.getUsername());
disableAndArchiveUser(userInfo.getDn(), archiveGroupDN, person);
disabledCount++;
}
continue;
}
// 5. 确定用户所属组织DN
String orgDN = findOrgDnByDeptId(person.getDeptId());
if (orgDN == null) {
logger.warn("找不到用户 {} 所属组织(deptId={}), 将使用默认组织",
person.getUsername(), person.getDeptId());
orgDN = AppConfig.AD_BASE_DN;
}
// 6. 生成目标DN - 使用用户名而不是ID
String targetUserDN = "CN=" + person.getUsername() + "," + orgDN;
//if(person.getUsername().equals("田振强")){
// System.out.println(111111);
//}
// 7. 处理不同情况
if (!exists) {
//System.err.println(person.getUsername());
// 用户不存在 - 新建用户
createNewUser(targetUserDN, person);
createdCount++;
} else if (!userInfo.getDn().equals(targetUserDN)) {
//System.err.println(person.getUsername());
// 用户存在但DN不同 - 移动用户
moveUser(userInfo.getDn(), targetUserDN, person);
movedCount++;
} else {
//System.err.println(person.getUsername());
// 用户存在且DN一致 - 更新属性
updateUserAttributes(userInfo.getDn(), person);
updatedCount++;
}
} catch (Exception e) {
logger.error("处理用户 {} 时发生错误: {}", person.getUsername(), e.getMessage(), e);
}
}
// 8. 处理已删除的用户
int deletedCount = handleDeletedUsers(existingUsers, processedUserIds, archiveGroupDN);
logger.info("人员同步完成 - 新建: {}, 移动: {}, 更新: {}, 禁用: {}, 删除: {}, 跳过: {}",
createdCount, movedCount, updatedCount, disabledCount, deletedCount, skippedCount);
//logger.info("人员同步完成 - 新建: {}, 移动: {}, 更新: {}, 禁用: {}, 删除: {}, 跳过: {}",
// createdCount, movedCount, updatedCount, disabledCount, skippedCount);
} catch (NamingException e) {
logger.error("同步人员到AD时发生错误: {}", e.getMessage(), e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 判断是否为有效的员工类型
*/
private boolean isValidEmployeeType(String empTypeName) {
if (empTypeName == null) return false;
// 只处理正式员工、试用员工、实习的人员
return empTypeName.contains("正式") ||
empTypeName.contains("试用") ||
empTypeName.contains("实习");
}
/**
* 加载AD中现有用户到缓存
*/
private Map<String, UserAdInfo> loadExistingUsers() throws NamingException, IOException {
Map<String, UserAdInfo> userMap = new HashMap<>();
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
String[] returnedAtts = {"distinguishedName", "info", "userAccountControl", "cn"};
searchControls.setReturningAttributes(returnedAtts);
String searchFilter = "(&(objectClass=user))";
ldapContext.setRequestControls(new Control[]{new PagedResultsControl(10000, Control.NONCRITICAL)});
NamingEnumeration<SearchResult> results = ldapContext.search(AppConfig.AD_BASE_DN, searchFilter, searchControls);
while (results.hasMoreElements()) {
SearchResult result = results.next();
String dn = result.getNameInNamespace();
Attributes attrs = result.getAttributes();
// 使用info属性(对应easuserId)作为用户ID
if (attrs.get("info") != null) {
String userId = attrs.get("info").get().toString();
// 获取用户账户控制属性,判断是否禁用
boolean disabled = false;
if (attrs.get("userAccountControl") != null) {
int uac = Integer.parseInt(attrs.get("userAccountControl").get().toString());
disabled = (uac & 2) != 0; // 账户禁用标志是第2位
}
// 获取用户显示名称
String displayName = "";
if (attrs.get("cn") != null) {
displayName = attrs.get("cn").get().toString();
}
userMap.put(userId, new UserAdInfo(userId, dn, disabled, displayName));
}
}
//System.err.println(personCount);
return userMap;
}
/**
* 创建新用户
*/
private void createNewUser(String userDN, ShrPerson person) throws NamingException {
logger.info("创建新用户: {} (ID: {})", person.getUsername(), person.getEasuserId());
Attributes attrs = new BasicAttributes();
Attribute objClass = new BasicAttribute("objectClass");
objClass.add("top");
objClass.add("person");
objClass.add("organizationalPerson");
objClass.add("user");
attrs.put(objClass);
// CN已经包含在DN中,使用用户名
attrs.put("cn", person.getUsername());
// 使用手机号作为登录名
if (person.getMobile() != null && !person.getMobile().isEmpty()) {
attrs.put("sAMAccountName", person.getMobile());
attrs.put("userPrincipalName", person.getMobile() + "@duowei.net.cn");
} else {
// 如果没有手机号,回退到使用用户ID
logger.warn("用户 {} 没有手机号,将使用ID作为登录名", person.getUsername());
attrs.put("sAMAccountName", person.getEasuserId());
attrs.put("userPrincipalName", person.getEasuserId() + "@duowei.net.cn");
}
// 设置显示名称
attrs.put("displayName", person.getUsername());
// 将easuserId存入info属性
attrs.put("info", person.getEasuserId());
// 设置姓和名
// 假设中文名格式为"姓+名",取第一个字为姓,其余为名
if (person.getUsername() != null && !person.getUsername().isEmpty()) {
String fullName = person.getUsername();
if (fullName.length() > 1) {
// 取第一个字为姓
String lastName = fullName.substring(0, 1);
// 取剩余部分为名
String firstName = fullName.substring(1);
attrs.put("sn", lastName);
attrs.put("givenName", firstName);
} else {
// 如果只有一个字,则全部作为姓
attrs.put("sn", fullName);
}
}
// 其他属性
if (person.getMobile() != null) {
attrs.put("mobile", person.getMobile());
}
if (person.getDeptId() != null) {
attrs.put("department", person.getDeptId());
}
// 设置密码
byte[] unicodePwd = generatePassword(AppConfig.AD_INIT_PASSWORD);
attrs.put(new BasicAttribute("unicodePwd", unicodePwd));
// 用户控制标志: 正常账户 + 密码不过期
int userAccountControl = 512 | 65536;
attrs.put(new BasicAttribute("userAccountControl", String.valueOf(userAccountControl)));
// 要求下次登录更改密码
attrs.put(new BasicAttribute("pwdLastSet", "0"));
// 创建用户
ldapContext.createSubcontext(userDN, attrs);
}
/**
* 移动用户到新位置
*/
private void moveUser(String currentDN, String targetDN, ShrPerson person) throws NamingException {
logger.info("移动用户: {} 从 {} 到 {}", person.getUsername(), currentDN, targetDN);
try {
// 执行重命名操作移动用户
ldapContext.rename(currentDN, targetDN);
// 移动后更新属性
updateUserAttributes(targetDN, person);
} catch (NamingException e) {
logger.error("移动用户 {} 时发生错误: {}", person.getUsername(), e.getMessage());
throw e;
}
}
/**
* 更新用户属性
*/
private void updateUserAttributes(String userDN, ShrPerson person) throws NamingException {
logger.debug("更新用户属性: {}", person.getUsername());
List<ModificationItem> mods = new ArrayList<>();
// 更新手机号
if (person.getMobile() != null) {
mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("mobile", person.getMobile())));
}
// 更新部门ID
if (person.getDeptId() != null) {
mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("department", person.getDeptId())));
}
/*更新登录名为手机号*/
if(person.getMobile() != null){
mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("sAMAccountName", person.getMobile())));
}
// 更新info属性(easuserId)
mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("info", person.getEasuserId())));
// 确保账户处于启用状态
mods.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("userAccountControl", "512")));
// 应用修改
if (!mods.isEmpty()) {
ModificationItem[] modsArray = mods.toArray(new ModificationItem[0]);
ldapContext.modifyAttributes(userDN, modsArray);
}
}
/**
* 禁用用户并移动到归档组
*/
private void disableAndArchiveUser(String userDN, String archiveGroupDN, ShrPerson person) throws NamingException {
logger.info("禁用并归档用户: {}", person.getUsername());
try {
// 首先禁用用户
ModificationItem[] disableMods = new ModificationItem[1];
disableMods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("userAccountControl", "514")); // 514 = 禁用账户
ldapContext.modifyAttributes(userDN, disableMods);
// 然后移动到归档组
String userName = person.getUsername();
String newDN = "CN=" + userName + "," + archiveGroupDN;
ldapContext.rename(userDN, newDN);
} catch (NamingException e) {
logger.error("禁用并归档用户 {} 时发生错误: {}", person.getUsername(), e.getMessage());
throw e;
}
}
/**
* 处理已删除的用户
*/
private int handleDeletedUsers(Map<String, UserAdInfo> existingUsers, Set<String> processedUserIds,
String archiveGroupDN) throws NamingException {
logger.info("处理已删除用户");
int count = 0;
for (UserAdInfo userInfo : existingUsers.values()) {
String userId = userInfo.getUserId();
// 如果用户未在当前处理列表中,且不在归档组,则归档
if (!processedUserIds.contains(userId) && !isInArchiveGroup(userInfo.getDn(), archiveGroupDN)) {
logger.info("用户ID {} 在SHR中不存在,移至封存组并禁用", userId);
try {
disableUser(userInfo.getDn());
moveUserToArchiveGroup(userInfo.getDn(), archiveGroupDN);
count++;
} catch (NamingException e) {
logger.error("处理已删除用户 {} 时发生错误: {}", userId, e.getMessage());
}
}
}
return count;
}
/**
* 检查用户是否已在归档组中
*/
private boolean isInArchiveGroup(String userDN, String archiveGroupDN) {
return userDN.endsWith(archiveGroupDN);
}
/**
* 用户AD信息类
*/
private static class UserAdInfo {
private final String userId;
private final String dn;
private final boolean disabled;
private final String displayName;
public UserAdInfo(String userId, String dn, boolean disabled, String displayName) {
this.userId = userId;
this.dn = dn;
this.disabled = disabled;
this.displayName = displayName;
}
public String getUserId() {
return userId;
}
public String getDn() {
return dn;
}
public boolean isDisabled() {
return disabled;
}
public String getDisplayName() {
return displayName;
}
}
public void close() {
try {
if (ldapContext != null) {
ldapContext.close();
logger.info("关闭LDAP连接");
}
} catch (NamingException e) {
logger.error("关闭LDAP连接时发生错误: {}", e.getMessage(), e);
}
}
/**
* 根据部门ID查找组织DN
*/
private String findOrgDnByDeptId(String deptId) {
if (deptId == null || deptId.isEmpty()) {
return null;
}
return orgIdToDnMap.get(deptId);
}
/**
* 从组织 DN 中提取组织名称
*/
private String getOrgNameFromDN(String dn) {
if (dn == null || dn.isEmpty()) {
return "未知组织";
}
try {
// DN 格式通常是 "OU=组织名称,其他部分"
// 提取第一个 OU= 后面的内容,直到下一个逗号
if (dn.contains("OU=")) {
int start = dn.indexOf("OU=") + 3; // OU= 后面的位置
int end = dn.indexOf(",", start);
if (end > start) {
return dn.substring(start, end);
} else {
return dn.substring(start);
}
}
// 如果没有找到 OU=,尝试从 dnToOuNameMap 获取
if (dnToOuNameMap.containsKey(dn)) {
return dnToOuNameMap.get(dn);
}
} catch (Exception e) {
logger.warn("无法从DN提取组织名称: {}", dn);
}
return "未知组织";
}
/**
* 检查指定DN的条目是否存在
*/
private boolean checkIfEntryExists(String dn) {
try {
ldapContext.lookup(dn);
return true;
} catch (NamingException e) {
return false;
}
}
/**
* 生成AD密码
* AD密码需要以特定格式提供,使用Unicode编码
*/
private byte[] generatePassword(String password) {
// 将密码转换为AD要求的Unicode字节格式
String quotedPassword = "\"" + password + "\"";
char[] unicodePwd = quotedPassword.toCharArray();
byte[] pwdBytes = new byte[unicodePwd.length * 2];
// 转换为Unicode格式
for (int i = 0; i < unicodePwd.length; i++) {
pwdBytes[i * 2] = (byte) (unicodePwd[i] & 0xff);
pwdBytes[i * 2 + 1] = (byte) (unicodePwd[i] >> 8);
}
return pwdBytes;
}
/**
* 禁用用户账户
*/
private void disableUser(String userDN) throws NamingException {
logger.info("禁用用户: {}", userDN);
// 用户账户控制: 禁用账户 (514)
ModificationItem[] mods = new ModificationItem[1];
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("userAccountControl", "514"));
ldapContext.modifyAttributes(userDN, mods);
}
/**
* 将用户移动到封存组
*/
private void moveUserToArchiveGroup(String userDN, String archiveGroupDN) throws NamingException {
logger.info("移动用户到封存组: {} -> {}", userDN, archiveGroupDN);
// 获取用户DN中的CN部分
String cn = "";
if (userDN.startsWith("CN=")) {
int endIndex = userDN.indexOf(',');
if (endIndex > 0) {
cn = userDN.substring(0, endIndex);
} else {
cn = userDN;
}
} else {
// 如果不是以CN=开头,使用整个DN
cn = "CN=" + getDnFirstComponent(userDN);
}
String newDN = cn + "," + archiveGroupDN;
// 执行移动操作
ldapContext.rename(userDN, newDN);
}
/**
* 从DN中提取第一个组件
*/
private String getDnFirstComponent(String dn) {
if (dn == null || dn.isEmpty()) {
return "";
}
// DN格式可能是 "CN=名称,OU=组织,..."
if (dn.contains("=")) {
int startIndex = dn.indexOf('=') + 1;
int endIndex = dn.indexOf(',', startIndex);
if (endIndex > startIndex) {
return dn.substring(startIndex, endIndex);
} else {
return dn.substring(startIndex);
}
}
return dn;
}
}
日志配置
<configuration>
<property name="LOG_PATH" value="D:/ADsync/logs" />
<property name="FILE_NAME" value="AdSync" />
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件命名规则 -->
<fileNamePattern>D:/ADsync/logs/AdSync.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 单个日志文件最大大小 -->
<maxFileSize>10MB</maxFileSize>
<!-- 保留最近 30 天的日志 -->
<maxHistory>30</maxHistory>
<!-- 总日志文件大小限制 -->
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="FILE" />
<appender-ref ref="CONSOLE" />
</root>
</configuration>
POM.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com</groupId>
<artifactId>hr-ad-synchronizer</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- HTTP客户端 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<!-- JSON处理 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.51</version>
</dependency>
<!-- 日志框架 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.36</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.11</version>
<scope>runtime</scope>
</dependency>
<!-- Apache Axis相关 -->
<dependency>
<groupId>org.apache.axis</groupId>
<artifactId>axis</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-discovery</groupId>
<artifactId>commons-discovery</artifactId>
<version>0.5</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<!-- SHR API依赖 -->
<dependency>
<groupId>com.shr</groupId>
<artifactId>api</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
<build>
<finalName>hr-ad-sync</finalName>
<plugins>
<!-- 编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- 使用 assembly 插件,它更简单且可靠 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.sync.HrAdSynchronizer</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
生成EXE文件
1、通过MAVEN打jar包
2、下载launch4j
3、通过launch4j生成exe文件:https://blog.csdn.net/qq_41804823/article/details/145967426
服务器定时任务
1、打开服务器管理
2、点击右上角“工具”,打开任务计划程序
3、新增任务计划程序库
异常问题注意事项
1、测试时,增加基础OU限制
2、出现权限异常问题,先检查赋值是否正确