#
vincentlu
1 天以前 501c7eaab236314f61a9064a237b049966a66818
#
3个文件已添加
359 ■■■■■ 已修改文件
zy-acs-manager/src/main/java/com/zy/acs/manager/core/scheduler/GuaranteeScheduler.java 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/GuaranteeRuntimeService.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-manager/src/main/java/com/zy/acs/manager/core/service/impl/GuaranteeRuntimeServiceImpl.java 161 ●●●●● 补丁 | 查看 | 原始文档 | 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;
    }
}