自动化立体仓库 - WMS系统
chen.llin
1 天以前 9611dc686299be640ce5e5f5990c747765161ec7
agv逻辑调整2
10个文件已修改
1681 ■■■■ 已修改文件
src/main/java/com/zy/asrs/entity/Task.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/mapper/BasDevpMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/BasDevpServiceImpl.java 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/MobileServiceImpl.java 135 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java 100 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/TaskLogServiceImpl.java 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/TaskServiceImpl.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/AgvScheduler.java 346 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/handler/AgvHandler.java 1005 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/handler/WorkMastHandler.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/Task.java
@@ -334,6 +334,13 @@
    @TableField("take_none")
    private String takeNone;
    /**
     * 是否删除 0:未删除 1:已删除
     */
    @ApiModelProperty(value = "是否删除 0:未删除 1:已删除")
    @TableField("is_deleted")
    private Integer isDeleted;
    public Task() {
    }
src/main/java/com/zy/asrs/mapper/BasDevpMapper.java
@@ -24,4 +24,7 @@
    @Update("UPDATE asr_bas_devp SET in_qty = in_qty + 1 WHERE dev_no = #{site}")
    void incrementInQty(@Param("site") int sourceStaNo);
    @Update("UPDATE asr_bas_devp SET in_qty = in_qty - 1 WHERE dev_no = #{site} AND in_qty > 0")
    void decrementInQty(@Param("site") int site);
}
src/main/java/com/zy/asrs/service/impl/BasDevpServiceImpl.java
@@ -49,15 +49,15 @@
            throw new CoolException(devpNo + "站点不存在");
        }
        if (pakin) {
            if (station.getAutoing() == null || !station.getAutoing().equals("Y")) {
                throw new CoolException(devpNo + "站点不是自动状态");
            }
            if (station.getLoading() == null || !station.getLoading().equals("Y")) {
                throw new CoolException(devpNo + "站点无物");
            }
            if (station.getWrkNo() != null && station.getWrkNo() > 0 && station.getWrkNo() < 9990) {
                throw new CoolException(devpNo + "站点已有工作号");
            }
//            if (station.getAutoing() == null || !station.getAutoing().equals("Y")) {
//                throw new CoolException(devpNo + "站点不是自动状态");
//            }
//            if (station.getLoading() == null || !station.getLoading().equals("Y")) {
//                throw new CoolException(devpNo + "站点无物");
//            }
//            if (station.getWrkNo() != null && station.getWrkNo() > 0 && station.getWrkNo() < 9990) {
//                throw new CoolException(devpNo + "站点已有工作号");
//            }
//            if(!station.getInEnable().equals("Y")) {
//                throw new CoolException(devpNo+"站点不是可入状态");
//            }
src/main/java/com/zy/asrs/service/impl/MobileServiceImpl.java
@@ -139,6 +139,12 @@
        int type = param.getType();
        String sourceSite = param.getSourceSite();
        String barcode = param.getBarcode();
        // 检查托盘码和暂存位编码是否相同
        if (barcode != null && sourceSite != null && barcode.trim().equals(sourceSite.trim())) {
            throw new CoolException("托盘码和暂存位编码不能相同");
        }
        int ioType;
        // 查询源站点(库位)信息,但不检查是否存在,允许下单成功
        // 站点不存在的检查将在定时任务(AgvHandler.callAgv)中进行
@@ -213,143 +219,36 @@
            throw new CoolException(barcode+ ":条码存在agv搬运任务!");
        }
        // 根据whs_type和库位编号前缀选择站点和机器人组
        // 如果库位不存在,使用默认逻辑(根据type判断),站点不存在的检查将在定时任务中进行
        // 根据whs_type确定机器人组(站点分配完全由定时任务处理)
        // 如果库位不存在,使用默认逻辑(根据type判断)
        Long whsType = locCache != null ? locCache.getWhsType() : null;
        String locNo = locCache != null ? locCache.getLocNo() : sourceSite;
        List<String> targetStations;
        String robotGroup;
        
        // 判断库位编号前缀:CA开头只做入库,WA开头才会被出库分配缓存区(从配置读取)
        String inboundOnlyPrefix = agvProperties.getLocationPrefix().getInboundOnly();
        String cacheAreaPrefix = agvProperties.getLocationPrefix().getCacheArea();
        boolean isCA = locNo != null && locNo.startsWith(inboundOnlyPrefix);
        boolean isWA = locNo != null && locNo.startsWith(cacheAreaPrefix);
        if (whsType != null && whsType.equals(agvProperties.getWhsTypeMapping().getInboundArea())) {
            // whs_type = 1: 入库区,使用东侧站点和Group-001
            targetStations = agvProperties.getEastStations();
            // whs_type = 1: 入库区,使用Group-001
            robotGroup = agvProperties.getRobotGroupEast();
            log.info("库位whs_type={},库位编号={}({}),使用入库区配置({}站点和{})",
                whsType, locNo, isCA ? "CA" : (isWA ? "WA" : "其他"),
                agvProperties.getEastDisplayName(), robotGroup);
            log.info("库位whs_type={},使用入库区配置({})", whsType, robotGroup);
        } else if (whsType != null && whsType.equals(agvProperties.getWhsTypeMapping().getCacheArea())) {
            // whs_type = 2: 缓存区,使用西侧站点和Group-002
            // 注意:如果有CA开头的入库,但是标记在西侧的(whs_type=2),也会分配到西侧的站点入库
            targetStations = agvProperties.getWestStations();
            // whs_type = 2: 缓存区,使用Group-002
            robotGroup = agvProperties.getRobotGroupWest();
            log.info("库位whs_type={},库位编号={}({}),使用缓存区配置({}站点和{})",
                whsType, locNo, isCA ? "CA" : (isWA ? "WA" : "其他"),
                agvProperties.getWestDisplayName(), robotGroup);
            log.info("库位whs_type={},使用缓存区配置({})", whsType, robotGroup);
        } else {
            // whs_type为空或其他值,根据type判断(兼容旧逻辑)
            // 如果库位不存在,也使用此逻辑
            if (type == 1) {
                targetStations = agvProperties.getEastStations();
                robotGroup = agvProperties.getRobotGroupEast();
            } else {
                targetStations = agvProperties.getWestStations();
                robotGroup = agvProperties.getRobotGroupWest();
            }
            if (locCache == null) {
                log.warn("源站点(库位){}不存在,使用type={}的默认逻辑,站点检查将在定时任务中进行", sourceSite, type);
                log.warn("源站点(库位){}不存在,使用type={}的默认逻辑,机器人组:{},站点分配将在定时任务中进行", sourceSite, type, robotGroup);
            } else {
                log.warn("库位whs_type={}未配置或不在映射范围内,使用type={}的默认逻辑", whsType, type);
                log.warn("库位whs_type={}未配置或不在映射范围内,使用type={}的默认逻辑,机器人组:{}", whsType, type, robotGroup);
            }
        }
        
        // 将站点字符串列表转换为整数列表
        List<Integer> siteIntList = targetStations.stream()
                .map(Integer::parseInt)
                .collect(Collectors.toList());
        // 判断能入站点(in_enable="Y"表示能入),排除dev_no=0的无效站点
        // 注意:不在此处检查站点是否存在或可用,允许下单成功
        // 站点检查和分配将在定时任务(AgvHandler.callAgv)中进行
        List<Integer> sites = basDevpMapper.selectList(
                new EntityWrapper<BasDevp>()
                        .eq("in_enable", "Y") // in_enable是能入
                        .in("dev_no", siteIntList)
                        .ne("dev_no", 0) // 排除dev_no=0的无效站点
        ).stream()
                .map(BasDevp::getDevNo)
                .filter(devNo -> devNo != null && devNo != 0) // 再次过滤,确保不为null或0
                .collect(Collectors.toList());
        // 获取没有出库任务的站点
        List<Integer> canInSites = sites.isEmpty() ? new ArrayList<>() : basDevpMapper.getCanInSites(sites);
        // 寻找入库任务最少的站点(且必须in_enable="Y"能入 和 canining="Y"可入),排除dev_no=0的无效站点
        // 注意:不在此处检查未完成的AGV任务,允许PDA持续申请下单
        // 未完成任务的检查将在发送AGV请求时进行(AgvHandler.callAgv)
        List<BasDevp> devList = canInSites.isEmpty() ? new ArrayList<>() : basDevpMapper.selectList(new EntityWrapper<BasDevp>()
                .in("dev_no", canInSites)
                .eq("in_enable", "Y") // in_enable是能入
                .eq("canining", "Y") // canining是可入
                .ne("dev_no", 0) // 排除dev_no=0的无效站点
        ).stream()
                .filter(dev -> dev.getDevNo() != null && dev.getDevNo() != 0) // 再次过滤,确保不为null或0
                .collect(Collectors.toList());
        // 选择站点(如果可入站点为空,则不在创建任务时分配站点,由定时任务分配)
        Integer endSite = null;
        if (!devList.isEmpty()) {
            // 入库任务数排序
            devList.sort(Comparator.comparing(BasDevp::getInQty));
            // 选择站点
            BasDevp basDevp;
            // 获取最少任务数
            int minInQty = devList.get(0).getInQty();
            // 筛选出任务数最少的站点列表
            List<BasDevp> minTaskSites = devList.stream()
                    .filter(dev -> dev.getInQty() == minInQty)
                    .collect(Collectors.toList());
            // 根据配置选择分配策略
            String strategy = agvProperties.getSiteAllocation().getStrategy();
            boolean enableRoundRobin = agvProperties.getSiteAllocation().isEnableRoundRobin();
            if (minTaskSites.size() > 1 && enableRoundRobin && "round-robin".equals(strategy)) {
                // 轮询分配:当多个站点任务数相同时,使用轮询
                String groupKey = (whsType != null && whsType.equals(agvProperties.getWhsTypeMapping().getInboundArea()))
                        ? "east" : "west";
                AtomicInteger counter = siteRoundRobinCounters.computeIfAbsent(groupKey, k -> new AtomicInteger(0));
                int index = counter.getAndIncrement() % minTaskSites.size();
                basDevp = minTaskSites.get(index);
                log.info("使用轮询分配策略,站点组:{},轮询索引:{},选中站点:{}", groupKey, index, basDevp.getDevNo());
            } else if (minTaskSites.size() > 1 && enableRoundRobin && "random".equals(strategy)) {
                // 随机分配
                Random random = new Random();
                int index = random.nextInt(minTaskSites.size());
                basDevp = minTaskSites.get(index);
                log.info("使用随机分配策略,选中站点:{}", basDevp.getDevNo());
            } else {
                // 默认:选择第一个(任务最少的)
                basDevp = devList.get(0);
                if (minTaskSites.size() > 1) {
                    log.info("多个站点任务数相同({}),但未启用轮询,选择第一个站点:{}", minInQty, basDevp.getDevNo());
                }
            }
            endSite = basDevp.getDevNo();
            // 检查站点是否有效(不能为0或null)
            if (endSite == null || endSite == 0) {
                log.error("分配的站点无效(dev_no={}),不分配站点,将在定时任务中分配", endSite);
                endSite = null; // 设置为null,由定时任务分配
            } else {
                // 入库暂存+1
                basDevpMapper.incrementInQty(endSite);
            }
        } else {
            // 没有可入站点,记录日志但不阻止下单,站点分配将在定时任务中处理
            String groupName = (whsType != null && whsType.equals(agvProperties.getWhsTypeMapping().getInboundArea()))
                    ? agvProperties.getEastDisplayName() : agvProperties.getWestDisplayName();
//            log.warn("{}可用站点({})中没有可入站点(in_enable='Y'且canining='Y'),暂不分配站点,将在定时任务中分配", groupName, canInSites);
        }
        // 站点分配完全由定时任务处理,此处不分配站点,只创建任务
        log.info("创建AGV任务,站点分配将在定时任务中处理,机器人组:{}", robotGroup);
        // 获取工作号
