Explorar el Código

fix(workhours): 补齐离职项目经理审批兜底

malk hace 1 día
padre
commit
a55b92d828

+ 107 - 5
mjava-akdsbeisen/src/main/java/com/malk/service/workhours/ReSubmitApprovalService.java

@@ -10,6 +10,8 @@ import com.malk.server.workhours.TimeCardApprovalStartRequest;
 import com.malk.server.workhours.WHConf;
 import com.malk.service.aliwork.YDClient;
 import com.malk.service.aliwork.YDService;
+import com.malk.service.dingtalk.DDClient;
+import com.malk.service.dingtalk.DDClient_Contacts;
 import com.malk.utils.UtilMap;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
@@ -47,6 +49,12 @@ public class ReSubmitApprovalService {
     @Autowired
     private WHConf whConf;
 
+    @Autowired
+    private DDClient ddClient;
+
+    @Autowired
+    private DDClient_Contacts ddClient_contacts;
+
     /**
      * 前端 v2.35 常量:发起时明细「审批意见」默认「同意」
      */
@@ -67,6 +75,9 @@ public class ReSubmitApprovalService {
      */
     private static final String DEFAULT_MANAGER_ID = "634735129";
 
+    private static final String PERSONNEL_EMPLOYEE = "employeeField_mkow4ydp";
+    private static final String PERSONNEL_MANAGER = "employeeField_mh8xhqc3";
+
     // ===== 汇总表字段(与 ApprovalWriteBackService 一致) =====
     private static final String S_SUBMITTER = "employeeField_mmacxew4";
     private static final String S_SUBMITTER_ID = "textField_mmbffvd9";
@@ -228,6 +239,7 @@ public class ReSubmitApprovalService {
         }
         List<Map> allRecords = queryAll(whConf.getFormUuidWorkHoursSummary(), search);
         ApprovalCoverage approvalCoverage = loadApprovalCoverage(monthText);
+        Map<String, String> projectManagerCache = new HashMap<>();
 
         // 2) 按 (submitterUid) 分组员工月度记录
         Map<String, List<Map>> perSubmitter = new LinkedHashMap<>();
@@ -253,7 +265,8 @@ public class ReSubmitApprovalService {
             String uid = e.getKey();
             List<Map> records = e.getValue();
             try {
-                CollectResult cr = collectApprovalData(uid, monthText, records, approvalCoverage);
+                CollectResult cr = collectApprovalData(
+                        uid, monthText, records, approvalCoverage, projectManagerCache);
                 if (cr.managerGroups.isEmpty() && cr.otherRows.isEmpty()) {
                     skipped++;
                     continue;
@@ -410,8 +423,11 @@ public class ReSubmitApprovalService {
         String deptManagerId;
     }
 
-    private CollectResult collectApprovalData(String uid, String monthText, List<Map> records,
-                                              ApprovalCoverage approvalCoverage) {
+    private CollectResult collectApprovalData(String uid,
+                                              String monthText,
+                                              List<Map> records,
+                                              ApprovalCoverage approvalCoverage,
+                                              Map<String, String> projectManagerCache) {
         CollectResult cr = new CollectResult();
         Set<String> seenBillableKeys = new HashSet<>();
         Set<String> seenNonBillableKeys = new HashSet<>();
@@ -447,7 +463,8 @@ public class ReSubmitApprovalService {
                     continue;
                 }
                 String pmRaw = firstIdOrText(row, S_BIL_MANAGER);
-                final String pm = StringUtils.isBlank(pmRaw) ? DEFAULT_MANAGER_ID : pmRaw;
+                final String pm = projectManagerCache.computeIfAbsent(
+                        StringUtils.defaultString(pmRaw), this::resolveProjectManager);
                 ManagerGroup mg = cr.managerGroups.computeIfAbsent(pm, k -> {
                     ManagerGroup m = new ManagerGroup();
                     m.managerId = pm;
@@ -481,7 +498,8 @@ public class ReSubmitApprovalService {
                     continue;
                 }
                 String pmRaw = firstIdOrText(row, S_NON_MANAGER);
-                final String pm = StringUtils.isBlank(pmRaw) ? DEFAULT_MANAGER_ID : pmRaw;
+                final String pm = projectManagerCache.computeIfAbsent(
+                        StringUtils.defaultString(pmRaw), this::resolveProjectManager);
                 ManagerGroup mg = cr.managerGroups.computeIfAbsent(pm, k -> {
                     ManagerGroup m = new ManagerGroup();
                     m.managerId = pm;
@@ -530,6 +548,81 @@ public class ReSubmitApprovalService {
         return cr;
     }
 
+    /**
+     * prd 项目经理离职时,改由人员档案中的在职主管审批;仅在整条替补链路不可用时兜底 Raymond。
+     */
+    String resolveProjectManager(String managerId) {
+        if (StringUtils.isBlank(managerId) || DEFAULT_MANAGER_ID.equals(managerId)) {
+            return DEFAULT_MANAGER_ID;
+        }
+
+        String accessToken;
+        try {
+            accessToken = ddClient.getAccessToken();
+        } catch (Exception ex) {
+            log.warn("[补发起] 获取钉钉 accessToken 失败,项目经理兜底 Raymond pm={} err={}",
+                    managerId, ex.getMessage());
+            return DEFAULT_MANAGER_ID;
+        }
+        if (isUserInService(accessToken, managerId)) {
+            return managerId;
+        }
+
+        String supervisorId;
+        try {
+            supervisorId = queryPersonnelManagerId(managerId);
+        } catch (Exception ex) {
+            log.warn("[补发起] 查询离职 PM 人员档案主管失败,兜底 Raymond pm={} err={}",
+                    managerId, ex.getMessage());
+            return DEFAULT_MANAGER_ID;
+        }
+        if (StringUtils.isBlank(supervisorId)) {
+            log.warn("[补发起] 离职 PM 未配置人员档案主管,兜底 Raymond pm={}", managerId);
+            return DEFAULT_MANAGER_ID;
+        }
+        if (DEFAULT_MANAGER_ID.equals(supervisorId)
+                || isUserInService(accessToken, supervisorId)) {
+            log.info("[补发起] 离职 PM 改由人员档案主管审批 pm={} supervisor={}",
+                    managerId, supervisorId);
+            return supervisorId;
+        }
+        log.warn("[补发起] 离职 PM 的人员档案主管也不在职,兜底 Raymond pm={} supervisor={}",
+                managerId, supervisorId);
+        return DEFAULT_MANAGER_ID;
+    }
+
+    private boolean isUserInService(String accessToken, String userId) {
+        try {
+            Map user = ddClient_contacts.getUserInfoById(accessToken, userId);
+            return user != null && StringUtils.isNotBlank(str(user, "userid"));
+        } catch (Exception ex) {
+            log.info("[补发起] 钉钉用户不在职或查询失败 userId={} err={}", userId, ex.getMessage());
+            return false;
+        }
+    }
+
+    private String queryPersonnelManagerId(String userId) {
+        Map<String, Object> search = new HashMap<>();
+        search.put(PERSONNEL_EMPLOYEE, Collections.singletonList(userId));
+        DDR_New response = ydClient.queryData(YDParam.builder()
+                .appType(whConf.getYidaAppType())
+                .systemToken(whConf.getYidaSystemToken())
+                .formUuid(whConf.getFormUuidPersonnel())
+                .searchFieldJson(JSON.toJSONString(search))
+                .currentPage(1)
+                .pageSize(1)
+                .build(), YDConf.FORM_QUERY.retrieve_search_form);
+        List<Map> records = response == null ? null : (List<Map>) response.getData();
+        if (records == null || records.isEmpty()) {
+            return "";
+        }
+        Object formData = records.get(0).get("formData");
+        if (!(formData instanceof Map)) {
+            return "";
+        }
+        return firstEmployeeId((Map) formData, PERSONNEL_MANAGER);
+    }
+
     /**
      * 加载指定月份两类审批单的完整明细覆盖索引。
      * fixme 流程列表内联子表恰好 50 行时必须 queryDetails 拉全,否则会把第 51 行以后误判为漏单。
@@ -1461,6 +1554,15 @@ public class ReSubmitApprovalService {
         return "";
     }
 
+    private String firstEmployeeId(Map m, String fieldPrefix) {
+        Object idArr = m == null ? null : m.get(fieldPrefix + "_id");
+        if (!(idArr instanceof List) || ((List) idArr).isEmpty()) {
+            return "";
+        }
+        Object first = ((List) idArr).get(0);
+        return first == null ? "" : String.valueOf(first);
+    }
+
     private String firstNonBlank(String... vals) {
         for (String v : vals) {
             if (StringUtils.isNotBlank(v)) {

+ 115 - 0
mjava-akdsbeisen/src/test/java/com/malk/service/workhours/ReSubmitApprovalServiceTest.java

@@ -2,9 +2,12 @@ package com.malk.service.workhours;
 
 import com.malk.server.aliwork.YDConf;
 import com.malk.server.aliwork.YDParam;
+import com.malk.server.dingtalk.DDR_New;
 import com.malk.server.workhours.TimeCardApprovalStartRequest;
 import com.malk.server.workhours.WHConf;
 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 org.mockito.ArgumentCaptor;
 import org.springframework.test.util.ReflectionTestUtils;
@@ -21,11 +24,14 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.verify;
 
 class ReSubmitApprovalServiceTest {
 
+    private static final String DEFAULT_MANAGER_ID = "634735129";
+
     @Test
     void shouldTreatBlankHistoricalOpinionAsCompletedApprovalCoverage() {
         assertTrue(ReSubmitApprovalService.isCompletedApprovedRow("COMPLETED", "agree", ""));
@@ -253,4 +259,113 @@ class ReSubmitApprovalServiceTest {
         assertEquals(1000d, ((Number) first.get("numberField_mmd5b5gl")).doubleValue());
         assertEquals(2d, ((Number) second.get("numberField_mmd5b5gl")).doubleValue());
     }
+
+    @Test
+    void shouldKeepActiveProjectManager() {
+        DDClient ddClient = mock(DDClient.class);
+        DDClient_Contacts contacts = mock(DDClient_Contacts.class);
+        YDClient ydClient = mock(YDClient.class);
+        ReSubmitApprovalService service = managerResolutionService(ddClient, contacts, ydClient);
+
+        when(ddClient.getAccessToken()).thenReturn("token");
+        when(contacts.getUserInfoById("token", "pm-active"))
+                .thenReturn(Collections.singletonMap("userid", "pm-active"));
+
+        assertEquals("pm-active", service.resolveProjectManager("pm-active"));
+        verifyNoInteractions(ydClient);
+    }
+
+    @Test
+    void shouldUseActiveSupervisorWhenProjectManagerHasLeft() {
+        DDClient ddClient = mock(DDClient.class);
+        DDClient_Contacts contacts = mock(DDClient_Contacts.class);
+        YDClient ydClient = mock(YDClient.class);
+        ReSubmitApprovalService service = managerResolutionService(ddClient, contacts, ydClient);
+
+        when(ddClient.getAccessToken()).thenReturn("token");
+        when(contacts.getUserInfoById("token", "pm-left"))
+                .thenThrow(new IllegalStateException("user not found"));
+        when(contacts.getUserInfoById("token", "supervisor-active"))
+                .thenReturn(Collections.singletonMap("userid", "supervisor-active"));
+        when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
+                .thenReturn(personnelResponse("supervisor-active"));
+
+        assertEquals("supervisor-active", service.resolveProjectManager("pm-left"));
+    }
+
+    @Test
+    void shouldFallBackWhenSupervisorIsInactive() {
+        DDClient ddClient = mock(DDClient.class);
+        DDClient_Contacts contacts = mock(DDClient_Contacts.class);
+        YDClient ydClient = mock(YDClient.class);
+        ReSubmitApprovalService service = managerResolutionService(ddClient, contacts, ydClient);
+
+        when(ddClient.getAccessToken()).thenReturn("token");
+        when(contacts.getUserInfoById("token", "pm-left"))
+                .thenThrow(new IllegalStateException("user not found"));
+        when(contacts.getUserInfoById("token", "supervisor-left"))
+                .thenThrow(new IllegalStateException("user not found"));
+        when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
+                .thenReturn(personnelResponse("supervisor-left"));
+
+        assertEquals(DEFAULT_MANAGER_ID, service.resolveProjectManager("pm-left"));
+    }
+
+    @Test
+    void shouldFallBackWhenSupervisorIsMissing() {
+        DDClient ddClient = mock(DDClient.class);
+        DDClient_Contacts contacts = mock(DDClient_Contacts.class);
+        YDClient ydClient = mock(YDClient.class);
+        ReSubmitApprovalService service = managerResolutionService(ddClient, contacts, ydClient);
+
+        when(ddClient.getAccessToken()).thenReturn("token");
+        when(contacts.getUserInfoById("token", "pm-left"))
+                .thenThrow(new IllegalStateException("user not found"));
+        when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
+                .thenReturn(new DDR_New());
+
+        assertEquals(DEFAULT_MANAGER_ID, service.resolveProjectManager("pm-left"));
+    }
+
+    @Test
+    void shouldFallBackWhenPersonnelLookupFails() {
+        DDClient ddClient = mock(DDClient.class);
+        DDClient_Contacts contacts = mock(DDClient_Contacts.class);
+        YDClient ydClient = mock(YDClient.class);
+        ReSubmitApprovalService service = managerResolutionService(ddClient, contacts, ydClient);
+
+        when(ddClient.getAccessToken()).thenReturn("token");
+        when(contacts.getUserInfoById("token", "pm-left"))
+                .thenThrow(new IllegalStateException("user not found"));
+        when(ydClient.queryData(any(YDParam.class), eq(YDConf.FORM_QUERY.retrieve_search_form)))
+                .thenThrow(new IllegalStateException("query failed"));
+
+        assertEquals(DEFAULT_MANAGER_ID, service.resolveProjectManager("pm-left"));
+    }
+
+    private ReSubmitApprovalService managerResolutionService(DDClient ddClient,
+                                                              DDClient_Contacts contacts,
+                                                              YDClient ydClient) {
+        WHConf whConf = new WHConf();
+        whConf.setYidaAppType("APP");
+        whConf.setYidaSystemToken("TOKEN");
+        whConf.setFormUuidPersonnel("FORM-PERSONNEL");
+
+        ReSubmitApprovalService service = new ReSubmitApprovalService();
+        ReflectionTestUtils.setField(service, "ddClient", ddClient);
+        ReflectionTestUtils.setField(service, "ddClient_contacts", contacts);
+        ReflectionTestUtils.setField(service, "ydClient", ydClient);
+        ReflectionTestUtils.setField(service, "whConf", whConf);
+        return service;
+    }
+
+    private DDR_New personnelResponse(String supervisorId) {
+        Map<String, Object> formData = new HashMap<>();
+        formData.put("employeeField_mh8xhqc3_id", Collections.singletonList(supervisorId));
+        Map<String, Object> record = new HashMap<>();
+        record.put("formData", formData);
+        DDR_New response = new DDR_New();
+        response.setData(Collections.singletonList(record));
+        return response;
+    }
 }