package com.malk.timer; import com.malk.service.personnel.PersonnelSyncService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import java.util.concurrent.atomic.AtomicBoolean; import java.util.Map; /** * 钉钉 -> 宜搭 人员档案 定时全量增量同步 * 工作日 (MON-FRI) 每天 05:30 / 13:15 / 18:30 各一次 fullSync (limit 取配置 personnel-sync.limitFirstN, 生产为 0 即真·全量) * 共 3 次/工作日; 周末不跑 */ @Slf4j @Configuration @EnableScheduling @ConditionalOnProperty(prefix = "enable", name = "scheduling", havingValue = "true", matchIfMissing = false) public class PersonnelSyncTimer { @Autowired private PersonnelSyncService personnelSyncService; // fixme: 防止上一轮未完成时下一轮重入 (两轮并发会使 QPS 翻倍) private final AtomicBoolean running = new AtomicBoolean(false); // fixme: 钉钉整点 QPS 高峰偶发 subcode=90002 限流 → 首次失败后再重试 2 次, 间隔 60s 让钉钉冷却; 仍失败放弃等下一次 cron private static final int MAX_ATTEMPTS = 3; private static final long RETRY_DELAY_MS = 60_000L; /** 工作日 05:30 / 13:15 / 18:30 各一次全量同步(单方法挂 3 条 cron,离散时点无法用单条 cron 表达) */ @Scheduled(cron = "0 30 5 ? * MON-FRI") @Scheduled(cron = "0 15 13 ? * MON-FRI") @Scheduled(cron = "0 30 18 ? * MON-FRI") public void scheduledFullSync() { runFullSync("05:30/13:15/18:30"); } private void runFullSync(String tag) { if (!running.compareAndSet(false, true)) { log.warn("[PersonnelSync] 上次定时同步尚未结束,跳过本次触发 tag={}", tag); return; } log.info("[PersonnelSync] 定时同步任务开始 tag={}", tag); try { for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { try { java.util.Map stats = personnelSyncService.fullSync(null); if (hasSyncFailures(stats)) { log.warn("[PersonnelSync] 定时同步存在未完成记录 tag={} attempt={}/{} {}", tag, attempt, MAX_ATTEMPTS, stats); if (attempt < MAX_ATTEMPTS) { if (!waitBeforeRetry(tag)) return; continue; } log.error("[PersonnelSync] 定时同步连续 {} 次仍有未完成记录,等待下一次 cron tag={} {}", MAX_ATTEMPTS, tag, stats); return; } log.info("[PersonnelSync] 定时同步任务完成 tag={} attempt={}/{} {}", tag, attempt, MAX_ATTEMPTS, stats); return; } catch (Exception e) { log.error("[PersonnelSync] 定时同步任务失败 tag={} attempt={}/{}", tag, attempt, MAX_ATTEMPTS, e); if (attempt < MAX_ATTEMPTS) { if (!waitBeforeRetry(tag)) return; } } } log.error("[PersonnelSync] 定时同步任务连续 {} 次失败,放弃本轮等待下一次 cron 触发 tag={}", MAX_ATTEMPTS, tag); } finally { running.set(false); } } static boolean hasSyncFailures(Map stats) { return stats == null || count(stats, "failed") > 0 || count(stats, "managerLookupFailed") > 0; } private static int count(Map stats, String key) { if (stats == null || !(stats.get(key) instanceof Number)) { return 0; } return ((Number) stats.get(key)).intValue(); } private static boolean waitBeforeRetry(String tag) { try { Thread.sleep(RETRY_DELAY_MS); return true; } catch (InterruptedException ie) { Thread.currentThread().interrupt(); log.warn("[PersonnelSync] 重试等待被中断,放弃本轮 tag={}", tag); return false; } } }