|
|
@@ -1,20 +1,14 @@
|
|
|
package com.malk.minglei.service.impl.employee;
|
|
|
|
|
|
-import com.alibaba.fastjson.JSON;
|
|
|
import com.malk.minglei.config.EmployeeRosterProperties;
|
|
|
-import com.malk.minglei.dto.DingTalkDedicatedAccountCreateRequest;
|
|
|
-import com.malk.minglei.dto.DingTalkDedicatedAccountCreateResult;
|
|
|
import com.malk.minglei.dto.EmployeeRosterInfo;
|
|
|
import com.malk.minglei.dto.EmployeeRosterSyncResult;
|
|
|
-import com.malk.minglei.service.approval.DingTalkApprovalClient;
|
|
|
import com.malk.minglei.service.employee.DingTalkEmployeeRosterClient;
|
|
|
import com.malk.minglei.service.employee.EmployeeRosterService;
|
|
|
import com.malk.server.dingtalk.DDConf;
|
|
|
import com.malk.service.dingtalk.DDClient;
|
|
|
import com.malk.service.dingtalk.DDClient_Contacts;
|
|
|
-import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
-import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.Collections;
|
|
|
@@ -23,11 +17,6 @@ import java.util.LinkedHashSet;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
import java.util.Set;
|
|
|
-import java.time.LocalDate;
|
|
|
-import java.time.ZoneId;
|
|
|
-import java.time.Instant;
|
|
|
-import java.time.format.DateTimeFormatter;
|
|
|
-import java.time.format.DateTimeParseException;
|
|
|
|
|
|
/**
|
|
|
* 员工花名册同步与查询服务实现。
|
|
|
@@ -40,8 +29,6 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
|
|
|
private static final long ROOT_DEPARTMENT_ID = 1L;
|
|
|
private static final int DEFAULT_USER_PAGE_SIZE = 100;
|
|
|
private static final int DEFAULT_ROSTER_BATCH_SIZE = 20;
|
|
|
- private static final ZoneId EMPLOYEE_NO_ZONE_ID = ZoneId.of("Asia/Shanghai");
|
|
|
- private static final String DEDICATED_ACCOUNT_PASSWORD_PREFIX = "ML";
|
|
|
|
|
|
private final DDClient dingTalkClient;
|
|
|
private final DDClient_Contacts contactsClient;
|
|
|
@@ -49,8 +36,6 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
|
|
|
private final DingTalkEmployeeRosterClient rosterClient;
|
|
|
private final EmployeeRosterRepository repository;
|
|
|
private final EmployeeRosterProperties properties;
|
|
|
- private final EmployeeRosterDuplicateNameNotifier duplicateNameNotifier;
|
|
|
- private final DingTalkApprovalClient approvalClient;
|
|
|
|
|
|
/**
|
|
|
* 创建员工花名册服务实现。
|
|
|
@@ -67,330 +52,13 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
|
|
|
DDConf dingTalkConfiguration,
|
|
|
DingTalkEmployeeRosterClient rosterClient,
|
|
|
EmployeeRosterRepository repository,
|
|
|
- EmployeeRosterProperties properties,
|
|
|
- EmployeeRosterDuplicateNameNotifier duplicateNameNotifier) {
|
|
|
- this(dingTalkClient, contactsClient, dingTalkConfiguration, rosterClient, repository, properties,
|
|
|
- duplicateNameNotifier, null);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 创建员工花名册服务实现。
|
|
|
- *
|
|
|
- * @param dingTalkClient 钉钉基础客户端
|
|
|
- * @param contactsClient 钉钉通讯录客户端
|
|
|
- * @param dingTalkConfiguration 钉钉应用配置
|
|
|
- * @param rosterClient 钉钉花名册客户端
|
|
|
- * @param repository 员工花名册本地仓储
|
|
|
- * @param properties 员工花名册同步配置
|
|
|
- * @param duplicateNameNotifier 同名员工通知组件
|
|
|
- * @param approvalClient 钉钉 OA 审批实例客户端
|
|
|
- */
|
|
|
- @Autowired
|
|
|
- public EmployeeRosterServiceImpl(DDClient dingTalkClient,
|
|
|
- DDClient_Contacts contactsClient,
|
|
|
- DDConf dingTalkConfiguration,
|
|
|
- DingTalkEmployeeRosterClient rosterClient,
|
|
|
- EmployeeRosterRepository repository,
|
|
|
- EmployeeRosterProperties properties,
|
|
|
- EmployeeRosterDuplicateNameNotifier duplicateNameNotifier,
|
|
|
- DingTalkApprovalClient approvalClient) {
|
|
|
+ EmployeeRosterProperties properties) {
|
|
|
this.dingTalkClient = dingTalkClient;
|
|
|
this.contactsClient = contactsClient;
|
|
|
this.dingTalkConfiguration = dingTalkConfiguration;
|
|
|
this.rosterClient = rosterClient;
|
|
|
this.repository = repository;
|
|
|
this.properties = properties;
|
|
|
- this.duplicateNameNotifier = duplicateNameNotifier;
|
|
|
- this.approvalClient = approvalClient;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 调用钉钉通讯录创建自建企业账号。钉钉 OA 部门选择字段允许包含对象或嵌套 value,
|
|
|
- * 此处只提取部门 ID 并去重,避免将展示名称传入钉钉接口。
|
|
|
- *
|
|
|
- * @param request 账号创建请求
|
|
|
- * @return 钉钉创建结果
|
|
|
- */
|
|
|
- @Override
|
|
|
- @Transactional(transactionManager = "employeeRosterTransactionManager")
|
|
|
- public DingTalkDedicatedAccountCreateResult createDingTalkDedicatedAccount(DingTalkDedicatedAccountCreateRequest request) {
|
|
|
- if (request == null) {
|
|
|
- throw new IllegalArgumentException("request must not be null");
|
|
|
- }
|
|
|
-
|
|
|
- String accessToken = dingTalkClient.getAccessToken();
|
|
|
- require(accessToken, "Unable to obtain DingTalk access token");
|
|
|
- DedicatedAccountData account = queryDedicatedAccountData(accessToken,
|
|
|
- requiredText(request.getOaProcessInstanceId(), "oaProcessInstanceId must not be blank"));
|
|
|
-
|
|
|
- List<Long> departmentIds = resolveDepartmentIds(accessToken, account.departmentList);
|
|
|
- if (departmentIds.isEmpty()) {
|
|
|
- throw new IllegalArgumentException("departmentList must contain at least one DingTalk department name or ID");
|
|
|
- }
|
|
|
-
|
|
|
- String employeeNo = optionalText(account.employeeNo);
|
|
|
- if (employeeNo == null) {
|
|
|
- employeeNo = repository.reserveNextEmployeeNo(LocalDate.now(EMPLOYEE_NO_ZONE_ID));
|
|
|
- }
|
|
|
- String loginId = employeeNo;
|
|
|
- String initialPassword = DEDICATED_ACCOUNT_PASSWORD_PREFIX + employeeNo;
|
|
|
-
|
|
|
- Map<String, Object> extraFields = new LinkedHashMap<String, Object>();
|
|
|
- extraFields.put("job_number", employeeNo);
|
|
|
- // 钉钉 title 是职位字段;未配置职位时兼容沿用原职称映射。
|
|
|
- putIfNotBlank(extraFields, "title", firstNotBlank(account.position, account.jobTitle));
|
|
|
- String gender = normalizeGender(account.gender);
|
|
|
- if (gender != null) {
|
|
|
- extraFields.put("gender", gender);
|
|
|
- }
|
|
|
- if (account.expectedArrivalDate != null) {
|
|
|
- extraFields.put("hired_date", Long.valueOf(account.expectedArrivalDate
|
|
|
- .atStartOfDay(EMPLOYEE_NO_ZONE_ID).toInstant().toEpochMilli()));
|
|
|
- }
|
|
|
- Map<String, Object> extension = new LinkedHashMap<String, Object>();
|
|
|
- putIfNotBlank(extension, "positionName", account.positionName);
|
|
|
- putIfNotBlank(extension, "jobTitle", account.jobTitle);
|
|
|
- if (account.age != null) {
|
|
|
- extension.put("age", account.age);
|
|
|
- }
|
|
|
- putIfNotBlank(extension, "jobGrade", account.jobGrade);
|
|
|
- putIfNotBlank(extension, "idCardNo", account.idCardNo);
|
|
|
- if (!extension.isEmpty()) {
|
|
|
- extraFields.put("extension", JSON.toJSONString(extension));
|
|
|
- }
|
|
|
- Map response = contactsClient.createUser_dingTalk(
|
|
|
- accessToken,
|
|
|
- loginId,
|
|
|
- initialPassword,
|
|
|
- account.name,
|
|
|
- departmentIds,
|
|
|
- extraFields);
|
|
|
- if (response == null) {
|
|
|
- throw new IllegalStateException("DingTalk dedicated account creation response is empty");
|
|
|
- }
|
|
|
- String userId = extractUserId(response);
|
|
|
- require(userId, "DingTalk dedicated account creation response does not contain userid");
|
|
|
-
|
|
|
- EmployeeRosterRecord record = new EmployeeRosterRecord();
|
|
|
- record.setDingtalkId(userId);
|
|
|
- record.setEmployeeNo(employeeNo);
|
|
|
- record.setUsername(account.name);
|
|
|
- record.setPositionName(optionalText(account.positionName));
|
|
|
- record.setHireDate(account.expectedArrivalDate == null ? null : account.expectedArrivalDate.toString());
|
|
|
- record.setJobTitle(optionalText(account.jobTitle));
|
|
|
- record.setJobGrade(optionalText(account.jobGrade));
|
|
|
- record.setIsResigned(1);
|
|
|
- if (repository.upsertAll(Collections.singletonList(record)) != 1) {
|
|
|
- throw new IllegalStateException("Failed to persist the newly created employee roster record");
|
|
|
- }
|
|
|
- return new DingTalkDedicatedAccountCreateResult(userId, employeeNo, departmentIds);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 查询 OA 审批实例并将人员表单控件转换为创建账号所需的数据。
|
|
|
- * 表单字段名称允许使用当前人事表单的中文名称或常见别名,以兼容控件 bizAlias 的调整。
|
|
|
- *
|
|
|
- * @param accessToken 钉钉访问令牌
|
|
|
- * @param processInstanceId 钉钉 OA 审批实例 ID
|
|
|
- * @return 已完成字段校验的账号创建数据
|
|
|
- */
|
|
|
- private DedicatedAccountData queryDedicatedAccountData(String accessToken, String processInstanceId) {
|
|
|
- if (approvalClient == null) {
|
|
|
- throw new IllegalStateException("DingTalk approval client is not configured");
|
|
|
- }
|
|
|
- Map<String, Object> detail = approvalClient.getProcessInstanceDetail(accessToken, processInstanceId);
|
|
|
- if (detail == null || Boolean.FALSE.equals(detail.get("success"))) {
|
|
|
- throw new IllegalArgumentException("DingTalk OA approval instance query was not successful");
|
|
|
- }
|
|
|
- Object resultValue = detail.get("result");
|
|
|
- if (!(resultValue instanceof Map)) {
|
|
|
- throw new IllegalArgumentException("OA approval instance does not contain result data");
|
|
|
- }
|
|
|
- Map result = (Map) resultValue;
|
|
|
- List<Map> components = formComponents(firstNotNull(result.get("formComponentValues"),
|
|
|
- result.get("form_component_values")));
|
|
|
- if (components.isEmpty()) {
|
|
|
- throw new IllegalArgumentException("OA approval instance does not contain formComponentValues");
|
|
|
- }
|
|
|
-
|
|
|
- DedicatedAccountData account = new DedicatedAccountData();
|
|
|
- account.name = requiredComponentText(components, "姓名", "员工姓名", "name");
|
|
|
- Object departmentValue = requiredComponentValue(components, "部门", "所属部门", "入职部门", "申请部门", "department");
|
|
|
- account.departmentList = Collections.<Object>singletonList(departmentValue);
|
|
|
- account.employeeNo = componentText(components, "工号", "员工工号", "员工编号", "jobNumber", "employeeNo");
|
|
|
- account.positionName = componentText(components, "岗位", "职务", "positionName");
|
|
|
- account.position = componentText(components, "职位", "position", "positionTitle");
|
|
|
- account.gender = componentText(components, "性别", "gender");
|
|
|
- account.age = componentAge(components);
|
|
|
- account.expectedArrivalDate = componentExpectedArrivalDate(components);
|
|
|
- account.jobTitle = componentText(components, "职称", "jobTitle");
|
|
|
- account.jobGrade = componentText(components, "职等", "职级", "jobGrade");
|
|
|
- account.idCardNo = componentText(components, "身份证号", "身份证号码", "idCardNo");
|
|
|
- return account;
|
|
|
- }
|
|
|
-
|
|
|
- private List<Map> formComponents(Object value) {
|
|
|
- Object structuredValue = parseJsonValue(value);
|
|
|
- if (!(structuredValue instanceof Iterable)) {
|
|
|
- return Collections.emptyList();
|
|
|
- }
|
|
|
- List<Map> components = new ArrayList<Map>();
|
|
|
- for (Object item : (Iterable) structuredValue) {
|
|
|
- if (item instanceof Map) {
|
|
|
- components.add((Map) item);
|
|
|
- }
|
|
|
- }
|
|
|
- return components;
|
|
|
- }
|
|
|
-
|
|
|
- private String requiredComponentText(List<Map> components, String... aliases) {
|
|
|
- String value = componentText(components, aliases);
|
|
|
- if (value == null) {
|
|
|
- throw new IllegalArgumentException("OA approval instance does not contain required field: " + aliases[0]);
|
|
|
- }
|
|
|
- return value;
|
|
|
- }
|
|
|
-
|
|
|
- private Object requiredComponentValue(List<Map> components, String... aliases) {
|
|
|
- Map component = findComponent(components, aliases);
|
|
|
- if (component == null) {
|
|
|
- throw new IllegalArgumentException("OA approval instance does not contain required field: " + aliases[0]);
|
|
|
- }
|
|
|
- Object value = parseJsonValue(component.get("extValue"));
|
|
|
- if (isMeaningfulValue(value)) {
|
|
|
- return value;
|
|
|
- }
|
|
|
- value = parseJsonValue(component.get("value"));
|
|
|
- if (!isMeaningfulValue(value)) {
|
|
|
- throw new IllegalArgumentException("OA approval instance field must not be blank: " + aliases[0]);
|
|
|
- }
|
|
|
- return value;
|
|
|
- }
|
|
|
-
|
|
|
- private String componentText(List<Map> components, String... aliases) {
|
|
|
- Map component = findComponent(components, aliases);
|
|
|
- if (component == null) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- String value = toDisplayText(parseJsonValue(component.get("value")));
|
|
|
- return value == null ? toDisplayText(parseJsonValue(component.get("extValue"))) : value;
|
|
|
- }
|
|
|
-
|
|
|
- private Integer componentAge(List<Map> components) {
|
|
|
- String ageText = componentText(components, "年龄", "age");
|
|
|
- if (ageText == null) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- try {
|
|
|
- int age = Integer.parseInt(ageText);
|
|
|
- if (age < 0 || age > 150) {
|
|
|
- throw new IllegalArgumentException("OA approval field age must be between 0 and 150");
|
|
|
- }
|
|
|
- return Integer.valueOf(age);
|
|
|
- } catch (NumberFormatException exception) {
|
|
|
- throw new IllegalArgumentException("OA approval field age must be an integer");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private LocalDate componentExpectedArrivalDate(List<Map> components) {
|
|
|
- String dateText = componentText(components, "预计到岗日期", "预计入职日期", "到岗日期", "expectedArrivalDate");
|
|
|
- if (dateText == null) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- try {
|
|
|
- if (dateText.matches("[0-9]{10,13}")) {
|
|
|
- long timestamp = Long.parseLong(dateText);
|
|
|
- if (dateText.length() == 10) {
|
|
|
- timestamp *= 1000L;
|
|
|
- }
|
|
|
- return Instant.ofEpochMilli(timestamp).atZone(EMPLOYEE_NO_ZONE_ID).toLocalDate();
|
|
|
- }
|
|
|
- return LocalDate.parse(dateText.length() >= 10 ? dateText.substring(0, 10) : dateText,
|
|
|
- DateTimeFormatter.ISO_LOCAL_DATE);
|
|
|
- } catch (DateTimeParseException | NumberFormatException exception) {
|
|
|
- throw new IllegalArgumentException("OA approval field 预计到岗日期 must use yyyy-MM-dd or a Unix timestamp");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private Map findComponent(List<Map> components, String... aliases) {
|
|
|
- for (String alias : aliases) {
|
|
|
- String normalizedAlias = normalizeKey(alias);
|
|
|
- for (Map component : components) {
|
|
|
- String name = firstText(component, "name", "label", "title", "componentName", "bizAlias", "biz_alias");
|
|
|
- if (normalizeKey(name).equals(normalizedAlias)) {
|
|
|
- return component;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- for (Map component : components) {
|
|
|
- String name = firstText(component, "name", "label", "title", "componentName", "bizAlias", "biz_alias");
|
|
|
- String normalizedName = normalizeKey(name);
|
|
|
- for (String alias : aliases) {
|
|
|
- if (!normalizedName.isEmpty() && normalizedName.contains(normalizeKey(alias))) {
|
|
|
- return component;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- private Object parseJsonValue(Object value) {
|
|
|
- if (!(value instanceof String)) {
|
|
|
- return value;
|
|
|
- }
|
|
|
- String text = optionalText((String) value);
|
|
|
- if (text == null || (!text.startsWith("{") && !text.startsWith("["))) {
|
|
|
- return text;
|
|
|
- }
|
|
|
- try {
|
|
|
- return JSON.parse(text);
|
|
|
- } catch (Exception ignored) {
|
|
|
- return text;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private boolean isMeaningfulValue(Object value) {
|
|
|
- if (value instanceof Iterable) {
|
|
|
- return ((Iterable) value).iterator().hasNext();
|
|
|
- }
|
|
|
- if (value instanceof Map) {
|
|
|
- return !((Map) value).isEmpty();
|
|
|
- }
|
|
|
- return toDisplayText(value) != null;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 复用自建企业账号创建时的部门解析规则,将部门名称或路径转换为钉钉部门 ID。
|
|
|
- *
|
|
|
- * @param departmentReferences 部门引用列表
|
|
|
- * @return 去重后的钉钉部门 ID 列表
|
|
|
- */
|
|
|
- @Override
|
|
|
- public List<Long> resolveDingTalkDepartmentIds(List<Object> departmentReferences) {
|
|
|
- String accessToken = dingTalkClient.getAccessToken();
|
|
|
- require(accessToken, "Unable to obtain DingTalk access token");
|
|
|
- return resolveDepartmentIds(accessToken, departmentReferences);
|
|
|
- }
|
|
|
-
|
|
|
- private String normalizeGender(String gender) {
|
|
|
- String value = optionalText(gender);
|
|
|
- if (value == null) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- if ("1".equals(value) || "男".equals(value) || "male".equalsIgnoreCase(value)) {
|
|
|
- return "1";
|
|
|
- }
|
|
|
- if ("2".equals(value) || "女".equals(value) || "female".equalsIgnoreCase(value)) {
|
|
|
- return "2";
|
|
|
- }
|
|
|
- throw new IllegalArgumentException("gender must be 1, 2, 男, 女, male or female");
|
|
|
- }
|
|
|
-
|
|
|
- private void putIfNotBlank(Map<String, Object> fields, String fieldName, String value) {
|
|
|
- String text = optionalText(value);
|
|
|
- if (text != null) {
|
|
|
- fields.put(fieldName, text);
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -433,7 +101,6 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
|
|
|
}
|
|
|
|
|
|
int syncedCount = repository.upsertAll(records);
|
|
|
- duplicateNameNotifier.notifyDuplicateUsernames(repository.findActiveDuplicateUsernameCounts());
|
|
|
return new EmployeeRosterSyncResult(departmentId, type == INCLUDE_CHILD_DEPARTMENTS, records.size(), fieldCodes.size(), syncedCount);
|
|
|
}
|
|
|
|
|
|
@@ -486,20 +153,6 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
|
|
|
return repository.findByEmployeeNo(employeeNo.trim());
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 根据员工姓名查询本地员工花名册记录。
|
|
|
- *
|
|
|
- * @param username 员工姓名
|
|
|
- * @return 员工花名册信息,不存在时返回 null
|
|
|
- */
|
|
|
- @Override
|
|
|
- public EmployeeRosterInfo findByUsername(String username) {
|
|
|
- if (username == null || username.trim().isEmpty()) {
|
|
|
- throw new IllegalArgumentException("username must not be blank");
|
|
|
- }
|
|
|
- return repository.findByUsername(username.trim());
|
|
|
- }
|
|
|
-
|
|
|
private List<Map> listUsers(String accessToken, long departmentId, int type) {
|
|
|
Set<Long> departmentIds = new LinkedHashSet<Long>();
|
|
|
departmentIds.add(departmentId);
|
|
|
@@ -519,192 +172,6 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
|
|
|
return new ArrayList<Map>(usersByUserId.values());
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 将 OA 传入的部门名称或部门 ID 转换为钉钉创建用户接口所需的部门 ID。
|
|
|
- *
|
|
|
- * <p>部门名称不能直接传给钉钉创建用户接口。名称按层级路径逐段查询直属子部门,
|
|
|
- * 避免为一次账号创建遍历整个部门树;直接传入部门 ID 的历史调用方式保持兼容。</p>
|
|
|
- *
|
|
|
- * @param accessToken 钉钉应用访问令牌
|
|
|
- * @param departmentList OA 部门选择结果,支持名称、ID 或包含名称/ID 的对象
|
|
|
- * @return 去重后的钉钉部门 ID 列表
|
|
|
- */
|
|
|
- private List<Long> resolveDepartmentIds(String accessToken, List<Object> departmentList) {
|
|
|
- Set<Long> departmentIds = new LinkedHashSet<Long>();
|
|
|
- Set<String> departmentNames = new LinkedHashSet<String>();
|
|
|
- if (departmentList != null) {
|
|
|
- for (Object department : departmentList) {
|
|
|
- collectDepartmentReferences(department, departmentIds, departmentNames);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (departmentNames.isEmpty()) {
|
|
|
- return new ArrayList<Long>(departmentIds);
|
|
|
- }
|
|
|
-
|
|
|
- for (String departmentName : departmentNames) {
|
|
|
- departmentIds.add(resolveDepartmentIdByPath(accessToken, departmentName));
|
|
|
- }
|
|
|
- return new ArrayList<Long>(departmentIds);
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 从根部门开始按路径段查询直属子部门,得到目标部门 ID。
|
|
|
- *
|
|
|
- * @param accessToken 钉钉应用访问令牌
|
|
|
- * @param departmentPath 以 - 或 / 分隔的部门路径
|
|
|
- * @return 路径末端的钉钉部门 ID
|
|
|
- */
|
|
|
- private Long resolveDepartmentIdByPath(String accessToken, String departmentPath) {
|
|
|
- long parentDepartmentId = ROOT_DEPARTMENT_ID;
|
|
|
- for (String departmentName : splitDepartmentPath(departmentPath)) {
|
|
|
- Set<Long> matchedDepartmentIds = new LinkedHashSet<Long>();
|
|
|
- List<Map> childDepartments = contactsClient.listSubDepartmentDetail(accessToken, parentDepartmentId);
|
|
|
- if (childDepartments != null) {
|
|
|
- for (Map childDepartment : childDepartments) {
|
|
|
- String childDepartmentName = firstText(childDepartment, "name", "dept_name", "deptName");
|
|
|
- if (!departmentName.equals(normalizeDepartmentName(childDepartmentName))) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- Long childDepartmentId = toPositiveLong(firstNotNull(childDepartment.get("dept_id"),
|
|
|
- childDepartment.get("deptId"), childDepartment.get("id")));
|
|
|
- if (childDepartmentId != null) {
|
|
|
- matchedDepartmentIds.add(childDepartmentId);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- if (matchedDepartmentIds.isEmpty()) {
|
|
|
- throw new IllegalArgumentException("departmentList contains an unknown DingTalk department path: " + departmentPath);
|
|
|
- }
|
|
|
- if (matchedDepartmentIds.size() > 1) {
|
|
|
- throw new IllegalArgumentException("departmentList matches multiple DingTalk departments for name: " + departmentName);
|
|
|
- }
|
|
|
- parentDepartmentId = matchedDepartmentIds.iterator().next().longValue();
|
|
|
- }
|
|
|
- return Long.valueOf(parentDepartmentId);
|
|
|
- }
|
|
|
-
|
|
|
- private List<String> splitDepartmentPath(String departmentPath) {
|
|
|
- List<String> departmentNames = new ArrayList<String>();
|
|
|
- for (String segment : departmentPath.split("[-/]")) {
|
|
|
- String departmentName = normalizeDepartmentName(segment);
|
|
|
- if (departmentName == null) {
|
|
|
- throw new IllegalArgumentException("departmentList contains an invalid DingTalk department path: " + departmentPath);
|
|
|
- }
|
|
|
- departmentNames.add(departmentName);
|
|
|
- }
|
|
|
- return departmentNames;
|
|
|
- }
|
|
|
-
|
|
|
- private void collectDepartmentReferences(Object value, Set<Long> departmentIds, Set<String> departmentNames) {
|
|
|
- if (value instanceof Iterable) {
|
|
|
- for (Object item : (Iterable<?>) value) {
|
|
|
- collectDepartmentReferences(item, departmentIds, departmentNames);
|
|
|
- }
|
|
|
- return;
|
|
|
- }
|
|
|
- if (value instanceof Map) {
|
|
|
- Map department = (Map) value;
|
|
|
- if (collectDepartmentIdsFromObject(department, departmentIds)) {
|
|
|
- return;
|
|
|
- }
|
|
|
- String departmentName = firstText(department, "name", "deptName", "dept_name", "departmentName",
|
|
|
- "department_name", "label", "text");
|
|
|
- if (departmentName != null) {
|
|
|
- departmentNames.add(departmentName);
|
|
|
- return;
|
|
|
- }
|
|
|
- collectDepartmentReferences(department.get("value"), departmentIds, departmentNames);
|
|
|
- collectDepartmentReferences(department.get("departments"), departmentIds, departmentNames);
|
|
|
- collectDepartmentReferences(department.get("departmentList"), departmentIds, departmentNames);
|
|
|
- collectDepartmentReferences(department.get("deptIdList"), departmentIds, departmentNames);
|
|
|
- return;
|
|
|
- }
|
|
|
- if (value instanceof Number) {
|
|
|
- addDepartmentId(((Number) value).longValue(), departmentIds);
|
|
|
- return;
|
|
|
- }
|
|
|
- if (value instanceof String) {
|
|
|
- String text = ((String) value).trim();
|
|
|
- if (text.isEmpty()) {
|
|
|
- return;
|
|
|
- }
|
|
|
- for (String departmentReference : text.split(",")) {
|
|
|
- String reference = optionalText(departmentReference);
|
|
|
- if (reference == null) {
|
|
|
- continue;
|
|
|
- }
|
|
|
- if (isNumericDepartmentId(reference)) {
|
|
|
- addDepartmentId(Long.parseLong(reference), departmentIds);
|
|
|
- } else {
|
|
|
- departmentNames.add(reference);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private boolean collectDepartmentIdsFromObject(Object value, Set<Long> departmentIds) {
|
|
|
- int departmentIdCount = departmentIds.size();
|
|
|
- if (value instanceof Map) {
|
|
|
- Map department = (Map) value;
|
|
|
- collectDepartmentIdsFromObject(department.get("id"), departmentIds);
|
|
|
- collectDepartmentIdsFromObject(department.get("deptId"), departmentIds);
|
|
|
- collectDepartmentIdsFromObject(department.get("dept_id"), departmentIds);
|
|
|
- collectDepartmentIdsFromObject(department.get("departmentId"), departmentIds);
|
|
|
- collectDepartmentIdsFromObject(department.get("department_id"), departmentIds);
|
|
|
- collectDepartmentIdsFromObject(department.get("value"), departmentIds);
|
|
|
- return departmentIds.size() > departmentIdCount;
|
|
|
- }
|
|
|
- if (value instanceof Iterable) {
|
|
|
- for (Object item : (Iterable<?>) value) {
|
|
|
- collectDepartmentIdsFromObject(item, departmentIds);
|
|
|
- }
|
|
|
- return departmentIds.size() > departmentIdCount;
|
|
|
- }
|
|
|
- if (value instanceof Number) {
|
|
|
- addDepartmentId(((Number) value).longValue(), departmentIds);
|
|
|
- } else if (value instanceof String) {
|
|
|
- String text = optionalText((String) value);
|
|
|
- if (text != null) {
|
|
|
- for (String departmentId : text.split(",")) {
|
|
|
- String candidate = optionalText(departmentId);
|
|
|
- if (candidate != null && isNumericDepartmentId(candidate)) {
|
|
|
- addDepartmentId(Long.parseLong(candidate), departmentIds);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- return departmentIds.size() > departmentIdCount;
|
|
|
- }
|
|
|
-
|
|
|
- private String normalizeDepartmentName(String departmentName) {
|
|
|
- return optionalText(departmentName);
|
|
|
- }
|
|
|
-
|
|
|
- private boolean isNumericDepartmentId(String value) {
|
|
|
- return value.matches("-?[0-9]+");
|
|
|
- }
|
|
|
-
|
|
|
- private Long toPositiveLong(Object value) {
|
|
|
- if (value instanceof Number) {
|
|
|
- long number = ((Number) value).longValue();
|
|
|
- return number > 0 ? Long.valueOf(number) : null;
|
|
|
- }
|
|
|
- String text = toStringValue(value);
|
|
|
- if (text == null || !isNumericDepartmentId(text)) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- long number = Long.parseLong(text);
|
|
|
- return number > 0 ? Long.valueOf(number) : null;
|
|
|
- }
|
|
|
-
|
|
|
- private void addDepartmentId(long departmentId, Set<Long> departmentIds) {
|
|
|
- if (departmentId < 1) {
|
|
|
- throw new IllegalArgumentException("departmentList contains an invalid DingTalk department ID");
|
|
|
- }
|
|
|
- departmentIds.add(Long.valueOf(departmentId));
|
|
|
- }
|
|
|
-
|
|
|
private void appendDepartmentUsers(String accessToken, long departmentId, Map<String, Map> usersByUserId) {
|
|
|
long cursor = 0L;
|
|
|
int pageSize = normalizePageSize(properties.getUserPageSize());
|
|
|
@@ -776,36 +243,9 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
|
|
|
record.setJobGradeId(findRosterValue(rosterValues, "职等", "ad410990-cee4-4e19-b70f-189a8ab87527", "job_grade"));
|
|
|
record.setDepartment(findRosterValue(rosterValues, "部门", "所在部门", "部门名称", "department"));
|
|
|
record.setFirstDepartment(findRosterValue(rosterValues, "一级部门", "一级部门名称", "first_department"));
|
|
|
- record.setHireDate(findRosterValue(rosterValues, "\u5165\u804c\u65f6\u95f4", "hire_date"));
|
|
|
- record.setPositionName(findRosterLabel(rosterValues, "\u804c\u4f4d", "sys00-position", "position_name"));
|
|
|
- record.setBankCardNo(findRosterValue(rosterValues, "\u94f6\u884c\u5361\u53f7", "bank_card_no"));
|
|
|
- record.setBankName(findRosterValue(rosterValues, "\u5f00\u6237\u884c", "bank_name"));
|
|
|
- record.setBankBranchCode(findRosterValue(rosterValues, "\u652f\u884c\u8054\u884c\u53f7", "bank_branch_code"));
|
|
|
- record.setIsResigned(isActive(user) ? 1 : 0);
|
|
|
return record;
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 钉钉通讯录明确返回非激活状态时,按离职处理;缺失状态字段时保持原有在职兼容行为。
|
|
|
- *
|
|
|
- * @param user 钉钉通讯录用户详情
|
|
|
- * @return 是否在职
|
|
|
- */
|
|
|
- private boolean isActive(Map user) {
|
|
|
- Object active = firstNotNull(user.get("active"), user.get("is_active"), user.get("isActive"));
|
|
|
- if (active == null) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- if (active instanceof Boolean) {
|
|
|
- return ((Boolean) active).booleanValue();
|
|
|
- }
|
|
|
- if (active instanceof Number) {
|
|
|
- return ((Number) active).intValue() != 0;
|
|
|
- }
|
|
|
- String value = String.valueOf(active).trim();
|
|
|
- return !"false".equalsIgnoreCase(value) && !"0".equals(value);
|
|
|
- }
|
|
|
-
|
|
|
private List<Map> extractUserList(Map result) {
|
|
|
if (result == null) {
|
|
|
return Collections.emptyList();
|
|
|
@@ -1106,20 +546,6 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private String requiredText(String value, String message) {
|
|
|
- if (value == null || value.trim().isEmpty()) {
|
|
|
- throw new IllegalArgumentException(message);
|
|
|
- }
|
|
|
- return value.trim();
|
|
|
- }
|
|
|
-
|
|
|
- private String optionalText(String value) {
|
|
|
- if (value == null || value.trim().isEmpty()) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- return value.trim();
|
|
|
- }
|
|
|
-
|
|
|
private static class RosterFieldValue {
|
|
|
|
|
|
private String label;
|
|
|
@@ -1145,23 +571,4 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
|
|
|
return label == null && value == null;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- /**
|
|
|
- * OA 审批实例解析出的账号创建资料,仅在当前服务内使用,避免把未经审批的数据暴露为接口入参。
|
|
|
- */
|
|
|
- private static class DedicatedAccountData {
|
|
|
-
|
|
|
- private String name;
|
|
|
- private List<Object> departmentList;
|
|
|
- private String employeeNo;
|
|
|
- private String positionName;
|
|
|
- private String position;
|
|
|
- private String gender;
|
|
|
- private Integer age;
|
|
|
- private LocalDate expectedArrivalDate;
|
|
|
- private String jobTitle;
|
|
|
- private String jobGrade;
|
|
|
- private String idCardNo;
|
|
|
- }
|
|
|
-
|
|
|
}
|