From 501c7eaab236314f61a9064a237b049966a66818 Mon Sep 17 00:00:00 2001
From: vincentlu <t1341870251@gmail.com>
Date: 星期四, 26 二月 2026 15:56:51 +0800
Subject: [PATCH] #

---
 zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/GuaranteeRuntimeService.java          |   32 +++++
 zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/impl/GuaranteeRuntimeServiceImpl.java |  161 ++++++++++++++++++++++++++
 zy-acs-manager/src/main/java/com/zy/acs/manager/core/scheduler/GuaranteeScheduler.java             |  166 +++++++++++++++++++++++++++
 3 files changed, 359 insertions(+), 0 deletions(-)

diff --git a/zy-acs-manager/src/main/java/com/zy/acs/manager/core/scheduler/GuaranteeScheduler.java b/zy-acs-manager/src/main/java/com/zy/acs/manager/core/scheduler/GuaranteeScheduler.java
new file mode 100644
index 0000000..c2e7601
--- /dev/null
+++ b/zy-acs-manager/src/main/java/com/zy/acs/manager/core/scheduler/GuaranteeScheduler.java
@@ -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;
+        }
+    }
+}
diff --git a/zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/GuaranteeRuntimeService.java b/zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/GuaranteeRuntimeService.java
new file mode 100644
index 0000000..5c5abec
--- /dev/null
+++ b/zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/GuaranteeRuntimeService.java
@@ -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);
+}
+
diff --git a/zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/impl/GuaranteeRuntimeServiceImpl.java b/zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/impl/GuaranteeRuntimeServiceImpl.java
new file mode 100644
index 0000000..04163fa
--- /dev/null
+++ b/zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/impl/GuaranteeRuntimeServiceImpl.java
@@ -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;
+    }
+}

--
Gitblit v1.9.1