@@ -363,7 +262,7 @@
                .setIoType(ioType) // 入出库状态: 1.入库
                .setTaskType("agv")
                .setIoPri(10D)
                .setStaNo(endSite != null ? String.valueOf(endSite) : null) // 如果分配了站点则设置,否则为null,由定时任务分配
                .setStaNo(null) // 站点分配完全由定时任务处理
                .setSourceStaNo(sourceSite) // 设置源站点
                .setInvWh(robotGroup) // 根据whs_type设置机器人组
                .setFullPlt(ioType == 10 ? "N" : "Y")// 空托入库(ioType=10)设置为N,其他入库设置为Y(满板)
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java
@@ -12,6 +12,7 @@
import com.zy.asrs.entity.result.StockVo;
import com.zy.asrs.mapper.TagMapper;
import com.zy.asrs.service.*;
import com.zy.asrs.task.handler.AgvHandler;
import com.zy.asrs.utils.MatUtils;
import com.zy.common.model.DetlDto;
import com.zy.common.utils.NodeUtils;
@@ -22,6 +23,7 @@
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@@ -58,6 +60,9 @@
    @Resource
    private TaskService taskService;
    @Resource
    private AgvHandler agvHandler;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public R agvCallback(AgvCallbackDto param) {
@@ -74,20 +79,48 @@
            param.getKind());
        
        // 根据taskId查询任务
        // taskId格式:T + 工作号(如"T5299")或 T + 任务ID(向后兼容)
        Task task = null;
        try {
            // 处理"T"前缀格式(如"T130"),这是AGV任务创建时的格式
            // 处理"T"前缀格式(如"T5299"或"T130")
            String numericId = taskId;
            if (taskId.startsWith("T") && taskId.length() > 1) {
                numericId = taskId.substring(1);
                log.debug("检测到T前缀格式的taskId,提取数字ID:{}", numericId);
                    log.debug("检测到T前缀格式的taskId,提取数字:{}", numericId);
            }
            // 尝试将taskId解析为Long类型的id
            Long taskIdLong = Long.parseLong(numericId);
            task = taskService.selectById(taskIdLong);
        } catch (NumberFormatException e) {
            // 如果不是数字,尝试通过其他字段查询(如sheetNo等)
            log.debug("taskId不是数字格式,尝试通过其他字段查询,taskId:{}", taskId);
            // 先尝试通过工作号(wrk_no)查询(优先,因为现在使用工作号作为taskId)
            try {
                Integer wrkNo = Integer.parseInt(numericId);
                task = taskService.selectOne(
                    new EntityWrapper<Task>().eq("wrk_no", wrkNo)
                );
                if (task != null) {
                    log.debug("通过工作号(wrk_no={})找到任务,taskId:{}", wrkNo, task.getId());
                }
            } catch (NumberFormatException e) {
                log.debug("无法解析为工作号,taskId:{}", taskId);
            }
            // 如果通过工作号没找到,尝试通过任务ID查询(向后兼容)
            if (task == null) {
                try {
                    Long taskIdLong = Long.parseLong(numericId);
                    task = taskService.selectById(taskIdLong);
                    if (task != null) {
                        log.debug("通过任务ID(id={})找到任务,taskId:{}", taskIdLong, taskId);
                    }
                } catch (NumberFormatException e) {
                    log.debug("无法解析为任务ID,taskId:{}", taskId);
                }
            }
        } catch (Exception e) {
            log.debug("解析taskId异常,taskId:{},异常:{}", taskId, e.getMessage());
        }
        // 如果还是没找到,尝试通过其他字段查询(如sheetNo等,向后兼容)
        if (task == null) {
            log.debug("通过工作号和任务ID都没找到,尝试通过其他字段查询,taskId:{}", taskId);
            task = taskService.selectOne(
                new EntityWrapper<Task>().eq("sheet_no", taskId)
            );
@@ -114,6 +147,9 @@
        
        task.setModiTime(new Date());
        
        // 标记任务是否已经被转移到历史表
        boolean taskMovedToHistory = false;
        // 如果有status字段,按原有逻辑处理(向后兼容)
        if (!Cools.isEmpty(param.getStatus())) {
            switch (param.getStatus()) {
@@ -128,7 +164,29 @@
                    }
                    break;
                case "Done":
                    task.setWrkSts(9L);
                    // status="Done" 才算搬运完成,结束掉AGV任务(适用于所有任务类型:入库、出库、转移)
                    if (task.getWrkSts() != null && task.getWrkSts() == 8L) {
                        task.setWrkSts(9L);
                        task.setModiTime(new Date());
                        String taskTypeName = "";
                        if (!Cools.isEmpty(param.getKind())) {
                            taskTypeName = param.getKind();
                        }
                        log.info("AGV回调搬运完成(status=Done),完结AGV呼叫单,taskId:{},任务类型:{}", taskId, taskTypeName);
                        // 保存任务更新后,转移到历史表
                        if (taskService.updateById(task)) {
                            try {
                                agvHandler.moveTaskToHistory(Collections.singletonList(task));
                                taskMovedToHistory = true;
                                log.info("AGV回调搬运完成,已将任务转移到历史表,taskId:{}", taskId);
                            } catch (Exception e) {
                                log.error("AGV回调搬运完成,转移任务到历史表失败,taskId:{}", taskId, e);
                            }
                        }
                    } else {
                        log.debug("AGV回调status=Done,但任务状态不是8(当前状态:{}),不结束任务,taskId:{}",
                            task.getWrkSts(), taskId);
                    }
                    break;
                case "Failed":
                case "Cancelled":
@@ -137,26 +195,26 @@
            }
        }
        
        // 根据任务类型进行相应处理
        // 根据任务类型进行相应处理(记录日志,但不在此处结束任务,任务结束统一由status="Done"处理)
        if (!Cools.isEmpty(param.getKind())) {
            String kind = param.getKind();
            if ("货物转运".equals(kind)) {
                log.info("处理货物转运任务,taskId:{}", taskId);
                log.info("处理货物转运任务,taskId:{},status:{}", taskId, param.getStatus());
            } else if ("实托入库".equals(kind) || "空托入库".equals(kind)) {
                log.info("处理入库任务,taskId:{}", taskId);
                // 入库任务:如果收到确认取货回调(loaded=true),完结AGV呼叫单
                if (Boolean.TRUE.equals(param.getLoaded())) {
                    // 如果任务状态是8(已呼叫AGV),更新为9(任务完成)
                    if (task.getWrkSts() != null && task.getWrkSts() == 8L) {
                        task.setWrkSts(9L);
                        log.info("入库任务收到确认取货回调,完结AGV呼叫单,taskId:{}", taskId);
                    }
                }
                log.info("处理入库任务,taskId:{},status:{}", taskId, param.getStatus());
                // 注意:任务结束统一由 status="Done" 处理,此处不再重复处理
            } else if ("实托出库".equals(kind)) {
                log.info("处理实托出库任务,taskId:{}", taskId);
                log.info("处理实托出库任务,taskId:{},status:{}", taskId, param.getStatus());
                // 注意:任务结束统一由 status="Done" 处理,此处不再重复处理
            }
        }
        
        // 如果任务已经被转移到历史表,直接返回成功
        if (taskMovedToHistory) {
            log.info("AGV任务回调处理成功,任务已转移到历史表,taskId:{}", taskId);
            return R.ok();
        }
        // 保存任务更新
        if (!taskService.updateById(task)) {
            log.error("更新任务失败,taskId:{}", taskId);
src/main/java/com/zy/asrs/service/impl/TaskLogServiceImpl.java
@@ -39,7 +39,9 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean save(Integer wrkNo) {
        Task task = taskService.selectOne(new EntityWrapper<Task>().eq("wrk_no", wrkNo));
        Task task = taskService.selectOne(new EntityWrapper<Task>()
                .eq("wrk_no", wrkNo)
                .andNew("(is_deleted = 0)"));
        if (Objects.isNull(task)) {
            throw new CoolException("数据错误:任务不存在!!");
        }
@@ -51,18 +53,18 @@
        }
        List<TaskDetl> detls = taskDetlService.selectList(new EntityWrapper<TaskDetl>().eq("wrk_no", wrkNo));
        if (Objects.isNull(detls) || detls.isEmpty()) {
            throw new CoolException("数据错误:任务不存在!!");
        // 空托入库(io_type=10)和空托出库(io_type=110)可能没有明细,允许为空
        if (Objects.nonNull(detls) && !detls.isEmpty()) {
            detls.forEach(detl -> {
                TaskDetlLog detlLog = new TaskDetlLog();
                BeanUtils.copyProperties(detl, detlLog);
                detlLog.setId(null);
                detlLog.setTaskID(taskLog.getId());
                if (!taskDetlLogService.insert(detlLog)) {
                    throw new CoolException("工作档明细历史保存失败!!");
                }
            });
        }
        detls.forEach(detl -> {
            TaskDetlLog detlLog = new TaskDetlLog();
            BeanUtils.copyProperties(detl, detlLog);
            detlLog.setId(null);
            detlLog.setTaskID(taskLog.getId());
            if (!taskDetlLogService.insert(detlLog)) {
                throw new CoolException("工作档明细历史保存失败!!");
            }
        });
        return true;
    }
src/main/java/com/zy/asrs/service/impl/TaskServiceImpl.java
@@ -40,7 +40,9 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void completeWrkMast(String workNo, Long userId) {
        Task wrkMast = this.selectOne(new EntityWrapper<Task>().eq("wrk_no", workNo));
        Task wrkMast = this.selectOne(new EntityWrapper<Task>()
                .eq("wrk_no", workNo)
                .andNew("(is_deleted = 0)"));
        if (Cools.isEmpty(wrkMast)) {
            throw new CoolException(workNo + "工作档不存在");
        }
@@ -69,7 +71,9 @@
    @Override
    @Transactional
    public void pickWrkMast(String workNo, Long userId) {
        Task wrkMast = this.selectById(workNo);
        Task wrkMast = this.selectOne(new EntityWrapper<Task>()
                .eq("wrk_no", workNo)
                .andNew("(is_deleted = 0)"));
        if (Cools.isEmpty(wrkMast)) {
            throw new CoolException(workNo + "工作档不存在");
        }
@@ -133,7 +137,9 @@
    @Transactional
    public void cancelWrkMast(String workNo, Long userId) {
        Date now = new Date();
        Task wrkMast = this.selectOne(new EntityWrapper<Task>().eq("wrk_no", workNo));
        Task wrkMast = this.selectOne(new EntityWrapper<Task>()
                .eq("wrk_no", workNo)
                .andNew("(is_deleted = 0)"));
        if (Cools.isEmpty(wrkMast)) {
            throw new CoolException(workNo + "工作档不存在");
        }
@@ -209,23 +215,29 @@
        if (!taskLogService.save(wrkMast.getWrkNo())) {
            throw new CoolException("保存工作历史档失败, workNo = " + wrkMast.getWrkNo());
        }
        // 删除工作主档
        boolean wrkMastRes = taskService.deleteById(wrkMast);
        // 逻辑删除工作主档
        wrkMast.setIsDeleted(1);
        boolean wrkMastRes = taskService.updateById(wrkMast);
        if (wrkMast.getIoType() != 10 && wrkMast.getIoType() != 110) {
            // 删除工作档明细
            boolean wrkDetlRes = taskDetlService.delete(new EntityWrapper<TaskDetl>().eq("wrk_no", workNo));
        }
        // 修改库位状态
        LocCache locMast = locCacheService.selectOne(new EntityWrapper<LocCache>().eq("loc_no", locNo));
        if (Cools.isEmpty(locMast)) {
            throw new CoolException("取消工作档失败,库位不存在:" + locNo);
        // 修改库位状态(如果库位不为空)
        boolean locMastRes = true;
        if (!Cools.isEmpty(locNo)) {
            LocCache locMast = locCacheService.selectOne(new EntityWrapper<LocCache>().eq("loc_no", locNo));
            if (Cools.isEmpty(locMast)) {
                throw new CoolException("取消工作档失败,库位不存在:" + locNo);
            }
            if (!Cools.isEmpty(locSts)) {
                locMast.setLocSts(locSts);
                locMast.setModiTime(now);
                locMast.setModiUser(userId);
                locMastRes = locCacheService.updateById(locMast);
            }
        }
        locMast.setLocSts(locSts);
        locMast.setModiTime(now);
        locMast.setModiUser(userId);
        boolean locMastRes = locCacheService.updateById(locMast);
        if (!wrkMastRes || !locMastRes) {
            throw new CoolException("保存数据失败");
        }
src/main/java/com/zy/asrs/task/AgvScheduler.java
@@ -21,6 +21,7 @@
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@@ -55,23 +56,170 @@
    private SchedulerProperties schedulerProperties;
    /**
     * 呼叫agv搬运
     * 记录上次处理的任务ID,用于轮询处理
     * 确保每次处理不同的任务,避免一直处理同一个任务
     */
    private Long lastProcessedTaskId = null;
    /**
     * 记录上次分配站点的任务ID,用于轮询处理
     */
    private Long lastAllocatedTaskId = null;
    /**
     * 分配站点定时任务
     * 查询状态7(待呼叫AGV)但没有分配站点的任务,为其分配可用站点
     * 只负责分配站点,不呼叫AGV
     * 每次只处理一个任务,避免高并发执行
     */
    @Scheduled(cron = "0/5 * * * * ? ")
    private void allocateSite() {
        if (!schedulerProperties.isEnabled()) {
            log.debug("定时任务allocateSite:调度器未启用,跳过执行");
            return;
        }
        // 构建查询条件:查询所有待呼叫AGV但没有分配站点的任务
        EntityWrapper<Task> wrapper = new EntityWrapper<Task>();
        wrapper.eq("wrk_sts", 7); // 待呼叫AGV状态
        wrapper.eq("task_type", "agv"); // AGV任务类型
        wrapper.andNew("(is_deleted = 0)");
        wrapper.andNew()
            .isNull("sta_no")
            .or()
            .eq("sta_no", "")
            .or()
            .eq("sta_no", "0");
        wrapper.orderBy("id", true); // 按id升序排序
        // 如果上次处理过任务,从下一个任务开始查询(轮询)
        if (lastAllocatedTaskId != null) {
            wrapper.gt("id", lastAllocatedTaskId);
        }
        // 查询待分配站点的任务,每次只查询一个
        List<Task> taskList = taskService.selectList(
            wrapper.last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY")
        );
        // 如果从上次任务之后没有找到任务,从头开始查询(实现循环轮询)
        if (taskList.isEmpty() && lastAllocatedTaskId != null) {
            lastAllocatedTaskId = null; // 重置,从头开始
            taskList = taskService.selectList(
                new EntityWrapper<Task>()
                    .eq("wrk_sts", 7)
                    .eq("task_type", "agv")
                    .andNew("(is_deleted = 0)")
                    .andNew()
                    .isNull("sta_no")
                    .or()
                    .eq("sta_no", "")
                    .or()
                    .eq("sta_no", "0")
                    .orderBy("id", true)
                    .last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY")
            );
        }
        if(taskList.isEmpty()) {
            log.debug("定时任务allocateSite:没有待分配站点的任务(wrk_sts=7,task_type=agv,sta_no为空)");
            return;
        }
        // 调用分配站点逻辑
        Task task = taskList.get(0);
        String displayTaskId = (task.getWrkNo() != null) ? String.valueOf(task.getWrkNo()) : String.valueOf(task.getId());
        log.info("定时任务allocateSite:开始为任务ID:{}分配站点(wrk_no={},ioType={})",
            displayTaskId, task.getWrkNo(), task.getIoType());
        String errorMsg = agvHandler.allocateSiteForTask(task);
        // 检查是否成功分配了站点
        String staNo = task.getStaNo();
        if (errorMsg == null && staNo != null && !staNo.isEmpty() && !staNo.equals("0")) {
            // 分配站点成功
            lastAllocatedTaskId = task.getId();
            log.info("定时任务allocateSite:任务ID:{}成功分配站点:{},更新lastAllocatedTaskId为{}",
                displayTaskId, staNo, lastAllocatedTaskId);
        } else {
            // 无法分配站点,不更新lastAllocatedTaskId,下次会重新尝试
            log.info("定时任务allocateSite:任务ID:{}无法分配站点:{},不更新lastAllocatedTaskId(当前:{}),下次将重新尝试",
                displayTaskId, errorMsg != null ? errorMsg : "所有站点都被占用", lastAllocatedTaskId);
        }
    }
    /**
     * 呼叫AGV定时任务
     * 查询状态7(待呼叫AGV)且已分配站点的任务,呼叫AGV
     * 呼叫成功后,状态从7(待呼叫AGV)变为8(正在搬运)
     * 每次只处理一个任务,避免高并发执行
     * 使用轮询机制,确保不会一直处理同一个任务
     */
    @Scheduled(cron = "0/5 * * * * ? ")
    private void callAgv() {
        if (!schedulerProperties.isEnabled()) {
            log.debug("定时任务callAgv:调度器未启用,跳过执行");
            return;
        }
        // 查询待呼叫agv任务,按id升序排序(id最小的优先呼叫)
        // 构建查询条件:查询状态7(待呼叫AGV)且已分配站点的任务
        EntityWrapper<Task> wrapper = new EntityWrapper<Task>();
        wrapper.eq("wrk_sts", 7); // 待呼叫AGV状态
        wrapper.eq("task_type", "agv"); // AGV任务类型
        wrapper.andNew("(is_deleted = 0)");
        wrapper.isNotNull("sta_no"); // 必须有站点分配
        wrapper.ne("sta_no", ""); // 站点不能为空字符串
        wrapper.ne("sta_no", "0"); // 站点不能为0
        wrapper.orderBy("id", true); // 按id升序排序
        // 如果上次处理过任务,从下一个任务开始查询(轮询)
        if (lastProcessedTaskId != null) {
            wrapper.gt("id", lastProcessedTaskId);
        }
        // 查询待呼叫agv任务,每次只查询一个
        List<Task> taskList = taskService.selectList(
            new EntityWrapper<Task>()
                .eq("wrk_sts", 7)
                .orderBy("id", true) // 按id升序,id最小的优先
            wrapper.last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY")
        );
        // 如果从上次任务之后没有找到任务,从头开始查询(实现循环轮询)
        if (taskList.isEmpty() && lastProcessedTaskId != null) {
            lastProcessedTaskId = null; // 重置,从头开始
            taskList = taskService.selectList(
                new EntityWrapper<Task>()
                    .eq("wrk_sts", 7)
                    .eq("task_type", "agv")
                    .andNew("(is_deleted = 0)")
                    .isNotNull("sta_no")
                    .ne("sta_no", "")
                    .ne("sta_no", "0")
                    .orderBy("id", true)
                    .last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY")
            );
        }
        if(taskList.isEmpty()) {
            log.debug("定时任务callAgv:没有待呼叫AGV的任务(wrk_sts=7,task_type=agv,sta_no不为空)");
            return;
        }
        agvHandler.callAgv(taskList);
        // 调用处理逻辑:呼叫AGV,成功后状态从7变为8
        Task task = taskList.get(0);
        String displayTaskId = (task.getWrkNo() != null) ? String.valueOf(task.getWrkNo()) : String.valueOf(task.getId());
        log.info("定时任务callAgv:开始处理任务ID:{}(wrk_no={},ioType={},sta_no={})",
            displayTaskId, task.getWrkNo(), task.getIoType(), task.getStaNo());
        boolean processed = agvHandler.callAgv(taskList);
        // 只有当任务成功处理(成功呼叫AGV,状态从7变为8)时,才更新lastProcessedTaskId
        // 如果任务被跳过(站点被占用等),不更新lastProcessedTaskId,下次会重新尝试
        if (processed) {
            lastProcessedTaskId = task.getId();
            log.info("定时任务callAgv:任务ID:{}成功呼叫AGV,状态已从7变为8,更新lastProcessedTaskId为{},下次将处理下一个任务",
                displayTaskId, lastProcessedTaskId);
        } else {
            log.info("定时任务callAgv:任务ID:{}被跳过,不更新lastProcessedTaskId(当前:{}),下次将重新尝试处理此任务",
                displayTaskId, lastProcessedTaskId);
        }
    }
    /**
@@ -115,7 +263,7 @@
        if (!schedulerProperties.isEnabled()) {
            return;
        }
        List<Task> taskList = taskService.selectList(new EntityWrapper<Task>().eq("wrk_sts", 9));
        List<Task> taskList = taskService.selectList(new EntityWrapper<Task>().eq("wrk_sts", 9).andNew("(is_deleted = 0)"));
        if(taskList.isEmpty()) {
            return;
        }
@@ -153,7 +301,8 @@
                Wrapper<Task> taskWrapper1 = new EntityWrapper<Task>()
                    .eq("task_type", "agv")
                    .eq("wrk_sts", 8L)  // 已呼叫AGV状态
                    .eq("wrk_no", wrkMast.getWrkNo());
                    .eq("wrk_no", wrkMast.getWrkNo())
                    .andNew("(is_deleted = 0)");
                List<Task> agvTasks = taskService.selectList(taskWrapper1);
                
                // 如果通过wrk_no没找到,且有条码,则通过条码查询
@@ -161,7 +310,8 @@
                    Wrapper<Task> taskWrapper2 = new EntityWrapper<Task>()
                        .eq("task_type", "agv")
                        .eq("wrk_sts", 8L)
                        .eq("barcode", wrkMast.getBarcode());
                        .eq("barcode", wrkMast.getBarcode())
                        .andNew("(is_deleted = 0)");
                    agvTasks = taskService.selectList(taskWrapper2);
                }
                
@@ -176,8 +326,10 @@
                        if (taskService.updateById(agvTask)) {
                            completedTasks.add(agvTask);
                            completedCount++;
                            // taskId使用工作号(wrk_no),如果工作号为空则使用任务ID
                            String displayTaskId = (agvTask.getWrkNo() != null) ? String.valueOf(agvTask.getWrkNo()) : String.valueOf(agvTask.getId());
                            log.info("入库任务工作档已入库成功,完结AGV呼叫单,taskId:{},wrkNo:{},barcode:{}", 
                                agvTask.getId(), wrkMast.getWrkNo(), wrkMast.getBarcode());
                                displayTaskId, wrkMast.getWrkNo(), wrkMast.getBarcode());
                        }
                    }
                }
@@ -202,11 +354,131 @@
    }
    /**
     * 检查并修复异常状态的AGV任务:正在搬运但没有分配站点
     * 这种情况可能是数据异常或并发问题导致的
     */
    @Scheduled(cron = "0/30 * * * * ? ")
    private void checkAbnormalTasksWithoutSite() {
        if (!schedulerProperties.isEnabled()) {
            return;
        }
        try {
            // 查询状态为8(正在搬运)但没有分配站点的任务
            List<Task> abnormalTasks = taskService.selectList(
                new EntityWrapper<Task>()
                    .eq("task_type", "agv")
                    .eq("wrk_sts", 8L)  // 正在搬运
                    .andNew("(is_deleted = 0)")
                    .andNew()
                    .isNull("sta_no")
                    .or()
                    .eq("sta_no", "")
                    .or()
                    .eq("sta_no", "0")
            );
            if (abnormalTasks.isEmpty()) {
                return;
            }
            log.warn("检测到{}个异常状态的AGV任务:正在搬运但没有分配站点,开始修复", abnormalTasks.size());
            Date now = new Date();
            int fixedCount = 0;
            int completedCount = 0;
            for (Task task : abnormalTasks) {
                String displayTaskId = (task.getWrkNo() != null) ? String.valueOf(task.getWrkNo()) : String.valueOf(task.getId());
                // 检查工作档和历史档状态
                WrkMast wrkMast = null;
                WrkMastLog wrkMastLog = null;
                if (task.getWrkNo() != null) {
                    wrkMast = wrkMastService.selectOne(
                        new EntityWrapper<WrkMast>().eq("wrk_no", task.getWrkNo())
                    );
                    wrkMastLog = wrkMastLogService.selectOne(
                        new EntityWrapper<WrkMastLog>().eq("wrk_no", task.getWrkNo())
                    );
                }
                // 如果工作档已完成或已转历史档,直接结束任务
                boolean shouldComplete = false;
                String reason = "";
                if (wrkMastLog != null) {
                    shouldComplete = true;
                    reason = "工作档已转历史档";
                } else if (wrkMast != null) {
                    Long wrkSts = wrkMast.getWrkSts();
                    Integer ioType = task.getIoType();
                    if (wrkSts != null && ioType != null) {
                        // 入库任务:状态4或5
                        if ((ioType == 1 || ioType == 10 || ioType == 53 || ioType == 57) &&
                            (wrkSts == 4L || wrkSts == 5L)) {
                            shouldComplete = true;
                            reason = String.format("工作档已完成(入库),状态:%d", wrkSts);
                        }
                        // 出库任务:状态14或15
                        else if ((ioType == 101 || ioType == 110 || ioType == 103 || ioType == 107) &&
                                 (wrkSts == 14L || wrkSts == 15L)) {
                            shouldComplete = true;
                            reason = String.format("工作档已完成(出库),状态:%d", wrkSts);
                        }
                    }
                }
                if (shouldComplete) {
                    // 工作档已完成,直接结束任务
                    task.setWrkSts(9L);
                    task.setModiTime(now);
                    if (taskService.updateById(task)) {
                        try {
                            agvHandler.moveTaskToHistory(Collections.singletonList(task));
                            completedCount++;
                            log.info("修复异常任务:{},{},已结束任务并转移到历史表,taskId:{}",
                                reason, displayTaskId, displayTaskId);
                        } catch (Exception e) {
                            log.error("修复异常任务:转移任务到历史表失败,taskId:{}", displayTaskId, e);
                        }
                    }
                } else {
                    // 工作档未完成,尝试分配站点或重置状态
                    // 先尝试分配站点
                    String errorMsg = agvHandler.allocateSiteForTask(task);
                    if (errorMsg == null && task.getStaNo() != null && !task.getStaNo().isEmpty() && !task.getStaNo().equals("0")) {
                        // 分配站点成功
                        fixedCount++;
                        log.info("修复异常任务:已为任务分配站点,taskId:{},站点:{}", displayTaskId, task.getStaNo());
                    } else {
                        // 无法分配站点,重置状态为7(待呼叫AGV),等待下次分配
                        task.setWrkSts(7L);
                        task.setModiTime(now);
                        if (taskService.updateById(task)) {
                            fixedCount++;
                            log.warn("修复异常任务:无法分配站点,重置状态为7(待呼叫AGV),taskId:{},原因:{}",
                                displayTaskId, errorMsg != null ? errorMsg : "所有站点都被占用");
                        }
                    }
                }
            }
            if (fixedCount > 0 || completedCount > 0) {
                log.info("修复异常任务完成:修复了{}个任务,结束了{}个已完成工作档的任务", fixedCount, completedCount);
            }
        } catch (Exception e) {
            log.error("检查并修复异常状态的AGV任务异常", e);
        }
    }
    /**
     * 检查AGV任务对应的工作档是否已完成或已转历史档并完结
     * 处理被跳过的AGV任务:
     * 1. 如果工作档已完成(wrk_sts=4,5,14,15)或已转历史档并完结,则完结AGV任务
     * 2. 如果工作档和历史档都没有数据,但是AGV已确认接收命令后超过1小时处于搬运中(状态8),也结束AGV任务
     *    注意:只有AGV确认接收命令后(plcStrTime不为空)才开始计时,如果AGV接受命令失败,会继续呼叫AGV
     * 1. 如果工作档已完成(wrk_sts=4,5,14,15),则完结AGV任务
     * 2. 如果工作档进入历史档,立即结束AGV任务
     * 3. 如果入库成功,也结束掉搬运任务(已在checkInboundCompletedTasks中实现)
     */
    @Scheduled(cron = "0/10 * * * * ? ")
    private void checkCompletedTasksInHistory() {
@@ -219,6 +491,7 @@
                new EntityWrapper<Task>()
                    .eq("task_type", "agv")
                    .eq("wrk_sts", 8L)  // 已呼叫AGV状态
                    .andNew("(is_deleted = 0)")
            );
            
            if (agvTasks.isEmpty()) {
@@ -228,8 +501,6 @@
            Date now = new Date();
            int completedCount = 0;
            List<Task> completedTasks = new ArrayList<>();
            // 1小时的毫秒数(从AGV确认接收命令开始计时)
            long oneHourInMillis = 60 * 60 * 1000L;
            
            for (Task agvTask : agvTasks) {
                boolean isCompleted = false;
@@ -282,43 +553,10 @@
                    }
                }
                
                // 如果工作档不存在或未完成,检查历史档是否已完结
                // 1. 如果工作档进入历史档,立即结束AGV任务(只要历史档存在就结束)
                if (!isCompleted && wrkMastLog != null) {
                    Integer ioType = agvTask.getIoType();
                    long logWrkSts = wrkMastLog.getWrkSts();
                    if (ioType != null) {
                        // 入库任务:状态5(库存更新完成)
                        if ((ioType == 1 || ioType == 10 || ioType == 53 || ioType == 57) &&
                            logWrkSts == 5L) {
                            isCompleted = true;
                            reason = String.format("工作档已转历史档并完结,历史档状态:%d", logWrkSts);
                        }
                        // 出库任务:状态15(出库更新完成)
                        else if ((ioType == 101 || ioType == 110 || ioType == 103 || ioType == 107) &&
                                 logWrkSts == 15L) {
                            isCompleted = true;
                            reason = String.format("工作档已转历史档并完结,历史档状态:%d", logWrkSts);
                        }
                    }
                }
                // 如果工作档和历史档都没有数据,检查是否超过1小时(从AGV确认接收命令开始计时)
                if (!isCompleted && wrkMast == null && wrkMastLog == null) {
                    // 只有AGV确认接收命令后(plcStrTime不为空)才开始计时
                    // 如果plcStrTime为空,说明AGV还没有确认接收命令,会继续呼叫,不结束任务
                    Date agvConfirmedTime = agvTask.getPlcStrTime();
                    if (agvConfirmedTime != null) {
                        long timeDiff = now.getTime() - agvConfirmedTime.getTime();
                        if (timeDiff >= oneHourInMillis) {
                            isCompleted = true;
                            long hours = timeDiff / (60 * 60 * 1000L);
                            long minutes = (timeDiff % (60 * 60 * 1000L)) / (60 * 1000L);
                            reason = String.format("工作档和历史档都不存在,AGV确认接收命令后超过1小时(实际:%d小时%d分钟)处于搬运中,自动结束", hours, minutes);
                        }
                    }
                    // 如果plcStrTime为空,说明AGV还没有确认接收命令,不结束任务,继续等待呼叫
                    isCompleted = true;
                    reason = String.format("工作档已转历史档,立即结束AGV任务,历史档状态:%d", wrkMastLog.getWrkSts());
                }
                
                // 如果已完成,更新AGV任务状态并收集到列表
@@ -328,8 +566,10 @@
                    if (taskService.updateById(agvTask)) {
                        completedTasks.add(agvTask);
                        completedCount++;
                        // taskId使用工作号(wrk_no),如果工作号为空则使用任务ID
                        String displayTaskId = (agvTask.getWrkNo() != null) ? String.valueOf(agvTask.getWrkNo()) : String.valueOf(agvTask.getId());
                        log.info("{},完结AGV呼叫单,taskId:{},wrkNo:{},barcode:{},站点:{}", 
                            reason, agvTask.getId(), agvTask.getWrkNo(), agvTask.getBarcode(), agvTask.getStaNo());
                            reason, displayTaskId, agvTask.getWrkNo(), agvTask.getBarcode(), agvTask.getStaNo());
                    }
                }
            }
@@ -345,7 +585,7 @@
            }
            
            if (completedCount > 0) {
                log.info("本次检查完结了{}个AGV呼叫单(工作档已完成或已转历史档或确认接收后超时)", completedCount);
                log.info("本次检查完结了{}个AGV呼叫单(工作档已完成或已转历史档)", completedCount);
            }
        } catch (Exception e) {
            log.error("检查工作档已完成或历史档完结任务并完结AGV呼叫单异常", e);
src/main/java/com/zy/asrs/task/handler/AgvHandler.java
@@ -3,6 +3,7 @@
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.core.common.Cools;
import com.zy.asrs.entity.Task;
import com.zy.asrs.entity.TaskLog;
import com.zy.asrs.entity.WrkMast;
@@ -15,6 +16,9 @@
import com.zy.asrs.service.LocCacheService;
import com.zy.asrs.service.TaskLogService;
import com.zy.asrs.service.TaskService;
import com.zy.asrs.service.WrkMastService;
import com.zy.asrs.service.WrkMastLogService;
import com.zy.asrs.entity.WrkMastLog;
import com.zy.common.constant.ApiInterfaceConstant;
import com.zy.common.properties.AgvProperties;
import com.zy.common.utils.HttpHandler;
@@ -62,6 +66,12 @@
    @Resource
    private AgvProperties agvProperties;
    @Resource
    private WrkMastService wrkMastService;
    @Resource
    private WrkMastLogService wrkMastLogService;
    /**
     * 站点轮询计数器,用于平均分配站点
     * Key: 站点组标识(如 "east" 或 "west"),Value: 当前轮询索引
@@ -69,128 +79,266 @@
    private final Map<String, AtomicInteger> siteRoundRobinCounters = new ConcurrentHashMap<>();
    /**
     * 呼叫agv搬运
     * 呼叫AGV
     *
     * 重要:此方法只能从 AgvScheduler.callAgv() 定时任务中调用!
     * 所有AGV呼叫请求必须通过定时任务统一处理,确保:
     * 1. 任务按顺序处理,避免并发冲突
     * 2. 站点分配和AGV呼叫分离,职责清晰
     * 3. 统一的错误处理和重试机制
     *
     * @param taskList 任务列表(通常只包含一个任务)
     * @return 是否成功处理了任务(成功呼叫AGV,状态从7变为8)
     */
    public void callAgv(List<Task> taskList) {
        if (!agvProperties.isSendTask()) {
            return;
    public boolean callAgv(List<Task> taskList) {
        // 记录调用堆栈,确保只能从定时任务调用
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        boolean calledFromScheduler = false;
        for (StackTraceElement element : stackTrace) {
            if (element.getClassName().contains("AgvScheduler") &&
                element.getMethodName().equals("callAgv")) {
                calledFromScheduler = true;
                break;
            }
        }
        if (!calledFromScheduler) {
            log.error("严重错误:callAgv方法只能从AgvScheduler.callAgv()定时任务中调用!当前调用堆栈:{}",
                Arrays.stream(stackTrace).limit(5).map(StackTraceElement::toString).collect(Collectors.joining("\n")));
            return false;
        }
        for (Task task : taskList) {
            // 如果任务状态已经是8(已呼叫AGV,正在搬运),则不再发送指令
            if (task.getWrkSts() != null && task.getWrkSts() == 8L) {
                // log.debug("任务ID:{}状态已是8(正在搬运),跳过发送", task.getId());
                continue;
        if (!agvProperties.isSendTask()) {
            log.warn("AGV呼叫:配置isSendTask=false,不发送AGV任务");
            return false;
        }
        // 每次只处理一个任务,避免高并发执行
        if (taskList == null || taskList.isEmpty()) {
            return false;
        }
        // 只处理第一个任务
        Task task = taskList.get(0);
        // 呼叫AGV定时任务只处理已分配站点的任务,站点分配由单独的定时任务处理
        String staNo = task.getStaNo();
        String displayTaskId = (task.getWrkNo() != null) ? String.valueOf(task.getWrkNo()) : String.valueOf(task.getId());
        // 检查站点是否有效(不为空、不为空字符串、不为0)
        if (staNo == null || staNo.isEmpty() || staNo.equals("0")) {
            // 没有有效站点,跳过(站点分配由allocateSite定时任务处理)
            log.warn("定时任务callAgv:任务ID:{}没有有效站点分配(sta_no={}),跳过发送,等待分配站点定时任务处理", displayTaskId, staNo);
            return false; // 返回false,表示未成功处理
        }
        // 检查站点是否有效(不为0且存在)
        try {
            Integer siteNo = Integer.parseInt(staNo);
            if (siteNo == null || siteNo == 0) {
                log.warn("定时任务callAgv:任务ID:{}的目标站点{}无效(为0),清空站点分配,将重新分配", displayTaskId, staNo);
                task.setStaNo(null);
                taskService.updateById(task);
                return false; // 返回false,让分配站点定时任务重新分配
            }
            // 检查站点是否存在
            List<BasDevp> basDevpList = basDevpMapper.selectList(new EntityWrapper<BasDevp>().eq("dev_no", siteNo));
            if (basDevpList == null || basDevpList.isEmpty()) {
                log.warn("定时任务callAgv:任务ID:{}的目标站点{}不存在,清空站点分配,将重新分配", displayTaskId, staNo);
                task.setStaNo(null);
                taskService.updateById(task);
                return false; // 返回false,让分配站点定时任务重新分配
            }
        } catch (NumberFormatException e) {
            // 站点格式错误,清空站点,让分配站点定时任务重新分配
            log.warn("定时任务callAgv:任务ID:{}的目标站点{}格式错误,清空站点分配,将重新分配", displayTaskId, staNo);
            task.setStaNo(null);
            taskService.updateById(task);
            return false; // 返回false,让分配站点定时任务重新分配
        }
        log.info("定时任务callAgv:任务ID:{}已有站点分配:{},准备发送AGV命令", displayTaskId, staNo);
        // 检查站点是否有状态8的同类型任务,有则跳过(不清空站点)
        // 规则:最多给每个站点分配1条任务,未完成则等待
        // 判断任务是否完成:通过查询agv工作档(wrk_mast),如果查不到就是完成了
        if (staNo != null && !staNo.isEmpty() && task.getIoType() != null) {
            // 根据当前任务类型,只检查同类型的正在搬运任务(状态8)
            // 入库任务(ioType < 100):只检查入库类型的正在搬运任务
            // 出库任务(ioType >= 100):只检查出库类型的正在搬运任务
            List<Integer> ioTypes;
            String taskType;
            if (task.getIoType() < 100) {
                // 入库任务:只检查入库类型(1, 10, 53, 57)
                ioTypes = Arrays.asList(1, 10, 53, 57);
                taskType = "入库";
            } else {
                // 出库任务:只检查出库类型(101, 110, 103, 107)
                ioTypes = Arrays.asList(101, 110, 103, 107);
                taskType = "出库";
            }
            
            // 如果任务没有分配站点,先分配站点(只有为空时才分配,已经分配了不要清空)
            String staNo = task.getStaNo();
            if (staNo == null || staNo.isEmpty()) {
                String errorMsg = allocateSiteForTask(task);
                if (errorMsg != null) {
                    // 无法分配站点,只在日志中记录,不记录到任务中(app不需要知道)
                    // log.warn("任务ID:{}无法分配站点:{}", task.getId(), errorMsg);
                    continue;
                }
                // 检查是否成功分配了站点(如果返回null且staNo仍为空,说明所有站点都在搬运,等待下次再试)
                staNo = task.getStaNo();
                if (staNo == null || staNo.isEmpty()) {
                    // 所有站点都在搬运,暂不分配,等待下次定时任务再尝试
                    continue;
                }
                // log.info("任务ID:{}已分配站点:{}", task.getId(), staNo);
            }
            // 通过SQL查询该站点所有AGV任务(包括状态7和8)
            // 规则:最多给每个站点分配1条任务,未完成则等待
            // 判断任务是否完成:通过查询agv工作档(wrk_mast),如果查不到就是完成了
            List<Task> allTasks = taskService.selectList(
                new EntityWrapper<Task>()
                    .eq("sta_no", staNo)
                    .eq("task_type", "agv")
                    .in("wrk_sts", 7L, 8L)
                    .in("io_type", ioTypes)
                    .ne("id", task.getId()) // 排除当前任务本身
                    .andNew("(is_deleted = 0)")
            );
            
            // 检查目标站点是否有效(不为0且存在)
            if (staNo != null && !staNo.isEmpty()) {
                try {
                    Integer siteNo = Integer.parseInt(staNo);
                    // 检查站点是否为0(无效站点),如果是0则不发送,但不清空站点
                    if (siteNo == null || siteNo == 0) {
                        // log.warn("任务ID:{}的目标站点{}无效(为0),跳过发送,保留站点分配", task.getId(), staNo);
                        continue;
            int taskCount = allTasks != null ? allTasks.size() : 0;
            log.info("定时任务:任务ID:{},站点{}查询到{}个AGV任务(排除当前任务),开始检查是否有未完成的任务",
                displayTaskId, staNo, taskCount);
            // 检查是否有有效的未完成任务(通过查询agv工作档判断)
            boolean hasValidTask = false;
            if (taskCount > 0) {
                for (Task agvTask : allTasks) {
                    if (agvTask.getWrkNo() != null) {
                        // 查询工作档,如果查不到就是完成了
                        WrkMast wrkMast = wrkMastService.selectOne(
                            new EntityWrapper<WrkMast>().eq("wrk_no", agvTask.getWrkNo())
                        );
                        if (wrkMast != null) {
                            // 工作档存在,检查是否已完成
                            Long wrkSts = wrkMast.getWrkSts();
                            Integer ioType = agvTask.getIoType();
                            if (wrkSts != null && ioType != null) {
                                // 入库任务:状态4或5表示完成
                                boolean isCompleted = false;
                                if ((ioType == 1 || ioType == 10 || ioType == 53 || ioType == 57) &&
                                    (wrkSts == 4L || wrkSts == 5L)) {
                                    isCompleted = true;
                                }
                                // 出库任务:状态14或15表示完成
                                else if ((ioType == 101 || ioType == 110 || ioType == 103 || ioType == 107) &&
                                         (wrkSts == 14L || wrkSts == 15L)) {
                                    isCompleted = true;
                                }
                                if (!isCompleted) {
                                    // 工作档存在且未完成,视为有效任务
                                    hasValidTask = true;
                                    log.info("定时任务:任务ID:{},站点{}已有1条未完成的{}AGV任务(工作号:{},工作档状态:{}),跳过当前任务,等待完成",
                                        displayTaskId, staNo, taskType, agvTask.getWrkNo(), wrkSts);
                                    break;
                                } else {
                                    log.info("定时任务:任务ID:{},站点{}的任务{}(工作号:{})已完成(工作档状态:{}),不算有效任务",
                                        displayTaskId, staNo, agvTask.getId(), agvTask.getWrkNo(), wrkSts);
                                }
                            } else {
                                // 工作档存在但状态未知,视为有效任务
                                hasValidTask = true;
                                log.info("定时任务:任务ID:{},站点{}已有1条{}AGV任务(工作号:{},工作档状态未知),跳过当前任务,等待完成",
                                    displayTaskId, staNo, taskType, agvTask.getWrkNo());
                                break;
                            }
                        } else {
                            // 如果工作档查不到,视为已完成,不算有效任务
                            log.info("定时任务:任务ID:{},站点{}的任务{}(工作号:{})工作档查不到,视为已完成,不算有效任务",
                                displayTaskId, staNo, agvTask.getId(), agvTask.getWrkNo());
                        }
                    }
                    List<BasDevp> basDevpList = basDevpMapper.selectList(new EntityWrapper<BasDevp>().eq("dev_no", siteNo));
                    if (basDevpList == null || basDevpList.isEmpty()) {
                        // 站点不存在,跳过发送,不清空站点
                        // log.warn("任务ID:{}的目标站点{}不存在,跳过发送,保留站点分配", task.getId(), staNo);
                        continue;
                    }
                } catch (NumberFormatException e) {
                    // 站点格式错误,跳过发送,不清空站点
                    // log.warn("任务ID:{}的目标站点{}格式错误,跳过发送,保留站点分配", task.getId(), staNo);
                    continue;
                }
            } else {
                // 没有站点,跳过
                continue;
                log.info("定时任务:任务ID:{},站点{}没有其他AGV任务,可以分配", displayTaskId, staNo);
            }
            
            // 检查站点是否有状态8的同类型任务,有则跳过(不清空站点)
            if (staNo != null && !staNo.isEmpty() && task.getIoType() != null) {
                // 根据当前任务类型,只检查同类型的正在搬运任务(状态8)
                // 入库任务(ioType < 100):只检查入库类型的正在搬运任务
                // 出库任务(ioType >= 100):只检查出库类型的正在搬运任务
                List<Integer> ioTypes;
                String taskType;
                if (task.getIoType() < 100) {
                    // 入库任务:只检查入库类型(1, 10, 53, 57)
                    ioTypes = Arrays.asList(1, 10, 53, 57);
                    taskType = "入库";
                } else {
                    // 出库任务:只检查出库类型(101, 110, 103, 107)
                    ioTypes = Arrays.asList(101, 110, 103, 107);
                    taskType = "出库";
            // 如果站点有有效的未完成任务,跳过当前任务
            if (hasValidTask) {
                log.warn("定时任务:任务ID:{},站点{}有未完成的{}AGV任务,跳过当前任务,等待完成。下次将重新尝试处理此任务",
                    displayTaskId, staNo, taskType);
                return false; // 返回false,表示未成功处理,下次会重新尝试
            }
            // 检查是否有状态8且已收到AGV确认的任务(用于日志记录)
            List<Task> transportingTasks = taskService.selectList(
                new EntityWrapper<Task>()
                    .eq("sta_no", staNo)
                    .eq("task_type", "agv")
                    .eq("wrk_sts", 8L) // 只检查正在搬运(8)的任务
                    .isNotNull("plc_str_time") // 只检查已收到AGV确认的任务(plc_str_time不为空)
                    .in("io_type", ioTypes)
                    .ne("id", task.getId()) // 排除当前任务本身
                    .andNew("(is_deleted = 0)")
            );
            // 检查并自动结束已完成工作档的AGV任务
            int originalCount = transportingTasks.size();
            List<Task> validTransportingTasks = checkAndCompleteFinishedTasks(transportingTasks, taskType);
            int completedCount = originalCount - validTransportingTasks.size();
            if (completedCount > 0) {
                log.info("定时任务:站点{}自动结束了{}个已完成工作档的AGV任务,剩余{}个正在搬运的任务",
                    staNo, completedCount, validTransportingTasks.size());
            }
            if (!validTransportingTasks.isEmpty()) {
                List<Integer> transportingTaskIds = validTransportingTasks.stream()
                        .map(Task::getWrkNo)
                        .filter(wrkNo -> wrkNo != null) // 过滤掉null值
                        .collect(Collectors.toList());
                // 记录被占用任务的详细信息
                StringBuilder taskDetailInfo = new StringBuilder();
                for (Task t : validTransportingTasks) {
                    if (taskDetailInfo.length() > 0) {
                        taskDetailInfo.append("; ");
                    }
                    String tDisplayTaskId = (t.getWrkNo() != null) ? String.valueOf(t.getWrkNo()) : String.valueOf(t.getId());
                    taskDetailInfo.append("任务").append(tDisplayTaskId)
                            .append("(wrk_no=").append(t.getWrkNo())
                            .append(",确认时间=").append(t.getPlcStrTime() != null ? t.getPlcStrTime().toString() : "未确认")
                            .append(",创建时间=").append(t.getAppeTime() != null ? t.getAppeTime().toString() : "未知")
                            .append(")");
                }
                // 检查状态为8(已呼叫AGV,正在搬运)的同类型任务
                List<Task> transportingTasks = taskService.selectList(
                    new EntityWrapper<Task>()
                        .eq("sta_no", staNo)
                        .eq("task_type", "agv")
                        .eq("wrk_sts", 8L) // 只检查正在搬运状态的任务
                        .in("io_type", ioTypes)
                        .ne("id", task.getId()) // 排除当前任务本身
                );
                if (!transportingTasks.isEmpty()) {
                    // log.info("站点{}有{}个正在搬运的{}AGV任务,跳过当前任务ID:{}",
                    //         staNo, transportingTasks.size(), taskType, task.getId());
                    continue;
                }
                log.info("定时任务:站点{}有{}个正在搬运的{}AGV任务(工作号:{}),跳过当前任务ID:{},下次将重新尝试。任务详情:{}",
                        staNo, validTransportingTasks.size(), taskType, transportingTaskIds, displayTaskId, taskDetailInfo.toString());
                return false; // 返回false,表示未成功处理,下次会重新尝试
            } else if (completedCount > 0) {
                // 所有占用任务都已结束,站点已释放,可以继续处理当前任务
                log.info("定时任务:站点{}的所有占用任务已自动结束,站点已释放,可以分配给当前任务ID:{}", staNo, task.getId());
            }
            // 呼叫agv
            String response = "";
            boolean success = false;
            String url = ApiInterfaceConstant.AGV_IP + ApiInterfaceConstant.AGV_CREATE_TASK_PATH;
            String namespace = "";
            switch (task.getIoType()) {
                case 1:
                case 10:
                case 53:
                case 57:
                    namespace = "入库";
                    break;
                case 3:
                    namespace = "转移";
                    break;
                case 101:
                case 110:
                case 103:
                case 107:
                    namespace = "出库";
                    break;
                default:
            }
            String body = getRequest(task,namespace);
            // 获取当前重试次数
            int currentRetryCount = getRetryCount(task);
            int maxRetryCount = agvProperties.getCallRetry().getMaxRetryCount();
            boolean retryEnabled = agvProperties.getCallRetry().isEnabled();
        }
        log.info("定时任务:任务ID:{},站点{}检查通过,准备发送AGV命令", displayTaskId, staNo);
        // 呼叫agv
        String response = "";
        boolean success = false;
        String url = ApiInterfaceConstant.AGV_IP + ApiInterfaceConstant.AGV_CREATE_TASK_PATH;
        String namespace = "";
        switch (task.getIoType()) {
            case 1:
            case 10:
            case 53:
            case 57:
                namespace = "入库";
                break;
            case 3:
                namespace = "转移";
                break;
            case 101:
            case 110:
            case 103:
            case 107:
                namespace = "出库";
                break;
            default:
        }
        String body = getRequest(task,namespace);
        // 获取当前重试次数
        int currentRetryCount = getRetryCount(task);
        int maxRetryCount = agvProperties.getCallRetry().getMaxRetryCount();
        boolean retryEnabled = agvProperties.getCallRetry().isEnabled();
            // 如果重试次数已达到最大值,跳过本次发送
            if (retryEnabled && currentRetryCount >= maxRetryCount) {
                // log.warn("{}呼叫agv搬运 - 任务ID:{}已达到最大重试次数({}),停止重试", 
@@ -199,82 +347,191 @@
                task.setErrorTime(new Date());
                task.setErrorMemo(String.format("AGV呼叫失败,已达到最大重试次数(%d次)", maxRetryCount));
                taskService.updateById(task);
                continue;
                return true; // 已达到最大重试次数,不再重试,返回true表示已处理(虽然失败)
            }
        // 打印请求信息(包含重试次数)
        // if (currentRetryCount > 0) {
        //     log.info("{}呼叫agv搬运(第{}次重试) - 请求地址:{}", namespace, currentRetryCount + 1, url);
        // } else {
        //     log.info("{}呼叫agv搬运 - 请求地址:{}", namespace, url);
        // }
        // log.info("{}呼叫agv搬运 - 请求参数:{}", namespace, body);
        boolean result = false; // 默认返回false,表示未成功处理
        try {
            // 使用仙工M4接口
            response = new HttpHandler.Builder()
                    .setUri(ApiInterfaceConstant.AGV_IP)
                    .setPath(ApiInterfaceConstant.AGV_CREATE_TASK_PATH)
                    .setJson(body)
                    .build()
                    .doPost();
            // 打印返回参数
            // log.info("{}呼叫agv搬运 - 返回参数:{}", namespace, response);
            
            // 打印请求信息(包含重试次数)
            // if (currentRetryCount > 0) {
            //     log.info("{}呼叫agv搬运(第{}次重试) - 请求地址:{}", namespace, currentRetryCount + 1, url);
            // } else {
            //     log.info("{}呼叫agv搬运 - 请求地址:{}", namespace, url);
            // }
            // log.info("{}呼叫agv搬运 - 请求参数:{}", namespace, body);
            try {
                // 使用仙工M4接口
                response = new HttpHandler.Builder()
                        .setUri(ApiInterfaceConstant.AGV_IP)
                        .setPath(ApiInterfaceConstant.AGV_CREATE_TASK_PATH)
                        .setJson(body)
                        .build()
                        .doPost();
                // 打印返回参数
                // log.info("{}呼叫agv搬运 - 返回参数:{}", namespace, response);
                // 检查响应是否为空
                if (response == null || response.trim().isEmpty()) {
                    String errorMsg = "AGV接口返回为空";
                    // log.error("{}呼叫agv搬运失败 - 任务ID:{},{}", namespace, task.getId(), errorMsg);
                    handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
                    continue;
                }
                JSONObject jsonObject = JSON.parseObject(response);
                if (jsonObject == null) {
                    String errorMsg = "响应JSON解析失败,响应内容:" + response;
                    // log.error("{}呼叫agv搬运失败 - 任务ID:{},{}", namespace, task.getId(), errorMsg);
                    handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
                    continue;
                }
                Integer code = jsonObject.getInteger("code");
                if (code != null && code.equals(200)) {
                    // 呼叫成功,清除重试次数和错误信息
                    success = true;
                    task.setWrkSts(8L);
                    task.setMemo(clearRetryInfo(task.getMemo())); // 清除重试信息
                    task.setErrorTime(null);
                    task.setErrorMemo(null);
                    taskService.updateById(task);
                    // log.info("{}呼叫agv搬运成功 - 任务ID:{}", namespace, task.getId());
                } else {
                    String message = jsonObject.getString("message");
                    String errorMsg = String.format("错误码:%s,错误信息:%s", code, message);
                    // log.error("{}呼叫agv搬运失败 - 任务ID:{},{}", namespace, task.getId(), errorMsg);
                    handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
                }
            } catch (Exception e) {
                String errorMsg = "异常信息:" + e.getMessage();
                // log.error("{}呼叫agv搬运异常 - 任务ID:{},请求地址:{},请求参数:{},{}",
                //         namespace, task.getId(), url, body, errorMsg, e);
            // 检查响应是否为空
            if (response == null || response.trim().isEmpty()) {
                String errorMsg = "AGV接口返回为空";
                log.warn("定时任务:{}呼叫agv搬运失败 - 任务ID:{},{}", namespace, displayTaskId, errorMsg);
                handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
            } finally {
                try {
                    // 保存接口日志
                    apiLogService.save(
                            namespace + "呼叫agv搬运",
                            url,
                            null,
                            "127.0.0.1",
                            body,
                            response,
                            success
                    );
                } catch (Exception e) {
                    // log.error(namespace + "呼叫agv保存接口日志异常:", e);
                // 如果达到最大重试次数,返回true表示已处理(虽然失败)
                // 否则返回false,让定时任务重新尝试
                if (retryEnabled && currentRetryCount >= maxRetryCount) {
                    result = true; // 已达到最大重试次数,返回true表示已处理(虽然失败)
                    log.info("定时任务:任务ID:{},AGV呼叫失败且已达到最大重试次数({}次),标记为已处理,不再重试",
                        displayTaskId, maxRetryCount);
                } else {
                    result = false; // 返回false,让定时任务重新尝试
                    log.info("定时任务:任务ID:{},下次将重新尝试发送AGV命令", displayTaskId);
                }
            } else {
                // 尝试解析JSON响应,捕获JSON解析异常
                JSONObject jsonObject = null;
                try {
                    jsonObject = JSON.parseObject(response);
                } catch (com.alibaba.fastjson.JSONException e) {
                    // JSON解析失败,响应可能不是有效的JSON格式(如"Server Error"等)
                    String errorMsg = String.format("AGV接口返回非JSON格式响应,响应内容:%s,解析错误:%s", response, e.getMessage());
                    log.error("定时任务:{}呼叫agv搬运失败 - 任务ID:{},{}", namespace, displayTaskId, errorMsg);
                    // 服务器错误时,标记站点为不可用,清空站点分配,不再为当前任务分配站点
                    try {
                        Integer siteNo = Integer.parseInt(staNo);
                        // 查询站点信息
                        List<BasDevp> basDevpList = basDevpMapper.selectList(new EntityWrapper<BasDevp>().eq("dev_no", siteNo));
                        if (basDevpList != null && !basDevpList.isEmpty()) {
                            BasDevp basDevp = basDevpList.get(0);
                            // 标记站点为不可用(设置canining='N')
                            basDevp.setCanining("N");
                            basDevpMapper.updateById(basDevp);
                            log.warn("定时任务:任务ID:{},AGV接口返回服务器错误,已标记站点{}为不可用(canining='N')",
                                displayTaskId, siteNo);
                            // 减少站点的入库任务数(之前分配站点时已经增加了in_qty)
                            basDevpMapper.decrementInQty(siteNo);
                            log.debug("定时任务:任务ID:{},站点{}的in_qty已减少", displayTaskId, siteNo);
                        }
                    } catch (Exception ex) {
                        log.error("定时任务:任务ID:{},标记站点{}为不可用时发生异常:{}", displayTaskId, staNo, ex.getMessage());
                    }
                    // 清空当前任务的站点分配
                    log.warn("定时任务:任务ID:{},AGV接口返回服务器错误,清空站点分配:{},不再为当前任务分配站点",
                        displayTaskId, staNo);
                    task.setStaNo(null);
                    taskService.updateById(task);
                    // 标记任务为失败,不再尝试分配
                    task.setErrorTime(new Date());
                    task.setErrorMemo(String.format("AGV接口返回服务器错误,站点已标记为不可用:%s", errorMsg));
                    taskService.updateById(task);
                    handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
                    // 服务器错误时,不再尝试分配,直接标记为已处理
                    result = true; // 返回true表示已处理(虽然失败),不再尝试分配
                    log.info("定时任务:任务ID:{},AGV呼叫失败(服务器错误),站点已标记为不可用,任务已标记为失败,不再尝试分配",
                        displayTaskId);
                }
                // 如果JSON解析成功,继续处理
                if (jsonObject != null) {
                    Integer code = jsonObject.getInteger("code");
                    if (code != null && code.equals(200)) {
                        // 呼叫成功,清除重试次数和错误信息
                        success = true;
                        // 如果当前状态不是8,更新为8;如果已经是8,保持不变(可能是重试成功)
                        Long currentStatus = task.getWrkSts();
                        if (currentStatus == null || currentStatus != 8L) {
                            task.setWrkSts(8L);
                            log.info("定时任务:{}呼叫agv搬运成功 - 任务ID:{},状态从{}更新为8", namespace, displayTaskId, currentStatus);
                        } else {
                            log.info("定时任务:{}呼叫agv搬运成功(重试) - 任务ID:{},状态保持为8", namespace, displayTaskId);
                        }
                        task.setMemo(clearRetryInfo(task.getMemo())); // 清除重试信息
                        task.setErrorTime(null);
                        task.setErrorMemo(null);
                        taskService.updateById(task);
                        log.info("定时任务:{}呼叫agv搬运成功 - 任务ID:{}", namespace, displayTaskId);
                        result = true; // 返回true,表示成功处理
                    } else {
                        String message = jsonObject.getString("message");
                        String errorMsg = String.format("错误码:%s,错误信息:%s", code, message);
                        log.warn("定时任务:{}呼叫agv搬运失败 - 任务ID:{},{}", namespace, displayTaskId, errorMsg);
                        // 检查是否是站点或库位相关的错误,如果是,清空站点分配,让定时任务重新分配
                        boolean shouldReallocateSite = false;
                        if (message != null) {
                            String lowerMessage = message.toLowerCase();
                            // 库位不存在、站点不存在等错误,应该重新分配站点
                            if (lowerMessage.contains("库位不存在") ||
                                lowerMessage.contains("站点不存在") ||
                                lowerMessage.contains("位置不存在") ||
                                lowerMessage.contains("库位无效") ||
                                lowerMessage.contains("站点无效")) {
                                shouldReallocateSite = true;
                            }
                        }
                        if (shouldReallocateSite) {
                            // 清空站点分配,让定时任务重新分配站点
                            log.warn("定时任务:任务ID:{},AGV呼叫失败({}),清空站点分配:{},下次将重新分配站点",
                                displayTaskId, errorMsg, staNo);
                            task.setStaNo(null);
                            taskService.updateById(task);
                        }
                        handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
                        // 如果达到最大重试次数,返回true表示已处理(虽然失败)
                        // 否则返回false,让定时任务重新尝试(如果站点被清空,会重新分配站点;如果站点未清空,会重新发送AGV)
                        if (retryEnabled && currentRetryCount >= maxRetryCount) {
                            result = true; // 已达到最大重试次数,返回true表示已处理(虽然失败)
                            log.info("定时任务:任务ID:{},AGV呼叫失败且已达到最大重试次数({}次),标记为已处理,不再重试",
                                displayTaskId, maxRetryCount);
                        } else {
                            result = false; // 返回false,让定时任务重新尝试(重新分配站点或重新发送AGV)
                            if (shouldReallocateSite) {
                                log.info("定时任务:任务ID:{},站点已清空,下次将重新分配站点", displayTaskId);
                            } else {
                                log.info("定时任务:任务ID:{},下次将重新尝试发送AGV命令", displayTaskId);
                            }
                        }
                    }
                }
            }
        } catch (Exception e) {
            String errorMsg = "异常信息:" + e.getMessage();
            log.error("定时任务:{}呼叫agv搬运异常 - 任务ID:{},请求地址:{},请求参数:{},{}",
                    namespace, displayTaskId, url, body, errorMsg, e);
            handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
            // 如果达到最大重试次数,返回true表示已处理(虽然失败)
            // 否则返回false,让定时任务重新尝试
            if (retryEnabled && currentRetryCount >= maxRetryCount) {
                result = true; // 已达到最大重试次数,返回true表示已处理(虽然失败)
                log.info("定时任务:任务ID:{},AGV呼叫异常且已达到最大重试次数({}次),标记为已处理,不再重试",
                    displayTaskId, maxRetryCount);
            } else {
                result = false; // 返回false,让定时任务重新尝试
                log.info("定时任务:任务ID:{},下次将重新尝试发送AGV命令", displayTaskId);
            }
        } finally {
            try {
                // 保存接口日志
                apiLogService.save(
                        namespace + "呼叫agv搬运",
                        url,
                        null,
                        "127.0.0.1",
                        body,
                        response,
                        success
                );
            } catch (Exception e) {
                // log.error(namespace + "呼叫agv保存接口日志异常:", e);
            }
        }
        return result;
    }
    /**
@@ -309,6 +566,114 @@
            // log.warn("{}呼叫agv搬运失败 - 任务ID:{},停止重试。错误信息:{}", 
            //         namespace, task.getId(), errorMsg);
        }
    }
    /**
     * 检查并自动结束已完成工作档的AGV任务
     * 如果任务对应的工作档已经完成(入库成功),则自动结束该AGV任务
     * @param transportingTasks 正在搬运的任务列表
     * @param taskTypeName 任务类型名称(用于日志)
     * @return 仍然有效的正在搬运的任务列表(已完成的已被移除)
     */
    private List<Task> checkAndCompleteFinishedTasks(List<Task> transportingTasks, String taskTypeName) {
        if (transportingTasks == null || transportingTasks.isEmpty()) {
            return transportingTasks;
        }
        List<Task> validTasks = new ArrayList<>();
        Date now = new Date();
        for (Task agvTask : transportingTasks) {
            boolean isCompleted = false;
            String reason = "";
            // 检查工作档是否存在
            WrkMast wrkMast = null;
            if (agvTask.getWrkNo() != null) {
                wrkMast = wrkMastService.selectOne(
                    new EntityWrapper<WrkMast>().eq("wrk_no", agvTask.getWrkNo())
                );
            }
            // 检查历史档是否存在
            WrkMastLog wrkMastLog = null;
            if (agvTask.getWrkNo() != null) {
                wrkMastLog = wrkMastLogService.selectOne(
                    new EntityWrapper<WrkMastLog>().eq("wrk_no", agvTask.getWrkNo())
                );
            }
            // 如果通过wrk_no没找到,且有条码,则通过条码查询
            if (wrkMastLog == null && !Cools.isEmpty(agvTask.getBarcode())) {
                List<WrkMastLog> logList = wrkMastLogService.selectList(
                    new EntityWrapper<WrkMastLog>().eq("barcode", agvTask.getBarcode())
                );
                if (!logList.isEmpty()) {
                    wrkMastLog = logList.get(0);
                }
            }
            // 如果工作档存在,检查是否已完成
            if (wrkMast != null) {
                Long wrkSts = wrkMast.getWrkSts();
                Integer ioType = agvTask.getIoType();
                if (wrkSts != null && ioType != null) {
                    // 入库任务:状态4(入库完成)或5(库存更新完成)
                    if ((ioType == 1 || ioType == 10 || ioType == 53 || ioType == 57) &&
                        (wrkSts == 4L || wrkSts == 5L)) {
                        isCompleted = true;
                        reason = String.format("工作档已完成,状态:%d", wrkSts);
                    }
                    // 出库任务:状态14(已出库未确认)或15(出库更新完成)
                    else if ((ioType == 101 || ioType == 110 || ioType == 103 || ioType == 107) &&
                             (wrkSts == 14L || wrkSts == 15L)) {
                        isCompleted = true;
                        reason = String.format("工作档已完成,状态:%d", wrkSts);
                    }
                }
            }
            // 1. 如果工作档进入历史档,立即结束AGV任务(只要历史档存在就结束)
            if (!isCompleted && wrkMastLog != null) {
                isCompleted = true;
                reason = String.format("工作档已转历史档,立即结束AGV任务,历史档状态:%d", wrkMastLog.getWrkSts());
            }
            // 如果已完成,更新AGV任务状态为完成
            if (isCompleted) {
                agvTask.setWrkSts(9L);
                agvTask.setModiTime(now);
                if (taskService.updateById(agvTask)) {
                    // taskId使用工作号(wrk_no),如果工作号为空则使用任务ID
                    String displayTaskId = (agvTask.getWrkNo() != null) ? String.valueOf(agvTask.getWrkNo()) : String.valueOf(agvTask.getId());
                    log.info("{},自动结束AGV任务,taskId:{},wrkNo:{},barcode:{},站点:{},释放站点供新任务使用",
                        reason, displayTaskId, agvTask.getWrkNo(), agvTask.getBarcode(), agvTask.getStaNo());
                    // 转移到历史表(会自动减少站点的in_qty)
                    try {
                        moveTaskToHistory(Collections.singletonList(agvTask));
                        log.info("自动结束AGV任务后已转移到历史表,taskId:{},站点:{}已释放", displayTaskId, agvTask.getStaNo());
                    } catch (Exception e) {
                        log.error("自动结束AGV任务后转移历史表失败,taskId:{}", displayTaskId, e);
                    }
                } else {
                    log.error("自动结束AGV任务失败,更新任务状态失败,taskId:{}",
                        (agvTask.getWrkNo() != null) ? String.valueOf(agvTask.getWrkNo()) : String.valueOf(agvTask.getId()));
                }
            } else {
                // 任务仍然有效,保留在列表中
                // 记录任务仍然有效的原因(用于调试)
                String displayTaskId = (agvTask.getWrkNo() != null) ? String.valueOf(agvTask.getWrkNo()) : String.valueOf(agvTask.getId());
                if (wrkMast == null && wrkMastLog == null) {
                    log.debug("任务ID:{}(站点:{})仍然有效:工作档和历史档都不存在,可能工作档还未创建", displayTaskId, agvTask.getStaNo());
                } else if (wrkMast != null) {
                    log.debug("任务ID:{}(站点:{})仍然有效:工作档状态={},任务类型={},未达到完成条件",
                        displayTaskId, agvTask.getStaNo(), wrkMast.getWrkSts(), agvTask.getIoType());
                }
                validTasks.add(agvTask);
            }
        }
        return validTasks;
    }
    /**
@@ -382,8 +747,10 @@
     */
    private String getRequest(Task task, String nameSpace) {
        JSONObject object = new JSONObject();
        // taskId使用任务ID,格式:T + 任务ID
        object.put("taskId", "T" + task.getId());
        // taskId使用工作号(wrk_no),格式:T + 工作号
        // 如果工作号为空,则使用任务ID作为备选
        String taskIdValue = (task.getWrkNo() != null) ? "T" + task.getWrkNo() : "T" + task.getId();
        object.put("taskId", taskIdValue);
        // fromBin使用源库位编号(sourceLocNo),如果为空则使用源站点编号(sourceStaNo)作为备选
        String fromBin = task.getSourceLocNo();
        if (fromBin == null || fromBin.isEmpty()) {
@@ -428,10 +795,16 @@
    /**
     * 为任务分配站点(定时任务中调用)
     * 注意:只会分配一个站点,找到第一个符合条件的站点就分配并退出
     * @param task 任务对象
     * @return 如果无法分配站点,返回错误信息;如果分配成功,返回null并更新task的staNo
     */
    private String allocateSiteForTask(Task task) {
    @Transactional(rollbackFor = Exception.class)
    public String allocateSiteForTask(Task task) {
        // taskId使用工作号(wrk_no),如果工作号为空则使用任务ID
        String displayTaskId = (task.getWrkNo() != null) ? String.valueOf(task.getWrkNo()) : String.valueOf(task.getId());
        log.debug("开始为任务ID:{}分配站点,任务类型:{},机器人组:{}",
            displayTaskId, task.getIoType(), task.getInvWh());
        // 根据任务的invWh(机器人组)判断是东侧还是西侧
        String robotGroup = task.getInvWh();
        List<String> targetStations;
@@ -449,12 +822,12 @@
            // 默认使用东侧
            targetStations = agvProperties.getEastStations();
            groupKey = "east";
            // log.warn("任务ID:{}的机器人组{}未识别,使用默认东侧站点", task.getId(), robotGroup);
            log.warn("任务ID:{}的机器人组{}未识别,使用默认东侧站点", displayTaskId, robotGroup);
        }
        
        if (targetStations.isEmpty()) {
            String errorMsg = "没有可用的目标站点配置";
            // log.warn("任务ID:{}", errorMsg, task.getId());
            log.warn("任务ID:{},{}", displayTaskId, errorMsg);
            return errorMsg;
        }
        
@@ -463,20 +836,46 @@
                .map(Integer::parseInt)
                .collect(Collectors.toList());
        
        log.info("任务ID:{},{}站点组配置的站点:{},共{}个站点",
            displayTaskId, groupKey.equals("east") ? agvProperties.getEastDisplayName() : agvProperties.getWestDisplayName(),
            targetStations, targetStations.size());
        // 判断能入站点(in_enable="Y"表示能入),排除dev_no=0的无效站点
        List<Integer> sites = basDevpMapper.selectList(
        List<BasDevp> allDevList = basDevpMapper.selectList(
                new EntityWrapper<BasDevp>()
                        .eq("in_enable", "Y")
                        .in("dev_no", siteIntList)
                        .ne("dev_no", 0) // 排除dev_no=0的无效站点
        ).stream()
        );
        // 记录所有站点的状态信息
        StringBuilder siteStatusInfo = new StringBuilder();
        for (BasDevp dev : allDevList) {
            if (siteStatusInfo.length() > 0) {
                siteStatusInfo.append("; ");
            }
            siteStatusInfo.append("站点").append(dev.getDevNo())
                    .append("(in_enable=").append(dev.getInEnable())
                    .append(",canining=").append(dev.getCanining()).append(")");
        }
        log.info("任务ID:{},候选站点状态:{}", displayTaskId, siteStatusInfo.toString());
        List<Integer> sites = allDevList.stream()
                .filter(dev -> "Y".equals(dev.getInEnable()))
                .map(BasDevp::getDevNo)
                .filter(devNo -> devNo != null && devNo != 0) // 再次过滤,确保不为null或0
                .collect(Collectors.toList());
        
        // 检查是否有站点不可用,如果有,说明需要在可用的站点之间平均分配
        List<Integer> unavailableSites = new ArrayList<>(siteIntList);
        unavailableSites.removeAll(sites);
        if (!unavailableSites.isEmpty()) {
            log.info("任务ID:{},{}站点组中有{}个站点不可用(in_enable!='Y'):{},将在{}个可用站点之间平均分配",
                displayTaskId, groupKey.equals("east") ? agvProperties.getEastDisplayName() : agvProperties.getWestDisplayName(),
                unavailableSites.size(), unavailableSites, sites.size());
        }
        if (sites.isEmpty()) {
            String errorMsg = "没有能入站点";
            // log.warn("任务ID:{}", errorMsg, task.getId());
            String errorMsg = "没有能入站点(in_enable='Y')";
            log.warn("任务ID:{},{},候选站点列表:{},站点状态:{}", displayTaskId, errorMsg, targetStations, siteStatusInfo.toString());
            return errorMsg;
        }
        
@@ -486,13 +885,27 @@
                .eq("in_enable", "Y")
                .eq("canining", "Y")
                .ne("dev_no", 0) // 排除dev_no=0的无效站点
        ).stream()
                .filter(dev -> dev.getDevNo() != null && dev.getDevNo() != 0) // 再次过滤,确保不为null或0
                .collect(Collectors.toList());
        );
        
        if (devListWithConfig.isEmpty()) {
            // 站点配置不允许入库(canining != "Y"),暂不分配,等待配置开通(只在定时任务中记录日志)
            // log.warn("任务ID:{}没有可入站点(站点未开通可入允许:canining='Y'),暂不分配站点,等待配置开通", task.getId());
            // 记录每个站点的canining状态
            StringBuilder caniningStatusInfo = new StringBuilder();
            for (Integer siteNo : sites) {
                BasDevp dev = allDevList.stream()
                        .filter(d -> d.getDevNo().equals(siteNo))
                        .findFirst()
                        .orElse(null);
                if (dev != null) {
                    if (caniningStatusInfo.length() > 0) {
                        caniningStatusInfo.append("; ");
                    }
                    caniningStatusInfo.append("站点").append(siteNo)
                            .append("(canining=").append(dev.getCanining()).append(")");
                }
            }
            log.warn("任务ID:{}没有可入站点(站点未开通可入允许:canining='Y'),暂不分配站点,等待配置开通。能入站点列表:{},canining状态:{}",
                    displayTaskId, sites, caniningStatusInfo.toString());
            return null; // 返回null,表示暂不分配,等待配置开通
        }
        
@@ -500,12 +913,14 @@
        List<Integer> configuredSites = devListWithConfig.stream()
                .map(BasDevp::getDevNo)
                .collect(Collectors.toList());
        log.info("任务ID:{},已配置可入站点列表:{}", displayTaskId, configuredSites);
        List<Integer> canInSites = basDevpMapper.getCanInSites(configuredSites);
        if (canInSites.isEmpty()) {
            // 所有已配置可入的站点都有出库任务,暂不分配,等待下次定时任务再尝试(只在定时任务中记录日志)
            // log.warn("任务ID:{}没有可入站点(请等待出库完成),暂不分配站点,等待下次定时任务再尝试", task.getId());
            log.warn("任务ID:{}没有可入站点(请等待出库完成),暂不分配站点,等待下次定时任务再尝试。已配置可入站点列表:{}", displayTaskId, configuredSites);
            return null; // 返回null,表示暂不分配,等待下次定时任务再尝试
        }
        log.info("任务ID:{},没有出库任务的站点列表:{}", displayTaskId, canInSites);
        
        // 寻找入库任务最少的站点(且必须in_enable="Y"能入 和 canining="Y"可入),排除dev_no=0的无效站点
        List<BasDevp> devList = basDevpMapper.selectList(new EntityWrapper<BasDevp>()
@@ -520,9 +935,20 @@
        if (devList.isEmpty()) {
            // 理论上不应该到这里,因为前面已经检查过了,但为了安全起见还是保留
            String errorMsg = "没有可入站点(in_enable='Y'且canining='Y')";
            // log.warn("任务ID:{}", errorMsg, task.getId());
            log.warn("任务ID:{},{},可入站点列表:{}", displayTaskId, errorMsg, canInSites);
            return errorMsg;
        }
        // 记录每个站点的入库任务数
        StringBuilder siteInQtyInfo = new StringBuilder();
        for (BasDevp dev : devList) {
            if (siteInQtyInfo.length() > 0) {
                siteInQtyInfo.append("; ");
            }
            siteInQtyInfo.append("站点").append(dev.getDevNo())
                    .append("(入库任务数=").append(dev.getInQty()).append(")");
        }
        log.info("任务ID:{},可入站点及其入库任务数:{}", displayTaskId, siteInQtyInfo.toString());
        
        // 先按规则排序(入库任务数排序)
        devList.sort(Comparator.comparing(BasDevp::getInQty));
@@ -543,98 +969,123 @@
            }
        }
        
        // 筛选出任务数最少的站点列表(按规则排序后的候选站点)
        int minInQty = devList.get(0).getInQty();
        List<BasDevp> minTaskSites = devList.stream()
                .filter(dev -> dev.getInQty() == minInQty)
        // 先查询agv工作档中未被分配站点的站点
        // 查询agv工作档中所有已分配站点的任务(sta_no不为空、不为空字符串、不为0)
        final List<String> allocatedSiteNos;
        if (checkIoTypes != null && !checkIoTypes.isEmpty()) {
            List<Task> allocatedTasks = taskService.selectList(
                new EntityWrapper<Task>()
                    .eq("task_type", "agv")
                    .in("wrk_sts", 7L, 8L) // 待呼叫AGV和正在搬运的任务
                    .in("io_type", checkIoTypes)
                    .isNotNull("sta_no")
                    .ne("sta_no", "")
                    .ne("sta_no", "0")
                    .andNew("(is_deleted = 0)")
            );
            // 获取已分配的站点编号列表
            allocatedSiteNos = allocatedTasks.stream()
                    .map(Task::getStaNo)
                    .filter(staNo -> staNo != null && !staNo.isEmpty() && !staNo.equals("0"))
                    .distinct()
                    .collect(Collectors.toList());
        } else {
            allocatedSiteNos = new ArrayList<>();
        }
        // 从可用站点中筛选出未被分配的站点
        List<BasDevp> unallocatedSites = devList.stream()
                .filter(dev -> {
                    String staNo = String.valueOf(dev.getDevNo());
                    return !allocatedSiteNos.contains(staNo);
                })
                .collect(Collectors.toList());
        // 只使用未分配站点
        if (unallocatedSites.isEmpty()) {
            // 未分配站点为空:不分配站点
            StringBuilder allocatedSitesInfo = new StringBuilder();
            for (String staNo : allocatedSiteNos) {
                if (allocatedSitesInfo.length() > 0) {
                    allocatedSitesInfo.append("; ");
                }
                allocatedSitesInfo.append("站点").append(staNo).append("已被分配");
            }
            log.warn("任务ID:{},所有可用站点都已被分配,暂不分配站点,等待下次定时任务再尝试。已分配站点:{}",
                displayTaskId, allocatedSitesInfo.length() > 0 ? allocatedSitesInfo.toString() : "无详细信息");
            return null; // 返回null,表示暂不分配,等待下次定时任务再尝试
        }
        // 存在未分配站点:根据配置的分配策略选择具体站点
        // 先按规则排序(入库任务数排序)
        unallocatedSites.sort(Comparator.comparing(BasDevp::getInQty));
        
        // 根据配置选择分配策略,确定优先分配的站点顺序
        List<BasDevp> orderedSites = new ArrayList<>();
        String strategy = agvProperties.getSiteAllocation().getStrategy();
        boolean enableRoundRobin = agvProperties.getSiteAllocation().isEnableRoundRobin();
        
        if (minTaskSites.size() > 1 && enableRoundRobin && "round-robin".equals(strategy)) {
        // 记录是否使用轮询策略,以及轮询计数器(用于在成功分配站点后递增)
        AtomicInteger roundRobinCounter = null;
        int roundRobinStartIndex = 0;
        if (unallocatedSites.size() > 1 && enableRoundRobin && "round-robin".equals(strategy)) {
            // 轮询分配:先按轮询策略排序
            AtomicInteger counter = siteRoundRobinCounters.computeIfAbsent(groupKey, k -> new AtomicInteger(0));
            int startIndex = counter.get() % minTaskSites.size();
            roundRobinCounter = siteRoundRobinCounters.computeIfAbsent(groupKey, k -> new AtomicInteger(0));
            roundRobinStartIndex = roundRobinCounter.get() % unallocatedSites.size(); // 获取当前索引,但不递增(成功分配后再递增)
            // 将轮询选中的站点放在最前面
            orderedSites.addAll(minTaskSites.subList(startIndex, minTaskSites.size()));
            orderedSites.addAll(minTaskSites.subList(0, startIndex));
            // 添加其他站点(任务数更多的)
            orderedSites.addAll(devList.stream()
                    .filter(dev -> dev.getInQty() > minInQty)
                    .collect(Collectors.toList()));
            log.debug("使用轮询分配策略,站点组:{},轮询起始索引:{}", groupKey, startIndex);
        } else if (minTaskSites.size() > 1 && enableRoundRobin && "random".equals(strategy)) {
            // 随机分配:先随机排序任务数最少的站点
            List<BasDevp> shuffledMinSites = new ArrayList<>(minTaskSites);
            Collections.shuffle(shuffledMinSites);
            orderedSites.addAll(shuffledMinSites);
            // 添加其他站点(任务数更多的)
            orderedSites.addAll(devList.stream()
                    .filter(dev -> dev.getInQty() > minInQty)
                    .collect(Collectors.toList()));
            log.debug("使用随机分配策略");
            orderedSites.addAll(unallocatedSites.subList(roundRobinStartIndex, unallocatedSites.size()));
            orderedSites.addAll(unallocatedSites.subList(0, roundRobinStartIndex));
            log.info("任务ID:{},使用轮询分配策略,站点组:{},轮询起始索引:{},候选站点:{}(共{}个未分配站点)",
                displayTaskId, groupKey, roundRobinStartIndex,
                unallocatedSites.stream().map(d -> String.valueOf(d.getDevNo())).collect(Collectors.joining(",")),
                unallocatedSites.size());
        } else if (unallocatedSites.size() > 1 && enableRoundRobin && "random".equals(strategy)) {
            // 随机分配:先随机排序未分配站点
            List<BasDevp> shuffledSites = new ArrayList<>(unallocatedSites);
            Collections.shuffle(shuffledSites);
            orderedSites.addAll(shuffledSites);
            log.info("任务ID:{},使用随机分配策略,候选站点:{}",
                displayTaskId, unallocatedSites.stream().map(d -> String.valueOf(d.getDevNo())).collect(Collectors.joining(",")));
        } else {
            // 默认:按入库任务数排序(已经排序好了)
            orderedSites = devList;
            orderedSites = unallocatedSites;
        }
        
        // 依次检查每个站点是否在搬运,找到第一个空闲站点就分配
        BasDevp selectedSite = null;
        for (BasDevp dev : orderedSites) {
            String staNo = String.valueOf(dev.getDevNo());
            // 如果任务类型不为空,检查该站点是否有正在搬运的同类型任务
            boolean isTransporting = false;
            if (checkIoTypes != null && !checkIoTypes.isEmpty()) {
                List<Task> transportingTasks = taskService.selectList(
                    new EntityWrapper<Task>()
                        .eq("sta_no", staNo)
                        .eq("task_type", "agv")
                        .eq("wrk_sts", 8L) // 只检查正在搬运状态的任务
                        .in("io_type", checkIoTypes)
                );
                isTransporting = !transportingTasks.isEmpty();
                if (isTransporting) {
                    // log.debug("站点{}有{}个正在搬运的{}AGV任务,检查下一个站点",
                    //     staNo, transportingTasks.size(), taskTypeName);
                    continue; // 该站点正在搬运,检查下一个站点
                }
            }
            // 找到第一个空闲站点,分配
            selectedSite = dev;
            // log.info("任务ID:{}按规则应分配到站点{},该站点空闲,分配成功", task.getId(), staNo);
            break;
        }
        // 如果所有站点都在搬运,则不分配站点(只在定时任务中记录日志,不返回错误信息)
        if (selectedSite == null) {
//            log.warn("任务ID:{},暂不分配站点,等待空闲 - 所有候选站点都有正在搬运的{}任务",
//                task.getId(), taskIoType != null && taskIoType < 100 ? "入库" : "出库");
            return null; // 返回null,表示暂不分配,等待下次定时任务再尝试
        }
        // 既然已经筛选出了未分配站点,直接根据分配策略选择第一个站点即可
        // 未分配站点在AGV工作档中都没有已分配的任务,可以直接分配
        BasDevp selectedSite = orderedSites.get(0);
        Integer endSite = selectedSite.getDevNo();
        String staNo = String.valueOf(endSite);
        log.info("任务ID:{},从{}个未分配站点中选择站点{}(入库任务数:{}),候选站点:{}",
            displayTaskId, orderedSites.size(), staNo, selectedSite.getInQty(),
            orderedSites.stream().map(d -> String.valueOf(d.getDevNo())).collect(Collectors.joining(",")));
        // 如果使用轮询策略且成功分配站点,递增轮询计数器(确保下次从下一个站点开始)
        if (roundRobinCounter != null && unallocatedSites.size() > 1) {
            roundRobinCounter.getAndIncrement();
            log.debug("任务ID:{}成功分配到站点{},轮询计数器已递增,下次将从下一个站点开始轮询", displayTaskId, staNo);
        }
        
        // 检查站点是否有效(不能为0或null)
        if (endSite == null || endSite == 0) {
            String errorMsg = String.format("分配的站点无效(dev_no=%s)", endSite);
            // log.error("任务ID:{},{}", task.getId(), errorMsg);
            log.error("任务ID:{},{}", displayTaskId, errorMsg);
            return errorMsg;
        }
        
        // 入库暂存+1
        basDevpMapper.incrementInQty(endSite);
        
        // 更新任务的站点编号
        // 更新任务的站点编号,并确保状态为7(待呼叫AGV)
        task.setStaNo(String.valueOf(endSite));
        if (task.getWrkSts() == null || task.getWrkSts() != 7L) {
            task.setWrkSts(7L); // 确保状态为7(待呼叫AGV)
            log.debug("任务ID:{}分配站点时,状态不是7,已更新为7(待呼叫AGV)", displayTaskId);
        }
        taskService.updateById(task);
        
        // log.info("任务ID:{}已分配站点:{}", task.getId(), endSite);
        log.info("任务ID:{}已分配站点:{},机器人组:{},任务类型:{}", displayTaskId, endSite, robotGroup, taskTypeName);
        return null; // 分配成功,返回null
    }
@@ -643,7 +1094,7 @@
     * @param staNo 站点编号
     * @return 机器人组名称
     */
    private String determineRobotGroupByStation(String staNo) {
        private String determineRobotGroupByStation(String staNo) {
        if (staNo == null || staNo.isEmpty()) {
            return agvProperties.getRobotGroupEast(); // 默认使用东侧机器人组
        }
@@ -684,6 +1135,8 @@
        // 批量更新暂存点状态
        List<String> locOList = new ArrayList<>();
        List<String> locFList = new ArrayList<>();
        // 收集需要减少in_qty的站点(入库任务)
        Set<Integer> sitesToDecrement = new HashSet<>();
        for (Task task : taskList) {
            String sourceStaNo = task.getSourceStaNo();
            String staNo = task.getStaNo();
@@ -691,9 +1144,30 @@
                locOList.add(sourceStaNo);
                locFList.add(staNo);
            } else if (task.getIoType() < 100) {
                // 入库任务:减少目标站点的in_qty
                locOList.add(sourceStaNo);
                if (staNo != null && !staNo.isEmpty()) {
                    try {
                        Integer siteNo = Integer.parseInt(staNo);
                        if (siteNo != null && siteNo > 0) {
                            sitesToDecrement.add(siteNo);
                        }
                    } catch (NumberFormatException e) {
                        log.warn("任务ID:{}的站点编号格式错误:{},跳过减少in_qty", task.getId(), staNo);
                    }
                }
            } else {
                locFList.add(staNo);
            }
        }
        // 减少站点的入库任务数(in_qty)
        for (Integer siteNo : sitesToDecrement) {
            try {
                basDevpMapper.decrementInQty(siteNo);
                log.debug("任务转移到历史表,站点{}的in_qty已减少", siteNo);
            } catch (Exception e) {
                log.error("任务转移到历史表,减少站点{}的in_qty失败", siteNo, e);
            }
        }
@@ -778,7 +1252,10 @@
        // 构造取消任务请求
        JSONObject cancelRequest = new JSONObject();
        cancelRequest.put("taskId", "T" + task.getId());
        // taskId使用工作号(wrk_no),格式:T + 工作号
        // 如果工作号为空,则使用任务ID作为备选
        String taskIdValue = (task.getWrkNo() != null) ? "T" + task.getWrkNo() : "T" + task.getId();
        cancelRequest.put("taskId", taskIdValue);
        cancelRequest.put("kind", kind);
        String body = cancelRequest.toJSONString();
@@ -793,7 +1270,9 @@
            JSONObject jsonObject = JSON.parseObject(response);
            if (jsonObject.getInteger("code") != null && jsonObject.getInteger("code").equals(200)) {
                success = true;
                log.info(namespace + "取消AGV任务成功:{}", task.getId());
                // taskId使用工作号(wrk_no),如果工作号为空则使用任务ID
                String displayTaskId = (task.getWrkNo() != null) ? String.valueOf(task.getWrkNo()) : String.valueOf(task.getId());
                log.info(namespace + "取消AGV任务成功:{}", displayTaskId);
            } else {
                log.error(namespace + "取消AGV任务失败!!!url:{};request:{};response:{}", url, body, response);
            }
src/main/java/com/zy/asrs/task/handler/WorkMastHandler.java
@@ -742,6 +742,7 @@
                .eq("barcode", completedTask.getBarcode())
                .in("io_type", 110, 101) // 空板出库或全板出库
                .eq("wrk_sts", 7) // 待呼叫AGV状态
                .andNew("(is_deleted = 0)")
        );
        if (!existingTasks.isEmpty()) {
            log.info("任务ID:{}的托盘码:{}已存在空托/满托出库任务,跳过生成", completedTask.getId(), completedTask.getBarcode());
@@ -862,6 +863,7 @@
                .eq("task_type", "agv")
                .eq("wrk_sts", 8L) // 只检查正在搬运状态的任务
                .in("io_type", 101, 110) // 出库到缓存区的任务类型
                .andNew("(is_deleted = 0)")
        );
        
        // 如果有正在搬运的任务,且工作档未完成,则不分配缓存库位
@@ -1176,6 +1178,7 @@
                    .eq("task_type", "agv")
                    .eq("wrk_sts", 8L) // 只检查正在搬运状态的任务
                    .in("io_type", checkIoTypes)
                    .andNew("(is_deleted = 0)")
            );
            
            if (!transportingTasks.isEmpty()) {