zy-acs-manager/src/main/java/com/zy/acs/manager/core/scheduler/GuaranteeScheduler.java
@@ -5,7 +5,6 @@ import com.zy.acs.manager.manager.entity.Guarantee; import com.zy.acs.manager.manager.enums.StatusType; import com.zy.acs.manager.manager.service.GuaranteeService; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; @@ -14,13 +13,9 @@ import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @Slf4j //@Component @Component public class GuaranteeScheduler { private final GuaranteeService guaranteeService; @@ -30,12 +25,6 @@ private boolean enabled; @Value("${guarantee.scheduler.default-lead-minutes:60}") private int defaultLeadMinutes; @Value("${guarantee.scheduler.lock-duration-minutes:5}") private int lockDurationMinutes; @Value("${guarantee.scheduler.window-duration-minutes:10}") private int windowDurationMinutes; private final Map<Long, PlanRuntimeState> runtimeStates = new ConcurrentHashMap<>(); public GuaranteeScheduler(GuaranteeService guaranteeService, GuaranteeRuntimeService guaranteeRuntimeService) { @@ -43,14 +32,14 @@ this.guaranteeRuntimeService = guaranteeRuntimeService; } @Scheduled(cron = "0/15 * * * * ?") @Scheduled(cron = "0/1 * * * * ?") public void drive() { if (!enabled) { return; } List<Guarantee> plans = guaranteeService.list(new LambdaQueryWrapper<Guarantee>() .eq(Guarantee::getDeleted, 0) .eq(Guarantee::getStatus, StatusType.ENABLE.val)); syncCache(plans); LocalDateTime now = LocalDateTime.now(); for (Guarantee plan : plans) { try { @@ -62,104 +51,33 @@ } private void processPlan(Guarantee plan, LocalDateTime now) { PlanRuntimeState state = runtimeStates.computeIfAbsent(plan.getId(), id -> new PlanRuntimeState()); if (state.getTargetTime() == null || now.isAfter(state.getWindowEnd())) { LocalDateTime next = computeNext(plan, now); if (next == null) { runtimeStates.remove(plan.getId()); LocalDateTime targetTime = resolveNextTarget(plan, now); if (targetTime == null) { return; } state.init(plan, next, resolveLeadMinutes(plan)); } if (now.isBefore(state.getPrepareStart())) { state.setPhase(Phase.IDLE); int leadMinutes = resolveLeadMinutes(plan); LocalDateTime prepareFrom = targetTime.minusMinutes(leadMinutes); if (now.isBefore(prepareFrom) || now.isAfter(targetTime)) { return; } if (now.isBefore(state.getLockStart())) { state.transition(Phase.PREPARING, () -> guaranteeRuntimeService.prepare(plan, state.getTargetTime())); return; } if (now.isBefore(state.getTargetTime())) { state.transition(Phase.LOCKING, () -> guaranteeRuntimeService.lock(plan, state.getTargetTime())); return; } if (now.isBefore(state.getWindowEnd())) { state.transition(Phase.WINDOW, () -> guaranteeRuntimeService.checkWindow(plan, state.getTargetTime())); guaranteeRuntimeService.checkWindow(plan, state.getTargetTime()); return; } if (state.getPhase() != Phase.FINISHED) { state.transition(Phase.FINISHED, () -> guaranteeRuntimeService.finish(plan, state.getTargetTime())); } LocalDateTime next = computeNext(plan, state.getWindowEnd().plusSeconds(1)); if (next == null) { runtimeStates.remove(plan.getId()); return; } state.init(plan, next, resolveLeadMinutes(plan)); guaranteeRuntimeService.prepare(plan, targetTime); } private void syncCache(List<Guarantee> plans) { Set<Long> activeIds = plans.stream().map(Guarantee::getId).collect(Collectors.toSet()); runtimeStates.keySet().removeIf(id -> !activeIds.contains(id)); } private int resolveLeadMinutes(Guarantee plan) { return Math.max(1, plan.getLeadTime() == null ? defaultLeadMinutes : plan.getLeadTime()); } private LocalDateTime computeNext(Guarantee plan, LocalDateTime reference) { private LocalDateTime resolveNextTarget(Guarantee plan, LocalDateTime now) { String cronExpr = plan.getCronExpr(); if (cronExpr == null || cronExpr.trim().isEmpty()) { return null; } try { CronExpression cron = CronExpression.parse(cronExpr); LocalDateTime base = reference == null ? LocalDateTime.now() : reference; LocalDateTime next = cron.next(base); if (next == null && !base.equals(LocalDateTime.now())) { next = cron.next(LocalDateTime.now()); } return next; return cron.next(now.minusSeconds(1)); // next(2026-02-27 08:15) => 2026-02-27 10:00:00 } catch (IllegalArgumentException ex) { log.error("Guarantee[{}] invalid cron {} ", plan.getName(), cronExpr, ex); return null; } } private enum Phase { IDLE, PREPARING, LOCKING, WINDOW, FINISHED } @Data private class PlanRuntimeState { private LocalDateTime targetTime; private LocalDateTime prepareStart; private LocalDateTime lockStart; private LocalDateTime windowEnd; private Phase phase = Phase.IDLE; private void init(Guarantee plan, LocalDateTime target, int leadMinutes) { this.targetTime = target; this.prepareStart = target.minusMinutes(Math.max(1, leadMinutes)); int lockMinutes = Math.min(lockDurationMinutes, leadMinutes); this.lockStart = target.minusMinutes(Math.max(1, lockMinutes)); this.windowEnd = target.plusMinutes(Math.max(1, windowDurationMinutes)); this.phase = Phase.IDLE; log.info("Guarantee[{}] next target {} prepare@{} lock@{} windowEnd@{}", plan.getName(), targetTime, prepareStart, lockStart, windowEnd); } private void transition(Phase targetPhase, Runnable callback) { if (this.phase.ordinal() >= targetPhase.ordinal()) { return; } callback.run(); this.phase = targetPhase; } private int resolveLeadMinutes(Guarantee plan) { return Math.max(1, plan.getLeadTime() == null ? defaultLeadMinutes : plan.getLeadTime()); } } zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/GuaranteeRuntimeService.java
@@ -12,8 +12,6 @@ import com.zy.acs.manager.manager.service.AgvDetailService; import com.zy.acs.manager.manager.service.AgvService; import com.zy.acs.manager.manager.service.TaskService; import lombok.AllArgsConstructor; import lombok.Data; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -22,7 +20,6 @@ import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Optional; @Slf4j @Service @@ -44,40 +41,12 @@ } public void prepare(Guarantee plan, LocalDateTime targetTime) { CapacitySnapshot snapshot = evaluate(plan); if (snapshot.availableCount >= plan.getRequiredCount()) { log.debug("Guarantee[{}] already has {} available vehicles for {}", plan.getName(), snapshot.availableCount, targetTime); int requiredCount = plan.getRequiredCount() == null ? 0 : plan.getRequiredCount(); if (requiredCount <= 0) { log.warn("Guarantee[{}] requiredCount is not configured, skip", plan.getName()); return; } int shortage = plan.getRequiredCount() - snapshot.availableCount; log.info("Guarantee[{}] shortage {} vehicles for {} (minSoc={}%), scheduling charge.", plan.getName(), shortage, targetTime, plan.getMinSoc()); dispatchChargeTasks(snapshot, shortage); } public void lock(Guarantee plan, LocalDateTime targetTime) { log.info("Guarantee[{}] entering lock stage for {}", plan.getName(), targetTime); // TODO persist lock state / inform dispatcher once integration is ready } public void checkWindow(Guarantee plan, LocalDateTime targetTime) { CapacitySnapshot snapshot = evaluate(plan); if (snapshot.availableCount < plan.getRequiredCount()) { int shortage = plan.getRequiredCount() - snapshot.availableCount; log.warn("Guarantee[{}] window shortage {} vehicles for {}. Trigger quick recharge.", plan.getName(), shortage, targetTime); dispatchChargeTasks(snapshot, shortage); } } public void finish(Guarantee plan, LocalDateTime targetTime) { log.info("Guarantee[{}] finished window for {}", plan.getName(), targetTime); // TODO release any lock/flag once detail implementation is defined } private CapacitySnapshot evaluate(Guarantee plan) { int minSoc = Optional.ofNullable(plan.getMinSoc()).orElse(50); int minSoc = plan.getMinSoc() == null ? 50 : plan.getMinSoc(); List<Agv> scopedAgvs = findScopedAgvs(plan); int available = 0; List<ChargeCandidate> candidates = new ArrayList<>(); @@ -86,45 +55,52 @@ if (detail == null || detail.getSoc() == null) { continue; } if (detail.getSoc() >= minSoc && isAvailable(agv, detail)) { if (!isIdle(agv, detail)) { continue; } int soc = detail.getSoc(); if (soc >= minSoc) { available++; } else { candidates.add(new ChargeCandidate(agv, detail)); candidates.add(new ChargeCandidate(agv, soc)); } } candidates.sort(Comparator.comparingInt(o -> Optional.ofNullable(o.detail.getSoc()).orElse(0))); return new CapacitySnapshot(available, candidates); if (available >= requiredCount) { log.debug("Guarantee[{}] already has {} vehicles >= {}% SOC for {}", plan.getName(), available, minSoc, targetTime); return; } int shortage = requiredCount - available; candidates.sort(Comparator.comparingInt(ChargeCandidate::getSoc)); int scheduled = 0; for (ChargeCandidate candidate : candidates) { if (scheduled >= shortage) { break; } log.info("Guarantee[{}] schedule AGV {} charging (soc={}%) for target {}", plan.getName(), candidate.getAgv().getName(), candidate.getSoc(), targetTime); mainLockWrapService.buildMinorTask(candidate.getAgv().getId(), TaskTypeType.TO_CHARGE, null, null); scheduled++; } if (scheduled < shortage) { log.warn("Guarantee[{}] still short of {} vehicles for {} (only {} idle low-soc AGVs)", plan.getName(), shortage - scheduled, targetTime, candidates.size()); } } private boolean isAvailable(Agv agv, AgvDetail detail) { private boolean isIdle(Agv agv, AgvDetail detail) { if (!Objects.equals(agv.getStatus(), StatusType.ENABLE.val)) { return false; } if (detail.getAgvStatus() != null && detail.getAgvStatus().equals(AgvStatusType.CHARGE)) { return false; } boolean busy = taskService.count(new LambdaQueryWrapper<Task>() long busyCount = taskService.count(new LambdaQueryWrapper<Task>() .eq(Task::getAgvId, agv.getId()) .in(Task::getTaskSts, TaskStsType.WAITING.val(), TaskStsType.ASSIGN.val(), TaskStsType.PROGRESS.val())) > 0; return !busy; } private void dispatchChargeTasks(CapacitySnapshot snapshot, int shortage) { if (shortage <= 0) { return; } int scheduled = 0; for (ChargeCandidate candidate : snapshot.candidates) { if (scheduled >= shortage) { break; } log.info("Scheduling AGV [{}] for charging to support guarantee", candidate.agv.getName()); mainLockWrapService.buildMinorTask(candidate.agv.getId(), TaskTypeType.TO_CHARGE, null, null); scheduled++; } TaskStsType.PROGRESS.val())); return busyCount == 0; } private List<Agv> findScopedAgvs(Guarantee plan) { @@ -140,16 +116,21 @@ return agvService.list(wrapper); } @Data private static class CapacitySnapshot { private final int availableCount; private final List<ChargeCandidate> candidates; private static class ChargeCandidate { private final Agv agv; private final int soc; ChargeCandidate(Agv agv, int soc) { this.agv = agv; this.soc = soc; } @Data @AllArgsConstructor private static class ChargeCandidate { private Agv agv; private AgvDetail detail; public Agv getAgv() { return agv; } public int getSoc() { return soc; } } }