Ver código fonte

接口开发11.3

ht 2 dias atrás
pai
commit
509357e39c

+ 8 - 0
src/main/java/com/malk/minglei/service/employee/EmployeeRosterService.java

@@ -34,6 +34,14 @@ public interface EmployeeRosterService {
      */
     EmployeeRosterInfo findByUserId(String userId);
 
+    /**
+     * 根据钉钉用户 ID 查询同步至本地花名册的部门 ID。
+     *
+     * @param userId 钉钉用户 ID
+     * @return dept_id_list 首个部门 ID;员工未同步或未配置部门时返回 null
+     */
+    String findDepartmentIdByUserId(String userId);
+
     /**
      * 根据员工工号查询员工花名册。
      *

+ 19 - 0
src/main/java/com/malk/minglei/service/impl/employee/EmployeeRosterRecord.java

@@ -14,6 +14,7 @@ public class EmployeeRosterRecord {
     private String jobGradeId;
     private String department;
     private String firstDepartment;
+    private Long departmentId;
     private String hireDate;
     private String positionName;
     private String bankCardNo;
@@ -92,6 +93,24 @@ public class EmployeeRosterRecord {
         this.firstDepartment = firstDepartment;
     }
 
+    /**
+     * 获取钉钉部门 ID。
+     *
+     * @return 钉钉返回的 dept_id_list 首个部门 ID
+     */
+    public Long getDepartmentId() {
+        return departmentId;
+    }
+
+    /**
+     * 设置钉钉部门 ID。
+     *
+     * @param departmentId 钉钉返回的 dept_id_list 首个部门 ID
+     */
+    public void setDepartmentId(Long departmentId) {
+        this.departmentId = departmentId;
+    }
+
     public String getHireDate() {
         return hireDate;
     }

+ 25 - 2
src/main/java/com/malk/minglei/service/impl/employee/EmployeeRosterRepository.java

@@ -10,6 +10,7 @@ import org.springframework.stereotype.Repository;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.sql.Types;
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
 import java.util.LinkedHashMap;
@@ -66,6 +67,22 @@ public class EmployeeRosterRepository {
         return findOne("dingtalk_id", dingtalkId);
     }
 
+    /**
+     * 根据钉钉用户 ID 查询已同步的部门 ID。
+     *
+     * @param dingtalkId 钉钉用户 ID
+     * @return dept_id_list 首个部门 ID;未找到时返回 null
+     */
+    public String findDepartmentIdByDingtalkId(String dingtalkId) {
+        JdbcTemplate jdbcTemplate = requireJdbcTemplate();
+        String sql = "SELECT department_id FROM " + resolveTableName()
+                + " WHERE dingtalk_id = ? AND is_resigned = 1 LIMIT 1";
+        List<String> rows = jdbcTemplate.query(sql,
+                preparedStatement -> preparedStatement.setString(1, dingtalkId),
+                (resultSet, rowNum) -> resultSet.getString("department_id"));
+        return rows.isEmpty() ? null : rows.get(0);
+    }
+
     /**
      * 根据员工工号查询员工花名册。
      *
@@ -178,6 +195,11 @@ public class EmployeeRosterRepository {
         statement.setString(index++, record.getJobGradeId());
         statement.setString(index++, record.getDepartment());
         statement.setString(index++, record.getFirstDepartment());
+        if (record.getDepartmentId() == null) {
+            statement.setNull(index++, Types.BIGINT);
+        } else {
+            statement.setLong(index++, record.getDepartmentId().longValue());
+        }
         statement.setString(index++, record.getHireDate());
         statement.setString(index++, record.getPositionName());
         statement.setString(index++, record.getBankCardNo());
@@ -195,8 +217,8 @@ public class EmployeeRosterRepository {
 
     private String createMergeSql(String tableName) {
         return "INSERT INTO " + tableName + " " +
-                "(dingtalk_id, employee_no, username, job_title, job_title_id, job_grade, job_grade_id, department, first_department, hire_date, position_name, bank_card_no, bank_name, bank_branch_code) " +
-                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " +
+                "(dingtalk_id, employee_no, username, job_title, job_title_id, job_grade, job_grade_id, department, first_department, department_id, hire_date, position_name, bank_card_no, bank_name, bank_branch_code) " +
+                "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " +
                 "ON DUPLICATE KEY UPDATE " +
                 "dingtalk_id = VALUES(dingtalk_id), " +
                 "employee_no = VALUES(employee_no), " +
@@ -207,6 +229,7 @@ public class EmployeeRosterRepository {
                 "job_grade_id = VALUES(job_grade_id), " +
                 "department = VALUES(department), " +
                 "first_department = VALUES(first_department), " +
+                "department_id = VALUES(department_id), " +
                 "hire_date = VALUES(hire_date), " +
                 "position_name = VALUES(position_name), " +
                 "bank_card_no = VALUES(bank_card_no), " +

+ 44 - 0
src/main/java/com/malk/minglei/service/impl/employee/EmployeeRosterServiceImpl.java

@@ -139,6 +139,20 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
         return repository.findByDingtalkId(userId.trim());
     }
 
+    /**
+     * 根据钉钉用户 ID 查询已同步到本地花名册的部门 ID。
+     *
+     * @param userId 钉钉用户 ID
+     * @return dept_id_list 首个部门 ID;不存在时返回 null
+     */
+    @Override
+    public String findDepartmentIdByUserId(String userId) {
+        if (userId == null || userId.trim().isEmpty()) {
+            throw new IllegalArgumentException("userId must not be blank");
+        }
+        return repository.findDepartmentIdByDingtalkId(userId.trim());
+    }
+
     /**
      * 根据员工工号查询本地员工花名册记录。
      *
@@ -317,6 +331,7 @@ 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.setDepartmentId(firstDepartmentId(user.get("dept_id_list")));
         record.setBankCardNo(findRosterValue(rosterValues, "银行卡号", "bank_card_no"));
         record.setBankName(findRosterValue(rosterValues, "开户行", "bank_name"));
         record.setBankBranchCode(findRosterValue(rosterValues, "支行联行号", "bank_branch_code"));
@@ -568,6 +583,35 @@ public class EmployeeRosterServiceImpl implements EmployeeRosterService {
         return Long.valueOf(text);
     }
 
+    /**
+     * 按钉钉返回顺序取员工所属部门列表中的首个有效部门 ID。
+     *
+     * @param departmentIds 钉钉用户详情中的 dept_id_list
+     * @return 首个有效部门 ID;未返回时为 null
+     */
+    private Long firstDepartmentId(Object departmentIds) {
+        if (departmentIds instanceof Iterable) {
+            for (Object departmentId : (Iterable<?>) departmentIds) {
+                Long value = toLong(departmentId);
+                if (value != null) {
+                    return value;
+                }
+            }
+            return null;
+        }
+        if (departmentIds != null && departmentIds.getClass().isArray()) {
+            Object[] values = (Object[]) departmentIds;
+            for (Object departmentId : values) {
+                Long value = toLong(departmentId);
+                if (value != null) {
+                    return value;
+                }
+            }
+            return null;
+        }
+        return toLong(departmentIds);
+    }
+
     private int normalizePageSize(int pageSize) {
         if (pageSize < 1) {
             return DEFAULT_USER_PAGE_SIZE;

+ 67 - 56
src/main/java/com/malk/minglei/service/impl/yida/MingleiDocumentServiceImpl.java

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.malk.minglei.config.YidaApplicationsProperties;
 import com.malk.minglei.dto.MingleiDocumentQueryRequest;
+import com.malk.minglei.service.employee.EmployeeRosterService;
 import com.malk.minglei.service.yida.MingleiDocumentService;
 import com.malk.server.aliwork.YDConf;
 import com.malk.server.aliwork.YDParam;
@@ -12,6 +13,8 @@ import com.malk.server.dingtalk.DDConf;
 import com.malk.service.aliwork.YDClient;
 import com.malk.service.dingtalk.DDClient;
 import com.malk.service.dingtalk.DDClient_Contacts;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 
@@ -29,6 +32,7 @@ import java.util.Set;
 @Service
 public class MingleiDocumentServiceImpl implements MingleiDocumentService {
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(MingleiDocumentServiceImpl.class);
     private static final String MINGLEI_DOCUMENT_FORM_UUID = "FORM-8F527F696C5B4AF7AB7A53979527FBA23USO";
     private static final String DEPARTMENT_ID_FIELD_ID = "textField_msztwhs2";
     private static final String DEPARTMENT_NAME_FIELD_ID = "textField_mszr6tuz";
@@ -45,7 +49,8 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
             FOURTH_LEVEL_DEPARTMENT_MANAGER_FIELD_ID,
             DEPARTMENT_LEADER_FIELD_ID
     };
-    private static final int PRECISE_QUERY_PAGE_SIZE = 100;
+    private static final int APPROVAL_MATRIX_QUERY_PAGE_SIZE = 1;
+    private static final int APPLICANT_CONFIGURATION_QUERY_PAGE_SIZE = 100;
 
     private final YDClient yidaClient;
     private final DDClient dingTalkClient;
@@ -53,6 +58,7 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
     private final DDConf dingTalkConfiguration;
     private final YidaApplicationsProperties.ApplicationCredentials masterDataConfiguration;
     private final ObjectMapper objectMapper;
+    private final EmployeeRosterService employeeRosterService;
 
     /**
      * 创建明磊主数据表单查询服务。
@@ -63,19 +69,22 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
      * @param dingTalkConfiguration 钉钉应用配置
      * @param applications 宜搭应用配置
      * @param objectMapper JSON 序列化组件
+     * @param employeeRosterService 员工花名册服务
      */
     public MingleiDocumentServiceImpl(@Qualifier("masterDataYidaClient") YDClient yidaClient,
                                       DDClient dingTalkClient,
                                       DDClient_Contacts dingTalkContactsClient,
                                       DDConf dingTalkConfiguration,
                                       YidaApplicationsProperties applications,
-                                      ObjectMapper objectMapper) {
+                                       ObjectMapper objectMapper,
+                                       EmployeeRosterService employeeRosterService) {
         this.yidaClient = yidaClient;
         this.dingTalkClient = dingTalkClient;
         this.dingTalkContactsClient = dingTalkContactsClient;
         this.dingTalkConfiguration = dingTalkConfiguration;
         this.masterDataConfiguration = applications.getMasterData();
         this.objectMapper = objectMapper;
+        this.employeeRosterService = employeeRosterService;
     }
 
     /**
@@ -101,23 +110,40 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
         if (request == null) {
             throw new IllegalArgumentException("Query request must not be null");
         }
+        long startedAt = System.nanoTime();
+        String queryType = null;
+        try {
+            String departmentId = trimToNull(request.getDeptId());
+            if (departmentId != null) {
+                queryType = "dept_id";
+                return queryApprovalMatrix(queryType, DEPARTMENT_ID_FIELD_ID, departmentId);
+            }
 
-        String departmentId = trimToNull(request.getDeptId());
-        if (departmentId != null) {
-            return extractDepartmentManagers(executeQuery(DEPARTMENT_ID_FIELD_ID, departmentId, 1, PRECISE_QUERY_PAGE_SIZE));
-        }
+            String userId = trimToNull(request.getUserId());
+            if (userId != null) {
+                queryType = "user_id";
+                long userDepartmentStartedAt = System.nanoTime();
+                String resolvedDepartmentId;
+                try {
+                    resolvedDepartmentId = resolveDepartmentId(userId);
+                } finally {
+                    LOGGER.info("Approval matrix query stage=RESOLVE_USER_DEPARTMENT durationMs={}",
+                            elapsedMillis(userDepartmentStartedAt));
+                }
+                return queryApprovalMatrix(queryType, DEPARTMENT_ID_FIELD_ID, resolvedDepartmentId);
+            }
 
-        String userId = trimToNull(request.getUserId());
-        if (userId != null) {
-            return extractDepartmentManagers(executeQuery(DEPARTMENT_ID_FIELD_ID, resolveDepartmentId(userId), 1, PRECISE_QUERY_PAGE_SIZE));
-        }
+            String departmentName = trimToNull(request.getDeptName());
+            if (departmentName != null) {
+                queryType = "dept_name";
+                return queryApprovalMatrix(queryType, DEPARTMENT_NAME_FIELD_ID, departmentName);
+            }
 
-        String departmentName = trimToNull(request.getDeptName());
-        if (departmentName != null) {
-            return extractDepartmentManagers(executeQuery(DEPARTMENT_NAME_FIELD_ID, departmentName, 1, PRECISE_QUERY_PAGE_SIZE));
+            throw new IllegalArgumentException("One of dept_id, user_id or dept_name must be provided");
+        } finally {
+            LOGGER.info("Approval matrix query completed queryType={} durationMs={}", queryType,
+                    elapsedMillis(startedAt));
         }
-
-        throw new IllegalArgumentException("One of dept_id, user_id or dept_name must be provided");
     }
 
     /**
@@ -133,7 +159,7 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
             throw new IllegalArgumentException("departmentId must not be blank");
         }
         return extractDepartmentManagers(executeQuery(DEPARTMENT_ID_FIELD_ID, requestedDepartmentId,
-                1, PRECISE_QUERY_PAGE_SIZE));
+                1, APPROVAL_MATRIX_QUERY_PAGE_SIZE));
     }
 
     /**
@@ -147,7 +173,8 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
         String requestedUserId = trimToNull(userId);
         require(requestedUserId, "userId must not be blank");
         String departmentId = resolveDepartmentId(requestedUserId);
-        Map<String, Object> queryResult = executeQuery(DEPARTMENT_ID_FIELD_ID, departmentId, 1, PRECISE_QUERY_PAGE_SIZE);
+        Map<String, Object> queryResult = executeQuery(DEPARTMENT_ID_FIELD_ID, departmentId, 1,
+                APPLICANT_CONFIGURATION_QUERY_PAGE_SIZE);
         Map formData = firstFormData(queryResult.get("records"));
         Map<String, Object> result = new LinkedHashMap<String, Object>();
         result.put("departmentId", departmentId);
@@ -253,6 +280,26 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
         return result;
     }
 
+    /**
+     * 查询审批矩阵时每个部门只应有一条有效配置,因此仅请求首条记录,避免传输和解析无关表单数据。
+     */
+    private Map<String, Object> queryApprovalMatrix(String queryType, String fieldId, String fieldValue) {
+        long startedAt = System.nanoTime();
+        Object totalCount = null;
+        try {
+            Map<String, Object> queryResult = executeQuery(fieldId, fieldValue, 1, APPROVAL_MATRIX_QUERY_PAGE_SIZE);
+            totalCount = queryResult.get("totalCount");
+            return extractDepartmentManagers(queryResult);
+        } finally {
+            LOGGER.info("Approval matrix query stage=YIDA_MATRIX_QUERY queryType={} durationMs={} totalCount={}",
+                    queryType, elapsedMillis(startedAt), totalCount);
+        }
+    }
+
+    private long elapsedMillis(long startedAt) {
+        return (System.nanoTime() - startedAt) / 1_000_000L;
+    }
+
     /**
      * 从精准查询命中的首条单据提取五个主管字段对应的用户 ID,并直接返回到顶层。
      *
@@ -336,28 +383,15 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
     }
 
     /**
-     * 调用钉钉通讯录接口读取员工所属部门,并取返回列表中的首个部门编号作为查询条件。
+     * 从已同步的本地员工花名册读取员工部门编号,避免审批矩阵查询依赖钉钉用户详情接口。
      *
      * @param userId 钉钉用户编号
      * @return 员工所属的部门编号
      */
     private String resolveDepartmentId(String userId) {
-        Map userInfo = dingTalkContactsClient.getUserInfoById(dingTalkClient.getAccessToken(), userId);
-        if (userInfo == null) {
-            throw new IllegalArgumentException("No DingTalk user information was found for user_id");
-        }
-
-        String mainDepartmentId = toStringValue(userInfo.get("main_dept_id"));
-        if (mainDepartmentId != null) {
-            return mainDepartmentId;
-        }
-
-        String departmentId = firstDepartmentId(userInfo.get("department"));
+        String departmentId = employeeRosterService.findDepartmentIdByUserId(userId);
         if (departmentId == null) {
-            departmentId = firstDepartmentId(userInfo.get("dept_id_list"));
-        }
-        if (departmentId == null) {
-            throw new IllegalArgumentException("No department was found for user_id");
+            throw new IllegalArgumentException("No synchronized employee roster department was found for user_id");
         }
         return departmentId;
     }
@@ -383,29 +417,6 @@ public class MingleiDocumentServiceImpl implements MingleiDocumentService {
         }
     }
 
-    private String firstDepartmentId(Object departmentIds) {
-        if (departmentIds instanceof Iterable) {
-            for (Object departmentId : (Iterable<?>) departmentIds) {
-                String value = toStringValue(departmentId);
-                if (value != null) {
-                    return value;
-                }
-            }
-            return null;
-        }
-        if (departmentIds != null && departmentIds.getClass().isArray()) {
-            Object[] values = (Object[]) departmentIds;
-            for (Object departmentId : values) {
-                String value = toStringValue(departmentId);
-                if (value != null) {
-                    return value;
-                }
-            }
-            return null;
-        }
-        return toStringValue(departmentIds);
-    }
-
     private String toStringValue(Object value) {
         return value == null ? null : trimToNull(String.valueOf(value));
     }

+ 3 - 1
src/main/resources/db/employee_roster.sql

@@ -9,6 +9,7 @@ CREATE TABLE employee_roster (
   job_grade_id VARCHAR(64) DEFAULT NULL COMMENT '职等ID',
   department VARCHAR(128) DEFAULT NULL COMMENT '部门',
   first_department VARCHAR(128) DEFAULT NULL COMMENT '一级部门',
+  department_id BIGINT DEFAULT NULL COMMENT '钉钉部门ID(dept_id_list首个)',
   bank_card_no VARCHAR(64) DEFAULT NULL COMMENT '银行卡号',
   bank_name VARCHAR(128) DEFAULT NULL COMMENT '开户行',
   bank_branch_code VARCHAR(64) DEFAULT NULL COMMENT '支行联行号',
@@ -16,5 +17,6 @@ CREATE TABLE employee_roster (
   UNIQUE KEY uk_dingtalk_id (dingtalk_id),
   UNIQUE KEY uk_employee_no (employee_no),
   KEY idx_department (department),
-  KEY idx_first_department (first_department)
+  KEY idx_first_department (first_department),
+  KEY idx_department_id (department_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工花名册表';

+ 26 - 2
src/test/java/com/malk/minglei/service/impl/employee/EmployeeRosterRepositoryTest.java

@@ -117,6 +117,27 @@ class EmployeeRosterRepositoryTest {
         assertTrue(sqlCaptor.getValue().contains("bank_branch_code"));
     }
 
+    @Test
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    void findDepartmentIdByDingtalkIdReadsSynchronizedDepartmentId() {
+        JdbcTemplate jdbcTemplate = mock(JdbcTemplate.class);
+        ObjectProvider<JdbcTemplate> jdbcTemplateProvider = mock(ObjectProvider.class);
+        given(jdbcTemplateProvider.getIfAvailable()).willReturn(jdbcTemplate);
+        given(jdbcTemplate.query(any(String.class), any(PreparedStatementSetter.class), any(RowMapper.class)))
+                .willReturn(Collections.singletonList("1001"));
+
+        EmployeeRosterRepository repository = new EmployeeRosterRepository(
+                jdbcTemplateProvider,
+                new EmployeeRosterProperties());
+
+        assertEquals("1001", repository.findDepartmentIdByDingtalkId("user-1"));
+
+        ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
+        verify(jdbcTemplate).query(sqlCaptor.capture(), any(PreparedStatementSetter.class), any(RowMapper.class));
+        assertTrue(sqlCaptor.getValue().contains("SELECT department_id FROM employee_roster"));
+        assertTrue(sqlCaptor.getValue().contains("dingtalk_id = ? AND is_resigned = 1"));
+    }
+
     @Test
     @SuppressWarnings({"rawtypes", "unchecked"})
     void mapsBankCardNumberAndBankNameForRosterQuery() throws Exception {
@@ -193,6 +214,7 @@ class EmployeeRosterRepositoryTest {
         EmployeeRosterRecord record = new EmployeeRosterRecord();
         record.setDingtalkId("user-1");
         record.setUsername("Alice");
+        record.setDepartmentId(Long.valueOf(1001L));
         record.setHireDate("2024-01-15");
         record.setPositionName("研发工程师");
 
@@ -205,12 +227,14 @@ class EmployeeRosterRepositoryTest {
         ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
         ArgumentCaptor<PreparedStatementSetter> setterCaptor = ArgumentCaptor.forClass(PreparedStatementSetter.class);
         verify(jdbcTemplate).update(sqlCaptor.capture(), setterCaptor.capture());
+        assertTrue(sqlCaptor.getValue().contains("department_id"));
         assertTrue(sqlCaptor.getValue().contains("hire_date"));
         assertTrue(sqlCaptor.getValue().contains("position_name"));
 
         PreparedStatement statement = mock(PreparedStatement.class);
         setterCaptor.getValue().setValues(statement);
-        verify(statement).setString(10, "2024-01-15");
-        verify(statement).setString(11, "研发工程师");
+        verify(statement).setLong(10, 1001L);
+        verify(statement).setString(11, "2024-01-15");
+        verify(statement).setString(12, "研发工程师");
     }
 }

+ 8 - 4
src/test/java/com/malk/minglei/service/impl/employee/EmployeeRosterServiceImplTest.java

@@ -79,9 +79,10 @@ class EmployeeRosterServiceImplTest {
         assertTrue(sql.getValue().contains("bank_branch_code = VALUES(bank_branch_code)"));
         PreparedStatement statement = mock(PreparedStatement.class);
         setter.getValue().setValues(statement);
-        verify(statement).setString(12, "6222020000000000000");
-        verify(statement).setString(13, "Test Bank");
-        verify(statement).setString(14, "001234567890");
+        verify(statement).setLong(10, 1L);
+        verify(statement).setString(13, "6222020000000000000");
+        verify(statement).setString(14, "Test Bank");
+        verify(statement).setString(15, "001234567890");
     }
 
     @Test
@@ -115,8 +116,10 @@ class EmployeeRosterServiceImplTest {
 
         given(dingTalkClient.getAccessToken()).willReturn("access-token");
         given(contactsClient.getDepartmentId_all("access-token", false, 1L)).willReturn(Collections.singletonList(2L));
+        Map<String, Object> userOne = user("user-1", "Alice", "1001");
+        userOne.put("dept_id_list", Arrays.asList(101L, 102L));
         given(contactsClient.listDeptUserDetail_v2(eq("access-token"), eq(1L), anyMap())).willReturn(mapOf(
-                "list", Arrays.asList(user("user-1", "Alice", "1001"), user("user-2", "Bob", "1002")),
+                "list", Arrays.asList(userOne, user("user-2", "Bob", "1002")),
                 "has_more", false));
         given(contactsClient.listDeptUserDetail_v2(eq("access-token"), eq(2L), anyMap())).willReturn(mapOf(
                 "list", Arrays.asList(user("user-2", "Bob", "1002"), user("user-3", "Carol", "1003")),
@@ -163,6 +166,7 @@ class EmployeeRosterServiceImplTest {
         assertEquals("10010", records.get(0).getJobGradeId());
         assertEquals("技术部", records.get(0).getDepartment());
         assertEquals("研发中心", records.get(0).getFirstDepartment());
+        assertEquals(Long.valueOf(101L), records.get(0).getDepartmentId());
     }
 
     @Test

+ 34 - 7
src/test/java/com/malk/minglei/service/impl/yida/CustomerReleaseDepartmentConfigurationTest.java

@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.malk.minglei.config.YidaApplicationsProperties;
 import com.malk.minglei.dto.ErpYidaPushMainData;
 import com.malk.minglei.dto.ErpYidaPushRequest;
+import com.malk.minglei.service.employee.EmployeeRosterService;
 import com.malk.minglei.service.yida.MingleiDocumentService;
 import com.malk.minglei.service.yida.YidaProcessOpenApiClient;
 import com.malk.server.aliwork.YDConf;
@@ -41,7 +42,7 @@ import static org.mockito.Mockito.when;
 class CustomerReleaseDepartmentConfigurationTest {
 
     @Test
-    void usesMainDepartmentConfigurationAndMemberIdsInsteadOfErpValues() {
+    void usesFirstSynchronizedDepartmentConfigurationAndMemberIdsInsteadOfErpValues() {
         Map<String, Object> user = new LinkedHashMap<String, Object>();
         user.put("main_dept_id", 20L);
         user.put("dept_id_list", Arrays.asList(10L, 20L));
@@ -54,11 +55,11 @@ class CustomerReleaseDepartmentConfigurationTest {
         try (Fixture fixture = new Fixture(user, configuration)) {
             JSONObject form = fixture.start();
             assertEquals("Configured Company", form.getString("selectField_mtv7uxgd"));
-            assertEquals(Collections.singletonList("20"), form.getJSONArray("departmentSelectField_msh186ry"));
+            assertEquals(Collections.singletonList("10"), form.getJSONArray("departmentSelectField_msh186ry"));
             assertEquals(Arrays.asList("manager-1", "manager-1b"), form.getJSONArray("employeeField_mt6thsdh"));
             assertEquals(Collections.singletonList("manager-2"), form.getJSONArray("employeeField_mt6thsdi"));
             assertEquals(Collections.singletonList("manager-3"), form.getJSONArray("employeeField_mt6v6alx"));
-            fixture.verifyDepartmentQuery("20");
+            fixture.verifyDepartmentQuery("10");
         }
     }
 
@@ -175,6 +176,7 @@ class CustomerReleaseDepartmentConfigurationTest {
         private final YDClient masterDataClient = mock(YDClient.class);
         private final YidaProcessOpenApiClient processClient = mock(YidaProcessOpenApiClient.class);
         private final DDClient_Contacts contactsClient = mock(DDClient_Contacts.class);
+        private final EmployeeRosterService employeeRosterService = mock(EmployeeRosterService.class);
         private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
         private final ErpYidaPushRequest request = new ErpYidaPushRequest();
         private final CustomerReleaseFormPushHandler handler;
@@ -183,8 +185,8 @@ class CustomerReleaseDepartmentConfigurationTest {
 
         private Fixture(Map<String, Object> user, Map<String, Object> configuration) {
             DDClient dingTalkClient = mock(DDClient.class);
-            when(dingTalkClient.getAccessToken()).thenReturn("access-token");
-            when(contactsClient.getUserInfoById("access-token", "applicant-1")).thenReturn(user);
+            when(employeeRosterService.findDepartmentIdByUserId("applicant-1"))
+                    .thenReturn(firstDepartmentId(user));
             DDConf ddConf = new DDConf();
             ddConf.setAppKey("app-key");
             ddConf.setAppSecret("app-secret");
@@ -207,7 +209,7 @@ class CustomerReleaseDepartmentConfigurationTest {
             startResponse.setResult("process-1");
             when(processClient.startProcess(anyMap())).thenReturn(startResponse);
             MingleiDocumentService documentService = new MingleiDocumentServiceImpl(masterDataClient,
-                    dingTalkClient, contactsClient, ddConf, applications, new ObjectMapper());
+                    dingTalkClient, contactsClient, ddConf, applications, new ObjectMapper(), employeeRosterService);
             context.registerBean(YidaProcessOpenApiClient.class, () -> processClient);
             context.registerBean(DDConf.class, () -> ddConf);
             context.registerBean(YidaApplicationsProperties.class, () -> applications);
@@ -239,7 +241,8 @@ class CustomerReleaseDepartmentConfigurationTest {
         }
 
         private void verifyDepartmentQuery(String departmentId) {
-            verify(contactsClient).getUserInfoById("access-token", "applicant-1");
+            verify(employeeRosterService).findDepartmentIdByUserId("applicant-1");
+            verify(contactsClient, never()).getUserInfoById(any(String.class), any(String.class));
             ArgumentCaptor<YDParam> parameter = ArgumentCaptor.forClass(YDParam.class);
             verify(masterDataClient, times(2)).queryData(parameter.capture(), eq(YDConf.FORM_QUERY.retrieve_search_form));
             YDParam applicantQuery = parameter.getAllValues().get(0);
@@ -251,6 +254,30 @@ class CustomerReleaseDepartmentConfigurationTest {
             assertEquals("eq", filter.getString("operator"));
         }
 
+        private String firstDepartmentId(Map<String, Object> user) {
+            Object departmentIds = user.get("dept_id_list");
+            if (departmentIds instanceof Iterable) {
+                for (Object departmentId : (Iterable<?>) departmentIds) {
+                    if (departmentId != null) {
+                        return String.valueOf(departmentId);
+                    }
+                }
+            }
+            Object departmentId = user.get("department");
+            if (departmentId instanceof Iterable) {
+                for (Object value : (Iterable<?>) departmentId) {
+                    if (value != null) {
+                        return String.valueOf(value);
+                    }
+                }
+            }
+            if (departmentId != null) {
+                return String.valueOf(departmentId);
+            }
+            Object mainDepartmentId = user.get("main_dept_id");
+            return mainDepartmentId == null ? null : String.valueOf(mainDepartmentId);
+        }
+
         private void verifyFinanceQuery(String departmentId) {
             ArgumentCaptor<YDParam> parameter = ArgumentCaptor.forClass(YDParam.class);
             verify(masterDataClient, times(2)).queryData(parameter.capture(), eq(YDConf.FORM_QUERY.retrieve_search_form));

+ 76 - 3
src/test/java/com/malk/minglei/service/impl/yida/MingleiDocumentServiceImplTest.java

@@ -2,23 +2,80 @@ package com.malk.minglei.service.impl.yida;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.malk.minglei.config.YidaApplicationsProperties;
+import com.malk.minglei.dto.MingleiDocumentQueryRequest;
+import com.malk.minglei.service.employee.EmployeeRosterService;
+import com.malk.server.aliwork.YDConf;
+import com.malk.server.aliwork.YDParam;
 import com.malk.server.dingtalk.DDConf;
+import com.malk.server.dingtalk.DDR_New;
 import com.malk.service.aliwork.YDClient;
 import com.malk.service.dingtalk.DDClient;
 import com.malk.service.dingtalk.DDClient_Contacts;
 import org.junit.jupiter.api.Test;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 
 class MingleiDocumentServiceImplTest {
 
+    @Test
+    @SuppressWarnings("rawtypes")
+    void queriesOnlyOneApprovalMatrixRecordForUserLookup() {
+        YDClient yidaClient = mock(YDClient.class);
+        DDClient dingTalkClient = mock(DDClient.class);
+        DDClient_Contacts contactsClient = mock(DDClient_Contacts.class);
+        EmployeeRosterService employeeRosterService = mock(EmployeeRosterService.class);
+        DDR_New yidaResponse = mock(DDR_New.class);
+        given(employeeRosterService.findDepartmentIdByUserId("user-1")).willReturn("10");
+        given(yidaClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
+                .willReturn(yidaResponse);
+        given(yidaResponse.getRecords()).willReturn(Collections.emptyList());
+
+        MingleiDocumentQueryRequest request = new MingleiDocumentQueryRequest();
+        request.setUserId("user-1");
+        MingleiDocumentServiceImpl service = configuredService(yidaClient, dingTalkClient, contactsClient, employeeRosterService);
+
+        service.queryDocuments(request);
+
+        org.mockito.ArgumentCaptor<YDParam> parameterCaptor = org.mockito.ArgumentCaptor.forClass(YDParam.class);
+        verify(yidaClient).queryData(parameterCaptor.capture(), eq(YDConf.FORM_QUERY.retrieve_search_form));
+        assertEquals(1, parameterCaptor.getValue().getPageSize());
+        verify(employeeRosterService).findDepartmentIdByUserId("user-1");
+        verify(dingTalkClient, never()).getAccessToken();
+        verify(contactsClient, never()).getUserInfoById(any(String.class), any(String.class));
+    }
+
+    @Test
+    void approvalMatrixUserLookupRequiresSynchronizedEmployeeDepartment() {
+        YDClient yidaClient = mock(YDClient.class);
+        DDClient dingTalkClient = mock(DDClient.class);
+        DDClient_Contacts contactsClient = mock(DDClient_Contacts.class);
+        EmployeeRosterService employeeRosterService = mock(EmployeeRosterService.class);
+        MingleiDocumentQueryRequest request = new MingleiDocumentQueryRequest();
+        request.setUserId("user-1");
+        MingleiDocumentServiceImpl service = configuredService(yidaClient, dingTalkClient, contactsClient, employeeRosterService);
+
+        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+                () -> service.queryDocuments(request));
+
+        assertEquals("No synchronized employee roster department was found for user_id", exception.getMessage());
+        verify(employeeRosterService).findDepartmentIdByUserId("user-1");
+        verify(dingTalkClient, never()).getAccessToken();
+        verify(contactsClient, never()).getUserInfoById(any(String.class), any(String.class));
+        verify(yidaClient, never()).queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form));
+    }
+
     /**
      * 验证 type 为 0 时会合并当前部门与子部门成员,并保持去重顺序。
      */
@@ -38,7 +95,8 @@ class MingleiDocumentServiceImplTest {
                 contactsClient,
                 mock(DDConf.class),
                 new YidaApplicationsProperties(),
-                new ObjectMapper());
+                new ObjectMapper(),
+                mock(EmployeeRosterService.class));
 
         List<String> userIds = service.listDepartmentUserIds(10L, 0);
 
@@ -62,7 +120,8 @@ class MingleiDocumentServiceImplTest {
                 contactsClient,
                 mock(DDConf.class),
                 new YidaApplicationsProperties(),
-                new ObjectMapper());
+                new ObjectMapper(),
+                mock(EmployeeRosterService.class));
 
         List<String> userIds = service.listDepartmentUserIds(10L, 1);
 
@@ -88,7 +147,8 @@ class MingleiDocumentServiceImplTest {
                 contactsClient,
                 mock(DDConf.class),
                 new YidaApplicationsProperties(),
-                new ObjectMapper());
+                new ObjectMapper(),
+                mock(EmployeeRosterService.class));
 
         List<String> userIds = service.listDepartmentUserIds(Arrays.asList(10L, 20L), 0);
 
@@ -96,4 +156,17 @@ class MingleiDocumentServiceImplTest {
         verify(contactsClient).getDepartmentId_all("access-token", false, 10L);
         verify(contactsClient).getDepartmentId_all("access-token", false, 20L);
     }
+
+    private MingleiDocumentServiceImpl configuredService(YDClient yidaClient, DDClient dingTalkClient,
+                                                          DDClient_Contacts contactsClient,
+                                                          EmployeeRosterService employeeRosterService) {
+        DDConf dingTalkConfiguration = new DDConf();
+        dingTalkConfiguration.setAppKey("app-key");
+        dingTalkConfiguration.setAppSecret("app-secret");
+        YidaApplicationsProperties applications = new YidaApplicationsProperties();
+        applications.getMasterData().setAppType("APP-TEST");
+        applications.getMasterData().setSystemToken("system-token");
+        return new MingleiDocumentServiceImpl(yidaClient, dingTalkClient, contactsClient, dingTalkConfiguration,
+                applications, new ObjectMapper(), employeeRosterService);
+    }
 }