|
|
@@ -0,0 +1,670 @@
|
|
|
+package com.muzhi.tianpai.service;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.alibaba.fastjson.JSONArray;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.aliyun.dingtalkyida_2_0.models.CreateOrUpdateFormDataHeaders;
|
|
|
+import com.aliyun.dingtalkyida_2_0.models.CreateOrUpdateFormDataRequest;
|
|
|
+import com.aliyun.dingtalkyida_2_0.models.SearchFormDatasHeaders;
|
|
|
+import com.aliyun.dingtalkyida_2_0.models.SearchFormDatasRequest;
|
|
|
+import com.aliyun.dingtalkyida_2_0.models.SearchFormDatasResponse;
|
|
|
+import com.aliyun.teaopenapi.models.Config;
|
|
|
+import com.aliyun.teautil.models.RuntimeOptions;
|
|
|
+import com.dingtalk.api.DefaultDingTalkClient;
|
|
|
+import com.dingtalk.api.request.OapiV2DepartmentGetRequest;
|
|
|
+import com.dingtalk.api.request.OapiV2DepartmentListsubidRequest;
|
|
|
+import com.dingtalk.api.request.OapiV2UserGetRequest;
|
|
|
+import com.dingtalk.api.request.OapiV2UserListRequest;
|
|
|
+import com.dingtalk.api.response.OapiV2DepartmentGetResponse;
|
|
|
+import com.dingtalk.api.response.OapiV2DepartmentListsubidResponse;
|
|
|
+import com.dingtalk.api.response.OapiV2UserGetResponse;
|
|
|
+import com.dingtalk.api.response.OapiV2UserListResponse;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import okhttp3.MediaType;
|
|
|
+import okhttp3.OkHttpClient;
|
|
|
+import okhttp3.Request;
|
|
|
+import okhttp3.RequestBody;
|
|
|
+import okhttp3.Response;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.scheduling.annotation.Scheduled;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
+
|
|
|
+import javax.crypto.Mac;
|
|
|
+import javax.crypto.spec.SecretKeySpec;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.security.MessageDigest;
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.ArrayDeque;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Base64;
|
|
|
+import java.util.Deque;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.LinkedHashSet;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.UUID;
|
|
|
+
|
|
|
+@Service
|
|
|
+@Slf4j
|
|
|
+public class PersonSyncService {
|
|
|
+
|
|
|
+ private static final String HIKVISION_API_PATH = "/api/resource/v2/person/personList";///api/resource/v2/person/advance/personList
|
|
|
+ private static final String HIKVISION_SIGN_PATH = "/artemis" + HIKVISION_API_PATH;
|
|
|
+ private static final String PERSON_INFO_API_PATH = "/api/resource/v1/person/condition/personInfo";
|
|
|
+ private static final String PERSON_INFO_SIGN_PATH = "/artemis" + PERSON_INFO_API_PATH;
|
|
|
+ private static final String SIGNATURE_HEADERS = "x-ca-key,x-ca-nonce,x-ca-timestamp";
|
|
|
+ private static final String CONTENT_TYPE = "application/json; charset=utf-8";
|
|
|
+ private static final MediaType JSON_MEDIA_TYPE = MediaType.parse(CONTENT_TYPE);
|
|
|
+ private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
+ private static final String YIDA_JOB_NUMBER_FIELD = "textField_ms5onsxa";
|
|
|
+ private static final String YIDA_SUPERVISOR_FIELD = "employeeField_mv6k4flb";
|
|
|
+ private static final String YIDA_SUPERVISOR_JOB_NUMBER_FIELD = "textField_mv6ky7t5";
|
|
|
+ private static final long DINGTALK_ROOT_DEPARTMENT_ID = 1L;
|
|
|
+ private static final long DINGTALK_USER_PAGE_SIZE = 100L;
|
|
|
+ private static final int MAX_DEPARTMENT_LEVELS = 20;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ @org.springframework.beans.factory.annotation.Qualifier("hikvisionHttpClient")
|
|
|
+ private OkHttpClient httpClient;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private AccessTokenService accessTokenService;
|
|
|
+
|
|
|
+ @Value("${person-sync.hikvision.base-url}")
|
|
|
+ private String hikvisionBaseUrl;
|
|
|
+
|
|
|
+ @Value("${person-sync.hikvision.app-key}")
|
|
|
+ private String hikvisionAppKey;
|
|
|
+
|
|
|
+ @Value("${person-sync.hikvision.app-secret}")
|
|
|
+ private String hikvisionAppSecret;
|
|
|
+
|
|
|
+ @Value("${person-sync.hikvision.partner-code:}")
|
|
|
+ private String hikvisionPartnerCode;
|
|
|
+
|
|
|
+ @Value("${person-sync.hikvision.page-size:50}")
|
|
|
+ private int hikvisionPageSize;
|
|
|
+
|
|
|
+ @Value("${person-sync.yida.app-type}")
|
|
|
+ private String yidaAppType;
|
|
|
+
|
|
|
+ @Value("${person-sync.yida.system-token}")
|
|
|
+ private String yidaSystemToken;
|
|
|
+
|
|
|
+ @Value("${person-sync.yida.form-uuid}")
|
|
|
+ private String yidaFormUuid;
|
|
|
+
|
|
|
+ @Value("${person-sync.yida.user-id:yida_pub_account}")
|
|
|
+ private String yidaUserId;
|
|
|
+
|
|
|
+ @Scheduled(fixedDelay = 86400000L, initialDelay = 0L)
|
|
|
+ public void syncPersons() {
|
|
|
+ log.info("海康人员同步到宜搭开始,当前时间:{}", LocalDateTime.now().format(DATE_TIME_FORMATTER));
|
|
|
+ int added = 0;
|
|
|
+ int updated = 0;
|
|
|
+ int failed = 0;
|
|
|
+
|
|
|
+ String accessToken = accessTokenService.getAccessToken();
|
|
|
+ if (!StringUtils.hasText(accessToken)) {
|
|
|
+ log.error("海康人员同步失败:宜搭 accessToken 为空");
|
|
|
+ log.info("宜搭人员同步统计:新增 {},更新 {},失败 {}", added, updated, failed);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ final List<JSONObject> persons;
|
|
|
+ try {
|
|
|
+ persons = fetchAllPersons();
|
|
|
+ log.info("海康读取人员总数:{}", persons.size());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("海康人员分页读取失败,本轮同步终止");
|
|
|
+ log.info("宜搭人员同步统计:新增 {},更新 {},失败 {}", added, updated, failed);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, String> dingTalkUserIdsByJobNumber = null;
|
|
|
+ try {
|
|
|
+ dingTalkUserIdsByJobNumber = loadDingTalkUserIdsByJobNumber(accessToken);
|
|
|
+ log.info("钉钉通讯录工号索引加载完成,共 {} 人", dingTalkUserIdsByJobNumber.size());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("钉钉通讯录工号索引加载失败,继续同步基础档案并保留已有主管字段,原因:{}",
|
|
|
+ StringUtils.hasText(e.getMessage()) ? e.getMessage() : e.getClass().getSimpleName());
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ com.aliyun.dingtalkyida_2_0.Client yidaClient = createYidaClient();
|
|
|
+ int current = 0;
|
|
|
+ for (JSONObject person : persons) {
|
|
|
+ current++;
|
|
|
+ String personId = person.getString("personId");
|
|
|
+ if (!StringUtils.hasText(personId)) {
|
|
|
+ failed++;
|
|
|
+ log.error("单条人员同步失败,人员ID为空");
|
|
|
+ log.info("第 {} 条处理完成,当前条数/总条数:{}/{}", current, current, persons.size());
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ String searchFieldJson = buildSearchFieldJson(personId);
|
|
|
+ String searchCondition = buildSearchCondition(personId);
|
|
|
+ boolean exists = existsInYida(yidaClient, accessToken, searchFieldJson);
|
|
|
+ createInYida(yidaClient, accessToken, searchCondition, person,
|
|
|
+ dingTalkUserIdsByJobNumber);
|
|
|
+ if (exists) {
|
|
|
+ updated++;
|
|
|
+ } else {
|
|
|
+ added++;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ failed++;
|
|
|
+ log.error("单条人员同步失败,人员ID:{},原因:{}", personId,
|
|
|
+ StringUtils.hasText(e.getMessage()) ? e.getMessage() : e.getClass().getSimpleName());
|
|
|
+ }
|
|
|
+ log.info("第 {} 条处理完成,当前条数/总条数:{}/{}", current, current, persons.size());
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ failed += persons.size();
|
|
|
+ log.error("宜搭客户端初始化失败,本轮人员写入终止");
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("宜搭人员同步统计:新增 {},更新 {},失败 {}", added, updated, failed);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected List<JSONObject> fetchAllPersons() throws Exception {
|
|
|
+ List<JSONObject> allPersons = new ArrayList<>();
|
|
|
+ int pageNo = 1;
|
|
|
+ int total = Integer.MAX_VALUE;
|
|
|
+ while (allPersons.size() < total) {
|
|
|
+ JSONObject requestJson = new JSONObject();
|
|
|
+ requestJson.put("pageNo", pageNo);
|
|
|
+ requestJson.put("pageSize", hikvisionPageSize);
|
|
|
+ JSONObject responseJson = executeHikvisionRequest(requestJson.toJSONString(), HIKVISION_SIGN_PATH);
|
|
|
+ if (!"0".equals(responseJson.getString("code"))) {
|
|
|
+ throw new IllegalStateException("海康接口返回非成功状态");
|
|
|
+ }
|
|
|
+ JSONObject data = responseJson.getJSONObject("data");
|
|
|
+ if (data == null) {
|
|
|
+ throw new IllegalStateException("海康接口未返回 data");
|
|
|
+ }
|
|
|
+ total = data.getIntValue("total");
|
|
|
+ JSONArray list = data.getJSONArray("list");
|
|
|
+ if (list == null) {
|
|
|
+ list = data.getJSONArray("rows");
|
|
|
+ }
|
|
|
+ if (list == null || list.isEmpty()) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ for (int i = 0; i < list.size(); i++) {
|
|
|
+ JSONObject source = list.getJSONObject(i);
|
|
|
+ JSONObject person = new JSONObject();
|
|
|
+ person.put("personId", source.getString("personId"));
|
|
|
+ person.put("personName", source.getString("personName"));
|
|
|
+ person.put("orgIndexCode", source.getString("orgIndexCode"));
|
|
|
+ person.put("phoneNo", source.getString("phoneNo"));
|
|
|
+ person.put("jobNo", source.getString("jobNo"));
|
|
|
+ person.put("certificateNo", source.getString("certificateNo"));
|
|
|
+ person.put("orgPathName", source.getString("orgPathName"));
|
|
|
+ allPersons.add(person);
|
|
|
+ }
|
|
|
+ pageNo++;
|
|
|
+ }
|
|
|
+ return allPersons;
|
|
|
+ }
|
|
|
+
|
|
|
+ private JSONObject executeHikvisionRequest(String requestBody, String signPath) throws Exception {
|
|
|
+ String timestamp = String.valueOf(System.currentTimeMillis());
|
|
|
+ String nonce = UUID.randomUUID().toString();
|
|
|
+ String contentMd5 = createContentMd5(requestBody);
|
|
|
+ String signature = createSignature(timestamp, nonce, contentMd5, signPath);
|
|
|
+ String url = trimTrailingSlash(hikvisionBaseUrl) + signPath;
|
|
|
+
|
|
|
+ Request.Builder builder = new Request.Builder()
|
|
|
+ .url(url)
|
|
|
+ .post(RequestBody.create(JSON_MEDIA_TYPE, requestBody))
|
|
|
+ .header("Accept", "*/*")
|
|
|
+ .header("Content-MD5", contentMd5)
|
|
|
+ .header("Content-Type", CONTENT_TYPE)
|
|
|
+ .header("x-ca-key", hikvisionAppKey)
|
|
|
+ .header("x-ca-timestamp", timestamp)
|
|
|
+ .header("x-ca-nonce", nonce)
|
|
|
+ .header("x-ca-signature-headers", SIGNATURE_HEADERS)
|
|
|
+ .header("x-ca-signature", signature);
|
|
|
+ if (StringUtils.hasText(hikvisionPartnerCode)) {
|
|
|
+ builder.header("x-partner-code", hikvisionPartnerCode);
|
|
|
+ }
|
|
|
+
|
|
|
+ System.out.println("海康接口请求地址:" + url);
|
|
|
+ System.out.println("海康接口请求参数:" + requestBody);
|
|
|
+ long startTime = System.currentTimeMillis();
|
|
|
+ try (Response response = httpClient.newCall(builder.build()).execute()) {
|
|
|
+ String responseBody = response.body() == null ? null : response.body().string();
|
|
|
+ System.out.println("海康接口响应状态码:" + response.code());
|
|
|
+ System.out.println("海康接口响应参数:" + responseBody);
|
|
|
+ if (!response.isSuccessful() || responseBody == null) {
|
|
|
+ throw new IllegalStateException("海康接口 HTTP 请求失败,状态码 " + response.code());
|
|
|
+ }
|
|
|
+ return JSON.parseObject(responseBody);
|
|
|
+ } finally {
|
|
|
+ log.info("海康人员接口调用耗时:{} 毫秒", System.currentTimeMillis() - startTime);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String createContentMd5(String requestBody) throws Exception {
|
|
|
+ MessageDigest md5 = MessageDigest.getInstance("MD5");
|
|
|
+ byte[] digest = md5.digest(requestBody.getBytes(StandardCharsets.UTF_8));
|
|
|
+ return Base64.getEncoder().encodeToString(digest);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String createSignature(String timestamp, String nonce, String contentMd5, String signPath) throws Exception {
|
|
|
+ String stringToSign = "POST\n*/*\n" + contentMd5 + "\n" + CONTENT_TYPE + "\n"
|
|
|
+ + "x-ca-key:" + hikvisionAppKey + "\n"
|
|
|
+ + "x-ca-nonce:" + nonce + "\n"
|
|
|
+ + "x-ca-timestamp:" + timestamp + "\n"
|
|
|
+ + signPath;
|
|
|
+ Mac mac = Mac.getInstance("HmacSHA256");
|
|
|
+ mac.init(new SecretKeySpec(hikvisionAppSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
|
|
+ return Base64.getEncoder().encodeToString(mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8)));
|
|
|
+ }
|
|
|
+
|
|
|
+ protected com.aliyun.dingtalkyida_2_0.Client createYidaClient() throws Exception {
|
|
|
+ Config config = new Config();
|
|
|
+ config.protocol = "https";
|
|
|
+ config.regionId = "central";
|
|
|
+ return new com.aliyun.dingtalkyida_2_0.Client(config);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildSearchFieldJson(String personId) {
|
|
|
+ JSONObject searchField = new JSONObject();
|
|
|
+ searchField.put("textField_ms5onsx3", personId);
|
|
|
+ return searchField.toJSONString();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildSearchCondition(String personId) {
|
|
|
+ JSONObject condition = new JSONObject();
|
|
|
+ condition.put("componentName", "TextField");
|
|
|
+ condition.put("type", "TEXT");
|
|
|
+ condition.put("value", personId);
|
|
|
+ condition.put("key", "textField_ms5onsx3");
|
|
|
+ condition.put("operator", "eq");
|
|
|
+ JSONArray conditions = new JSONArray();
|
|
|
+ conditions.add(condition);
|
|
|
+ return conditions.toJSONString();
|
|
|
+ }
|
|
|
+
|
|
|
+ protected boolean existsInYida(com.aliyun.dingtalkyida_2_0.Client client, String accessToken,
|
|
|
+ String searchFieldJson) throws Exception {
|
|
|
+ SearchFormDatasHeaders headers = new SearchFormDatasHeaders();
|
|
|
+ headers.xAcsDingtalkAccessToken = accessToken;
|
|
|
+ SearchFormDatasRequest request = new SearchFormDatasRequest()
|
|
|
+ .setSystemToken(yidaSystemToken)
|
|
|
+ .setSearchFieldJson(searchFieldJson)
|
|
|
+ .setCurrentPage(1)
|
|
|
+ .setPageSize(1)
|
|
|
+ .setFormUuid(yidaFormUuid)
|
|
|
+ .setUserId(yidaUserId)
|
|
|
+ .setAppType(yidaAppType);
|
|
|
+ SearchFormDatasResponse response = client.searchFormDatasWithOptions(request, headers, new RuntimeOptions());
|
|
|
+ return response.getBody() != null && response.getBody().getData() != null
|
|
|
+ && !response.getBody().getData().isEmpty();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void createInYida(com.aliyun.dingtalkyida_2_0.Client client, String accessToken,
|
|
|
+ String searchCondition, JSONObject person,
|
|
|
+ Map<String, String> dingTalkUserIdsByJobNumber) throws Exception {
|
|
|
+ SupervisorInfo supervisor = findSupervisorBestEffort(person, accessToken, dingTalkUserIdsByJobNumber);
|
|
|
+ String orgPathName = fetchOrgPathNameBestEffort(person.getString("personId"),
|
|
|
+ person.getString("orgPathName"));
|
|
|
+ JSONObject formData = buildPersonFormData(person, orgPathName, supervisor);
|
|
|
+ CreateOrUpdateFormDataHeaders headers = new CreateOrUpdateFormDataHeaders();
|
|
|
+ headers.xAcsDingtalkAccessToken = accessToken;
|
|
|
+ CreateOrUpdateFormDataRequest request = new CreateOrUpdateFormDataRequest()
|
|
|
+ .setSystemToken(yidaSystemToken)
|
|
|
+ .setFormUuid(yidaFormUuid)
|
|
|
+ .setUserId(yidaUserId)
|
|
|
+ .setSearchCondition(searchCondition)
|
|
|
+ .setAppType(yidaAppType)
|
|
|
+ .setFormDataJson(formData.toJSONString());
|
|
|
+ client.createOrUpdateFormDataWithOptions(request, headers, new RuntimeOptions());
|
|
|
+ }
|
|
|
+
|
|
|
+ private SupervisorInfo findSupervisorBestEffort(JSONObject person, String accessToken,
|
|
|
+ Map<String, String> userIdsByJobNumber) {
|
|
|
+ String jobNumber = person.getString("jobNo");
|
|
|
+ if (userIdsByJobNumber == null || !StringUtils.hasText(jobNumber)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return findSupervisor(jobNumber.trim(), accessToken, userIdsByJobNumber);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("主管查询失败,继续同步基础档案并保留已有主管字段,人员ID:{},工号:{},原因:{}",
|
|
|
+ person.getString("personId"), jobNumber,
|
|
|
+ StringUtils.hasText(e.getMessage()) ? e.getMessage() : e.getClass().getSimpleName());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // null 表示查询失败或无法定位员工,省略主管字段以保留宜搭已有值。
|
|
|
+ private JSONObject buildPersonFormData(JSONObject person, String orgPathName, SupervisorInfo supervisor) {
|
|
|
+ JSONObject formData = buildPersonFormData(person, orgPathName,
|
|
|
+ supervisor == null ? null : supervisor.getUserId(),
|
|
|
+ supervisor == null ? null : supervisor.getJobNumber());
|
|
|
+ if (supervisor == null) {
|
|
|
+ formData.remove(YIDA_SUPERVISOR_FIELD);
|
|
|
+ formData.remove(YIDA_SUPERVISOR_JOB_NUMBER_FIELD);
|
|
|
+ }
|
|
|
+ return formData;
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject buildPersonFormData(JSONObject person, String orgPathName, String supervisorUserId) {
|
|
|
+ return buildPersonFormData(person, orgPathName, supervisorUserId, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject buildPersonFormData(JSONObject person, String orgPathName, String supervisorUserId,
|
|
|
+ String supervisorJobNumber) {
|
|
|
+ JSONObject formData = new JSONObject();
|
|
|
+ formData.put("textField_ms5onsx3", person.getString("personId"));
|
|
|
+ formData.put("textField_ms5onsx4", person.getString("personName"));
|
|
|
+ formData.put("textField_ms5onsx5", person.getString("orgIndexCode"));
|
|
|
+ formData.put("textField_ms5onsx7", person.getString("phoneNo"));
|
|
|
+ formData.put(YIDA_JOB_NUMBER_FIELD, person.getString("jobNo"));
|
|
|
+ formData.put("employeeField_mu0lsyn1", person.getString("jobNo"));
|
|
|
+ formData.put("textField_ms5onsxc", person.getString("certificateNo"));
|
|
|
+ formData.put("textField_msy09ao8", orgPathName);
|
|
|
+ JSONArray supervisorUserIds = new JSONArray();
|
|
|
+ if (StringUtils.hasText(supervisorUserId)) {
|
|
|
+ supervisorUserIds.add(supervisorUserId.trim());
|
|
|
+ }
|
|
|
+ formData.put(YIDA_SUPERVISOR_FIELD, supervisorUserIds);
|
|
|
+ formData.put(YIDA_SUPERVISOR_JOB_NUMBER_FIELD,
|
|
|
+ StringUtils.hasText(supervisorJobNumber) ? supervisorJobNumber.trim() : "");
|
|
|
+ return formData;
|
|
|
+ }
|
|
|
+
|
|
|
+ SupervisorInfo findSupervisor(String jobNumber, String accessToken,
|
|
|
+ Map<String, String> dingTalkUserIdsByJobNumber) throws Exception {
|
|
|
+ String employeeUserId = dingTalkUserIdsByJobNumber.get(jobNumber);
|
|
|
+ if (!StringUtils.hasText(employeeUserId)) {
|
|
|
+ log.warn("钉钉通讯录未找到工号对应用户,保留已有主管字段,工号:{}", jobNumber);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ OapiV2UserGetResponse.UserGetResponse employee = getDingTalkUser(employeeUserId, accessToken);
|
|
|
+ String supervisorUserId = findSupervisorUserId(employee, accessToken);
|
|
|
+ if (!StringUtils.hasText(supervisorUserId)) {
|
|
|
+ log.warn("钉钉通讯录未找到用户的上级主管,工号:{},userId:{}", jobNumber, employeeUserId);
|
|
|
+ return SupervisorInfo.empty();
|
|
|
+ }
|
|
|
+
|
|
|
+ OapiV2UserGetResponse.UserGetResponse supervisor = getDingTalkUser(supervisorUserId, accessToken);
|
|
|
+ String supervisorJobNumber = supervisor == null ? null : supervisor.getJobNumber();
|
|
|
+ if (!StringUtils.hasText(supervisorJobNumber)) {
|
|
|
+ log.warn("钉钉主管未配置工号,主管 userId:{}", supervisorUserId);
|
|
|
+ }
|
|
|
+ return new SupervisorInfo(supervisorUserId, supervisorJobNumber);
|
|
|
+ }
|
|
|
+
|
|
|
+ String findSupervisorUserId(String jobNumber, String accessToken) throws Exception {
|
|
|
+ OapiV2UserGetResponse.UserGetResponse user = findDingTalkUserByJobNumber(jobNumber, accessToken);
|
|
|
+ if (user == null) {
|
|
|
+ log.warn("钉钉通讯录未找到工号对应用户,工号:{}", jobNumber);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return findSupervisorUserId(user, accessToken);
|
|
|
+ }
|
|
|
+
|
|
|
+ private String findSupervisorUserId(OapiV2UserGetResponse.UserGetResponse user,
|
|
|
+ String accessToken) throws Exception {
|
|
|
+ String directManagerUserId = user.getManagerUserid();
|
|
|
+ if (isUsableSupervisor(directManagerUserId, user.getUserid())) {
|
|
|
+ return directManagerUserId.trim();
|
|
|
+ }
|
|
|
+ return findDepartmentSupervisorUserId(user, accessToken);
|
|
|
+ }
|
|
|
+
|
|
|
+ protected OapiV2UserGetResponse.UserGetResponse findDingTalkUserByJobNumber(
|
|
|
+ String jobNumber, String accessToken) throws Exception {
|
|
|
+ String userId = loadDingTalkUserIdsByJobNumber(accessToken).get(jobNumber);
|
|
|
+ if (!StringUtils.hasText(userId)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return getDingTalkUser(userId, accessToken);
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, String> loadDingTalkUserIdsByJobNumber(String accessToken) throws Exception {
|
|
|
+ Map<String, String> userIdsByJobNumber = new HashMap<>();
|
|
|
+ Deque<Long> pendingDepartmentIds = new ArrayDeque<>();
|
|
|
+ Set<Long> visitedDepartmentIds = new LinkedHashSet<>();
|
|
|
+ pendingDepartmentIds.add(DINGTALK_ROOT_DEPARTMENT_ID);
|
|
|
+
|
|
|
+ while (!pendingDepartmentIds.isEmpty()) {
|
|
|
+ Long departmentId = pendingDepartmentIds.removeFirst();
|
|
|
+ if (departmentId == null || !visitedDepartmentIds.add(departmentId)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ List<OapiV2UserListResponse.ListUserResponse> users =
|
|
|
+ listDingTalkDepartmentUsers(departmentId, accessToken);
|
|
|
+ for (OapiV2UserListResponse.ListUserResponse user : users) {
|
|
|
+ addDingTalkUserToJobNumberIndex(userIdsByJobNumber, user);
|
|
|
+ }
|
|
|
+
|
|
|
+ List<Long> childDepartmentIds = listDingTalkSubDepartmentIds(departmentId, accessToken);
|
|
|
+ for (Long childDepartmentId : childDepartmentIds) {
|
|
|
+ if (childDepartmentId != null && !visitedDepartmentIds.contains(childDepartmentId)) {
|
|
|
+ pendingDepartmentIds.addLast(childDepartmentId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (userIdsByJobNumber.isEmpty()) {
|
|
|
+ throw new IllegalStateException("钉钉通讯录未返回任何带工号的用户");
|
|
|
+ }
|
|
|
+ return userIdsByJobNumber;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void addDingTalkUserToJobNumberIndex(Map<String, String> userIdsByJobNumber,
|
|
|
+ OapiV2UserListResponse.ListUserResponse user) {
|
|
|
+ if (user == null || !StringUtils.hasText(user.getJobNumber())
|
|
|
+ || !StringUtils.hasText(user.getUserid())) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ String jobNumber = user.getJobNumber().trim();
|
|
|
+ String userId = user.getUserid().trim();
|
|
|
+ String existingUserId = userIdsByJobNumber.putIfAbsent(jobNumber, userId);
|
|
|
+ if (StringUtils.hasText(existingUserId) && !existingUserId.equals(userId)) {
|
|
|
+ throw new IllegalStateException("钉钉通讯录存在重复工号:" + jobNumber);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ protected List<Long> listDingTalkSubDepartmentIds(Long departmentId,
|
|
|
+ String accessToken) throws Exception {
|
|
|
+ DefaultDingTalkClient client = new DefaultDingTalkClient(
|
|
|
+ "https://oapi.dingtalk.com/topapi/v2/department/listsubid");
|
|
|
+ OapiV2DepartmentListsubidRequest request = new OapiV2DepartmentListsubidRequest();
|
|
|
+ request.setDeptId(departmentId);
|
|
|
+ OapiV2DepartmentListsubidResponse response = client.execute(request, accessToken);
|
|
|
+ if (response == null || !response.isSuccess() || response.getResult() == null) {
|
|
|
+ throw new IllegalStateException("钉钉子部门查询失败,部门ID:" + departmentId);
|
|
|
+ }
|
|
|
+ List<Long> departmentIds = response.getResult().getDeptIdList();
|
|
|
+ return departmentIds == null ? new ArrayList<Long>() : departmentIds;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected List<OapiV2UserListResponse.ListUserResponse> listDingTalkDepartmentUsers(
|
|
|
+ Long departmentId, String accessToken) throws Exception {
|
|
|
+ List<OapiV2UserListResponse.ListUserResponse> users = new ArrayList<>();
|
|
|
+ long cursor = 0L;
|
|
|
+ boolean hasMore;
|
|
|
+ do {
|
|
|
+ DefaultDingTalkClient client = new DefaultDingTalkClient(
|
|
|
+ "https://oapi.dingtalk.com/topapi/v2/user/list");
|
|
|
+ OapiV2UserListRequest request = new OapiV2UserListRequest();
|
|
|
+ request.setDeptId(departmentId);
|
|
|
+ request.setCursor(cursor);
|
|
|
+ request.setSize(DINGTALK_USER_PAGE_SIZE);
|
|
|
+ request.setContainAccessLimit(false);
|
|
|
+ OapiV2UserListResponse response = client.execute(request, accessToken);
|
|
|
+ if (response == null || !response.isSuccess() || response.getResult() == null) {
|
|
|
+ throw new IllegalStateException("钉钉部门成员查询失败,部门ID:" + departmentId);
|
|
|
+ }
|
|
|
+ if (response.getResult().getList() != null) {
|
|
|
+ users.addAll(response.getResult().getList());
|
|
|
+ }
|
|
|
+ hasMore = Boolean.TRUE.equals(response.getResult().getHasMore());
|
|
|
+ Long nextCursor = response.getResult().getNextCursor();
|
|
|
+ if (hasMore && (nextCursor == null || nextCursor == cursor)) {
|
|
|
+ throw new IllegalStateException("钉钉部门成员分页游标无效,部门ID:" + departmentId);
|
|
|
+ }
|
|
|
+ if (nextCursor != null) {
|
|
|
+ cursor = nextCursor;
|
|
|
+ }
|
|
|
+ } while (hasMore);
|
|
|
+ return users;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String findDepartmentSupervisorUserId(OapiV2UserGetResponse.UserGetResponse user,
|
|
|
+ String accessToken) throws Exception {
|
|
|
+ Set<Long> currentDepartmentIds = new LinkedHashSet<>();
|
|
|
+ if (user.getDeptIdList() != null) {
|
|
|
+ currentDepartmentIds.addAll(user.getDeptIdList());
|
|
|
+ }
|
|
|
+ Set<Long> visitedDepartmentIds = new LinkedHashSet<>();
|
|
|
+
|
|
|
+ for (int level = 0; level < MAX_DEPARTMENT_LEVELS && !currentDepartmentIds.isEmpty(); level++) {
|
|
|
+ Set<Long> parentDepartmentIds = new LinkedHashSet<>();
|
|
|
+ for (Long departmentId : currentDepartmentIds) {
|
|
|
+ if (departmentId == null || !visitedDepartmentIds.add(departmentId)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ OapiV2DepartmentGetResponse.DeptGetResponse department =
|
|
|
+ getDingTalkDepartment(departmentId, accessToken);
|
|
|
+ if (department == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String managerUserId = firstUsableSupervisor(
|
|
|
+ department.getDeptManagerUseridList(), user.getUserid());
|
|
|
+ if (managerUserId != null) {
|
|
|
+ return managerUserId;
|
|
|
+ }
|
|
|
+ Long parentId = department.getParentId();
|
|
|
+ if (parentId != null && parentId > 0 && !parentId.equals(departmentId)
|
|
|
+ && !visitedDepartmentIds.contains(parentId)) {
|
|
|
+ parentDepartmentIds.add(parentId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ currentDepartmentIds = parentDepartmentIds;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ protected OapiV2DepartmentGetResponse.DeptGetResponse getDingTalkDepartment(
|
|
|
+ Long departmentId, String accessToken) throws Exception {
|
|
|
+ DefaultDingTalkClient client = new DefaultDingTalkClient(
|
|
|
+ "https://oapi.dingtalk.com/topapi/v2/department/get");
|
|
|
+ OapiV2DepartmentGetRequest request = new OapiV2DepartmentGetRequest();
|
|
|
+ request.setDeptId(departmentId);
|
|
|
+ OapiV2DepartmentGetResponse response = client.execute(request, accessToken);
|
|
|
+ if (response == null || !response.isSuccess() || response.getResult() == null) {
|
|
|
+ throw new IllegalStateException("钉钉部门详情查询失败,部门ID:" + departmentId);
|
|
|
+ }
|
|
|
+ return response.getResult();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String firstUsableSupervisor(List<String> managerUserIds, String employeeUserId) {
|
|
|
+ if (managerUserIds == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ for (String managerUserId : managerUserIds) {
|
|
|
+ if (isUsableSupervisor(managerUserId, employeeUserId)) {
|
|
|
+ return managerUserId.trim();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isUsableSupervisor(String managerUserId, String employeeUserId) {
|
|
|
+ return StringUtils.hasText(managerUserId)
|
|
|
+ && (!StringUtils.hasText(employeeUserId) || !managerUserId.trim().equals(employeeUserId.trim()));
|
|
|
+ }
|
|
|
+
|
|
|
+ protected OapiV2UserGetResponse.UserGetResponse getDingTalkUser(
|
|
|
+ String userId, String accessToken) throws Exception {
|
|
|
+ DefaultDingTalkClient client = new DefaultDingTalkClient(
|
|
|
+ "https://oapi.dingtalk.com/topapi/v2/user/get");
|
|
|
+ OapiV2UserGetRequest request = new OapiV2UserGetRequest();
|
|
|
+ request.setUserid(userId);
|
|
|
+ OapiV2UserGetResponse response = client.execute(request, accessToken);
|
|
|
+ if (response == null || !response.isSuccess() || response.getResult() == null) {
|
|
|
+ throw new IllegalStateException("钉钉用户详情查询失败,userId:" + userId);
|
|
|
+ }
|
|
|
+ return response.getResult();
|
|
|
+ }
|
|
|
+
|
|
|
+ static final class SupervisorInfo {
|
|
|
+ private final String userId;
|
|
|
+ private final String jobNumber;
|
|
|
+
|
|
|
+ SupervisorInfo(String userId, String jobNumber) {
|
|
|
+ this.userId = userId;
|
|
|
+ this.jobNumber = jobNumber;
|
|
|
+ }
|
|
|
+
|
|
|
+ static SupervisorInfo empty() {
|
|
|
+ return new SupervisorInfo(null, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ String getUserId() {
|
|
|
+ return userId;
|
|
|
+ }
|
|
|
+
|
|
|
+ String getJobNumber() {
|
|
|
+ return jobNumber;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通过 personId 调海康人员详情接口获取 orgPathName,取值失败时回退到人员列表原值
|
|
|
+ */
|
|
|
+ protected String fetchOrgPathNameBestEffort(String personId, String fallbackOrgPathName) {
|
|
|
+ try {
|
|
|
+ String orgPathName = fetchOrgPathName(personId);
|
|
|
+ if (StringUtils.hasText(orgPathName)) {
|
|
|
+ return orgPathName;
|
|
|
+ }
|
|
|
+ log.warn("海康人员详情未返回 orgPathName,人员ID:{},使用人员列表原值", personId);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("海康人员详情查询失败,人员ID:{},使用人员列表原值,原因:{}", personId,
|
|
|
+ StringUtils.hasText(e.getMessage()) ? e.getMessage() : e.getClass().getSimpleName());
|
|
|
+ }
|
|
|
+ return fallbackOrgPathName;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String fetchOrgPathName(String personId) throws Exception {
|
|
|
+ JSONArray paramValue = new JSONArray();
|
|
|
+ paramValue.add(personId);
|
|
|
+ JSONObject requestJson = new JSONObject();
|
|
|
+ requestJson.put("paramName", "personId");
|
|
|
+ requestJson.put("paramValue", paramValue);
|
|
|
+
|
|
|
+ JSONObject responseJson = executeHikvisionRequest(requestJson.toJSONString(), PERSON_INFO_SIGN_PATH);
|
|
|
+ if (!"0".equals(responseJson.getString("code"))) {
|
|
|
+ throw new IllegalStateException("海康人员详情接口返回非成功状态");
|
|
|
+ }
|
|
|
+ JSONObject data = responseJson.getJSONObject("data");
|
|
|
+ JSONArray list = data == null ? null : data.getJSONArray("list");
|
|
|
+ if (list == null || list.isEmpty() || !(list.get(0) instanceof JSONObject)) {
|
|
|
+ throw new IllegalStateException("海康人员详情接口未返回 data.list[0]");
|
|
|
+ }
|
|
|
+ return list.getJSONObject(0).getString("orgPathName");
|
|
|
+ }
|
|
|
+
|
|
|
+ private String trimTrailingSlash(String value) {
|
|
|
+ while (value.endsWith("/")) {
|
|
|
+ value = value.substring(0, value.length() - 1);
|
|
|
+ }
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+}
|