| zy-acs-manager/src/main/java/com/zy/acs/manager/core/scheduler/GuaranteeScheduler.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/GuaranteeRuntimeService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/impl/GuaranteeRuntimeServiceImpl.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
zy-acs-manager/src/main/java/com/zy/acs/manager/core/scheduler/GuaranteeScheduler.java
New file @@ -0,0 +1,166 @@ package com.zy.acs.manager.core.scheduler; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.zy.acs.manager.core.service.GuaranteeRuntimeService; 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; import org.springframework.scheduling.support.CronExpression; import org.springframework.stereotype.Component; 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 public class GuaranteeScheduler { private final GuaranteeService guaranteeService; private final GuaranteeRuntimeService guaranteeRuntimeService; @Value("${guarantee.scheduler.enabled:true}") 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) { this.guaranteeService = guaranteeService; this.guaranteeRuntimeService = guaranteeRuntimeService; } @Scheduled(cron = "0/15 * * * * ?") 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 { processPlan(plan, now); } catch (Exception ex) { log.error("GuaranteeScheduler failed for plan {}", plan.getName(), ex); } } } 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()); return; } state.init(plan, next, resolveLeadMinutes(plan)); } if (now.isBefore(state.getPrepareStart())) { state.setPhase(Phase.IDLE); 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)); } 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) { 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; } 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; } } } zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/GuaranteeRuntimeService.java
New file @@ -0,0 +1,32 @@ package com.zy.acs.manager.core.service; import com.zy.acs.manager.manager.entity.Guarantee; import java.time.LocalDateTime; /** * Runtime guarantee orchestration entry. */ public interface GuaranteeRuntimeService { /** * Lead time entrance: make sure enough vehicles can reach the target window. */ void prepare(Guarantee plan, LocalDateTime targetTime); /** * Lock stage: restrict non-essential dispatching for reserve vehicles. */ void lock(Guarantee plan, LocalDateTime targetTime); /** * Executed repeatedly during the guarantee window to keep SLA. */ void checkWindow(Guarantee plan, LocalDateTime targetTime); /** * Called once after the window ends to release state/records. */ void finish(Guarantee plan, LocalDateTime targetTime); } zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/impl/GuaranteeRuntimeServiceImpl.java
New file @@ -0,0 +1,161 @@ package com.zy.acs.manager.core.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.zy.acs.common.enums.AgvStatusType; import com.zy.acs.manager.core.service.GuaranteeRuntimeService; import com.zy.acs.manager.core.service.MainLockWrapService; import com.zy.acs.manager.manager.entity.Agv; import com.zy.acs.manager.manager.entity.AgvDetail; import com.zy.acs.manager.manager.entity.Guarantee; import com.zy.acs.manager.manager.entity.Task; import com.zy.acs.manager.manager.enums.StatusType; import com.zy.acs.manager.manager.enums.TaskStsType; import com.zy.acs.manager.manager.enums.TaskTypeType; 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; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.Optional; @Slf4j @Service public class GuaranteeRuntimeServiceImpl implements GuaranteeRuntimeService { private final AgvService agvService; private final AgvDetailService agvDetailService; private final TaskService taskService; private final MainLockWrapService mainLockWrapService; public GuaranteeRuntimeServiceImpl(AgvService agvService, AgvDetailService agvDetailService, TaskService taskService, MainLockWrapService mainLockWrapService) { this.agvService = agvService; this.agvDetailService = agvDetailService; this.taskService = taskService; this.mainLockWrapService = mainLockWrapService; } @Override 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); 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); } @Override 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 } @Override 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); } } @Override 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); List<Agv> scopedAgvs = findScopedAgvs(plan); int available = 0; List<ChargeCandidate> candidates = new ArrayList<>(); for (Agv agv : scopedAgvs) { AgvDetail detail = agvDetailService.selectByAgvId(agv.getId()); if (detail == null || detail.getSoc() == null) { continue; } if (detail.getSoc() >= minSoc && isAvailable(agv, detail)) { available++; } else { candidates.add(new ChargeCandidate(agv, detail)); } } candidates.sort(Comparator.comparingInt(o -> Optional.ofNullable(o.detail.getSoc()).orElse(0))); return new CapacitySnapshot(available, candidates); } private boolean isAvailable(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>() .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++; } } private List<Agv> findScopedAgvs(Guarantee plan) { LambdaQueryWrapper<Agv> wrapper = new LambdaQueryWrapper<Agv>() .eq(Agv::getStatus, StatusType.ENABLE.val); if ("MODEL".equalsIgnoreCase(plan.getScopeType()) && plan.getScopeValue() != null) { try { wrapper.eq(Agv::getAgvModel, Long.valueOf(plan.getScopeValue())); } catch (NumberFormatException ignore) { log.warn("Guarantee[{}] invalid scopeValue {}", plan.getName(), plan.getScopeValue()); } } return agvService.list(wrapper); } @Data private static class CapacitySnapshot { private final int availableCount; private final List<ChargeCandidate> candidates; } @Data @AllArgsConstructor private static class ChargeCandidate { private Agv agv; private AgvDetail detail; } }