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