自动化立体仓库 - WMS系统
chen.lin
4 天以前 10778ff6207c31641187acb487d4b67c0de59b24
agv增加一个新单号防重复  ,增加一个手动呼叫agv
10个文件已修改
705 ■■■■■ 已修改文件
src/main/java/com/zy/asrs/entity/Task.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/TaskLog.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/param/AgvCallParams.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/MobileServiceImpl.java 489 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/config/AdminInterceptor.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/ConfigMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/task/task.js 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/wrkMast/wrkMast.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/task/task.html 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/Task.java
@@ -267,6 +267,7 @@
    @ApiModelProperty(value = "")
    @TableField("error_memo")
    private String errorMemo;
    @ApiModelProperty(value = "")
    @TableField("error_time2")
    private Date errorTime2;
src/main/java/com/zy/asrs/entity/TaskLog.java
@@ -275,11 +275,11 @@
    @TableField("error_memo")
    private String errorMemo;
    @ApiModelProperty(value = "")
    @TableField("error_time2")
    @TableField(value = "error_time2",exist = false)
    private Date errorTime2;
    @ApiModelProperty(value = "")
    @TableField("error_memo2")
    @TableField(value = "error_memo2",exist = false)
    private String errorMemo2;
src/main/java/com/zy/asrs/entity/param/AgvCallParams.java
@@ -20,10 +20,16 @@
    @ApiModelProperty("终点位置")
    private String tarSite;
    @ApiModelProperty("目标库位(入库时使用)")
    private String tarLoc;
    @ApiModelProperty("托盘码")
    private String barcode;
    @ApiModelProperty("操作类型")
    private String type;
    @ApiModelProperty("呼叫类型:manual-手动输入, outbound-起点+出库, inbound-起点+入库")
    private String callType;
}
src/main/java/com/zy/asrs/service/impl/MobileServiceImpl.java
@@ -129,6 +129,9 @@
    @Resource
    private AgvProperties agvProperties;
    @Autowired
    private com.zy.asrs.task.handler.AgvHandler agvHandler;
    /**
     * 站点轮询计数器,用于平均分配站点
     * Key: 站点组标识(如 "east" 或 "west"),Value: 当前轮询索引
@@ -1358,11 +1361,22 @@
     */
    @Override
    public R callAgvMove(AgvCallParams params, Long userId) {
        if (Objects.isNull(params.getTarSite())) {
            throw new CoolException("目标参数不能为空!!");
        }
        if (Objects.isNull(params.getOrgSite())) {
            throw new CoolException("源站点不能为空!!");
        }
        if (Objects.isNull(params.getBarcode())) {
            throw new CoolException("托盘码不能为空!!");
        }
        String callType = params.getCallType();
        if (callType == null || callType.isEmpty()) {
            callType = "manual"; // 默认手动输入
        }
        if ("manual".equals(callType)) {
            // 手动输入:需要目标站点
            if (Objects.isNull(params.getTarSite())) {
                throw new CoolException("手动输入模式下,目标站点不能为空!!");
        }
        LocCache locCache = locCacheService.selectOne(new EntityWrapper<LocCache>()
                .eq("frozen", 0)
@@ -1373,8 +1387,28 @@
        if (Objects.isNull(locCache)) {
            throw new CoolException("请检查目标库位是否闲置中!!");
        }
        generateAgvTask("agv", locCache, params.getOrgSite(), params.getBarcode(), userId);
        } else if ("outbound".equals(callType)) {
            // 起点+出库:自动分配站点和缓存位
            generateOutboundAgvTask(params.getOrgSite(), params.getBarcode(), userId);
        } else if ("inbound".equals(callType)) {
            // 起点+入库:自动分配站点,库位手动输入
            if (Objects.isNull(params.getTarLoc())) {
                throw new CoolException("入库模式下,目标库位不能为空!!");
            }
            LocCache locCache = locCacheService.selectOne(new EntityWrapper<LocCache>()
                    .eq("frozen", 0)
                    .eq("loc_sts", LocStsType.LOC_STS_TYPE_O.type)
                    .eq("loc_no", params.getTarLoc())
                    .orderAsc(Arrays.asList("loc_no"))
                    .last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY"));
            if (Objects.isNull(locCache)) {
                throw new CoolException("请检查目标库位是否闲置中!!");
            }
            generateInboundAgvTask(locCache, params.getOrgSite(), params.getBarcode(), userId);
        } else {
            throw new CoolException("不支持的呼叫类型:" + callType);
        }
        return R.ok();
    }
@@ -1543,11 +1577,12 @@
        task.setWrkNo(workNo)
                .setAgvWrkNo(agvWrkNo) // 设置AGV工作号
                .setIoTime(now)
                .setWrkSts(1L) // 工作状态:11.生成出库ID
                .setWrkSts(7L) // 工作状态:7.待呼叫AGV(与自动创建保持一致,由定时任务分配站点并呼叫)
                .setIoType(1) // 入出库状态: 11.库格移载
                .setTaskType("agv")
                .setIoPri(10D)
                .setLocNo(loc.getLocNo()) // 目标库位
                .setStaNo(null) // 站点分配由定时任务处理(与自动创建保持一致)
                .setFullPlt("Y") // 满板:Y
                .setPicking("N") // 拣料
                .setExitMk("N")// 退出
@@ -1599,4 +1634,448 @@
            throw new CoolException("移转失败,目标库位状态:" + loc.getLocSts$());
        }
    }
    /**
     * 生成出库AGV任务(起点+出库,自动分配站点和缓存位)
     * @param orgSite 源站点
     * @param barcode 托盘码
     * @param userId 用户ID
     */
    @Transactional(rollbackFor = Exception.class)
    public void generateOutboundAgvTask(String orgSite, String barcode, Long userId) {
        // 根据源站点判断是东侧还是西侧,确定缓存区站点和whs_type
        List<String> eastStations = agvProperties.getEastStations();
        List<String> westStations = agvProperties.getWestStations();
        List<String> cacheStations;
        Long targetWhsType;
        String robotGroup;
        String cacheAreaPrefix = agvProperties.getLocationPrefix().getCacheArea();
        boolean isEmptyPallet = false; // 默认满板出库,可根据需要调整
        if (eastStations.contains(orgSite)) {
            // 东侧出库站点,查找东侧WA库位(whs_type=1)
            cacheStations = agvProperties.getEastStations();
            robotGroup = agvProperties.getRobotGroupEast();
            targetWhsType = agvProperties.getWhsTypeMapping().getInboundArea(); // whs_type=1
        } else if (westStations.contains(orgSite)) {
            // 西侧出库站点,查找西侧WA库位(whs_type=2)
            cacheStations = agvProperties.getWestStations();
            robotGroup = agvProperties.getRobotGroupWest();
            targetWhsType = agvProperties.getWhsTypeMapping().getCacheArea(); // whs_type=2
        } else {
            // 默认使用西侧
            cacheStations = agvProperties.getWestStations();
            robotGroup = agvProperties.getRobotGroupWest();
            targetWhsType = agvProperties.getWhsTypeMapping().getCacheArea();
            log.warn("源站点{}不在配置的站点列表中,使用默认西侧配置", orgSite);
        }
        // 自动分配缓存位(使用优先级分配逻辑)
        LocCache locCache = allocateCacheLocationByPriority(targetWhsType, cacheAreaPrefix, isEmptyPallet);
        if (Objects.isNull(locCache)) {
            throw new CoolException("暂无满足需求的缓存位!");
        }
        // 自动分配站点(使用出库到缓存区的分配策略)
        String allocatedSite = allocateCacheStationForOutbound(cacheStations, 101);
        if (allocatedSite == null) {
            throw new CoolException("无法分配缓存区站点,所有站点都在使用中!");
        }
        // 获取工作号
        int workNo = commonService.getWorkNo(WorkNoType.PICK.type);
        // 生成AGV工作号
        String agvWrkNo = AgvUtils.generateAgvWrkNo(workNo);
        // 保存工作档
        Task task = new Task();
        Date now = new Date();
        task.setWrkNo(workNo)
                .setAgvWrkNo(agvWrkNo)
                .setIoTime(now)
                .setWrkSts(7L) // 工作状态:7.待呼叫AGV(站点已分配)
                .setIoType(101) // 入出库状态:101.全板出库
                .setTaskType("agv")
                .setIoPri(10D)
                .setStaNo(allocatedSite) // 站点已自动分配
                .setLocNo(locCache.getLocNo()) // 目标缓存位
                .setFullPlt("Y") // 满板:Y
                .setPicking("N") // 拣料
                .setExitMk("N")// 退出
                .setSourceStaNo(orgSite) // 源站点
                .setSourceLocNo(locCache.getLocNo()) // 源库位(缓存位)
                .setEmptyMk(locCache.getLocSts().equals("D") ? "Y" : "N")// 空板
                .setBarcode(barcode)// 托盘码
                .setLinkMis("N")
                .setInvWh(robotGroup) // 根据源站点设置机器人组
                .setAppeUser(userId)
                .setAppeTime(now)
                .setModiUser(userId)
                .setModiTime(now);
        if (!taskService.insert(task)) {
            throw new CoolException("保存工作档失败");
        }
        // 更新缓存位状态:O(闲置)-> R(出库预约)
        locCache.setLocSts(LocStsType.LOC_STS_TYPE_R.type);
        locCache.setModiUser(userId);
        locCache.setModiTime(now);
        if (!locCacheService.updateById(locCache)) {
            throw new CoolException("更新缓存位状态失败");
        }
    }
    /**
     * 生成入库AGV任务(起点+入库,自动分配站点,库位手动指定)
     * @param locCache 目标库位
     * @param orgSite 源站点
     * @param barcode 托盘码
     * @param userId 用户ID
     */
    @Transactional(rollbackFor = Exception.class)
    public void generateInboundAgvTask(LocCache locCache, String orgSite, String barcode, Long userId) {
        // 判断是否是空托入库:库位状态为"D"(空桶/空栈板)表示空托入库
        boolean isEmptyPallet = LocStsType.LOC_STS_TYPE_D.type.equals(locCache.getLocSts());
        Integer ioType = isEmptyPallet ? 10 : 1; // 10=空托入库,1=实托入库
        // 只有实托入库才需要组托信息
        List<WaitPakin> pakins = null;
        if (!isEmptyPallet) {
            // 实托入库:检查组托信息是否存在
            pakins = waitPakinService.selectList(new EntityWrapper<WaitPakin>().eq("zpallet", barcode));
            if (Objects.isNull(pakins) || pakins.isEmpty()) {
                throw new CoolException("组托信息不存在!!");
            }
        }
        // 获取工作号
        int workNo = commonService.getWorkNo(WorkNoType.PICK.type);
        // 生成AGV工作号
        String agvWrkNo = AgvUtils.generateAgvWrkNo(workNo);
        // 保存工作档
        Task task = new Task();
        Date now = new Date();
        // 根据库位whs_type确定机器人组
        Long whsType = locCache.getWhsType();
        String robotGroup;
        if (whsType != null && whsType.equals(agvProperties.getWhsTypeMapping().getInboundArea())) {
            robotGroup = agvProperties.getRobotGroupEast();
        } else if (whsType != null && whsType.equals(agvProperties.getWhsTypeMapping().getCacheArea())) {
            robotGroup = agvProperties.getRobotGroupWest();
        } else {
            robotGroup = agvProperties.getRobotGroupEast(); // 默认东侧
        }
        task.setWrkNo(workNo)
                .setAgvWrkNo(agvWrkNo) // 设置AGV工作号
                .setIoTime(now)
                .setWrkSts(7L) // 工作状态:7.待呼叫AGV(站点将自动分配)
                .setIoType(ioType) // 入出库状态:10=空托入库,1=实托入库
                .setTaskType("agv")
                .setIoPri(10D)
                .setLocNo(locCache.getLocNo()) // 目标库位
                .setStaNo(null) // 站点将自动分配
                .setFullPlt(isEmptyPallet ? "N" : "Y") // 空托入库设置为N,实托入库设置为Y
                .setPicking("N") // 拣料
                .setExitMk("N")// 退出
                .setSourceStaNo(orgSite) // 源站点
                .setSourceLocNo(orgSite) // 源库位(使用源站点)
                .setEmptyMk(isEmptyPallet ? "Y" : "N")// 空板:空托入库为Y,实托入库为N
                .setBarcode(barcode)// 托盘码
                .setLinkMis("N")
                .setInvWh(robotGroup) // 根据whs_type设置机器人组
                .setAppeUser(userId)
                .setAppeTime(now)
                .setModiUser(userId)
                .setModiTime(now);
        if (!taskService.insert(task)) {
            throw new CoolException("保存工作档失败");
        }
        // 立即分配站点(跟随入库逻辑)
        String errorMsg = agvHandler.allocateSiteForTask(task);
        if (errorMsg != null) {
            throw new CoolException("分配站点失败:" + errorMsg);
        }
        // 重新查询任务以获取分配后的站点
        task = taskService.selectById(task.getId());
        if (task.getStaNo() == null || task.getStaNo().isEmpty() || task.getStaNo().equals("0")) {
            throw new CoolException("站点分配失败,无法继续!");
        }
        // 只有实托入库才需要保存工作档明细
        if (!isEmptyPallet && pakins != null && !pakins.isEmpty()) {
            List<TaskDetl> taskDetls = new ArrayList<>();
            pakins.forEach(pakin -> {
                TaskDetl wrkDetl = new TaskDetl();
                BeanUtils.copyProperties(pakin, wrkDetl);
                wrkDetl.setWrkNo(workNo)
                        .setIoTime(new Date())
                        .setOrderNo(pakin.getOrderNo())
                        .setAnfme(pakin.getAnfme())
                        .setZpallet(pakin.getZpallet())
                        .setBatch(pakin.getBatch())
                        .setMatnr(pakin.getMatnr())
                        .setMaktx(pakin.getMaktx())
                        .setAppeUser(userId)
                        .setUnit(pakin.getUnit())
                        .setModel(pakin.getModel())
                        .setAppeTime(new Date())
                        .setModiUser(userId);
                taskDetls.add(wrkDetl);
            });
            if (!taskDetlService.insertBatch(taskDetls)) {
                throw new CoolException("保存工作档明细失败");
            }
        }
        // 修改目标库位状态:O(闲置)-> S(入库预约)
        if (locCache.getLocSts().equals(LocStsType.LOC_STS_TYPE_O.type)) {
            locCache.setLocSts(LocStsType.LOC_STS_TYPE_S.type);
            locCache.setModiTime(now);
            locCache.setModiUser(userId);
            if (!locCacheService.updateById(locCache)) {
                throw new CoolException("更新目标库位状态失败");
            }
        } else {
            throw new CoolException("移转失败,目标库位状态:" + locCache.getLocSts());
        }
    }
    /**
     * 按优先级分配缓存库位(跟随出库逻辑)
     * @param whsType 库区类型
     * @param cacheAreaPrefix 缓存区库位前缀(如"WA")
     * @param isEmptyPallet 是否空托
     * @return 分配的缓存库位,如果无法分配则返回null
     */
    private LocCache allocateCacheLocationByPriority(Long whsType, String cacheAreaPrefix, boolean isEmptyPallet) {
        // 查询所有符合条件的空库位
        List<LocCache> allLocations = locCacheService.selectList(new EntityWrapper<LocCache>()
                .eq("whs_type", whsType)
                .like("loc_no", cacheAreaPrefix + "%")
                .eq("frozen", 0)
                .eq("loc_sts", LocStsType.LOC_STS_TYPE_O.type) // O.闲置
                .ne("full_plt", isEmptyPallet ? "Y" : "N") // 空托不选满板库位,满托不选空板库位
        );
        if (allLocations == null || allLocations.isEmpty()) {
            return null;
        }
        // 按row1分组
        Map<Integer, List<LocCache>> locationsByRow = allLocations.stream()
                .filter(loc -> loc.getRow1() != null)
                .collect(Collectors.groupingBy(LocCache::getRow1));
        if (locationsByRow.isEmpty()) {
            return null;
        }
        // 优先级1:分配第三列(bay1=3),且该排的1、2、3列都是空的
        for (Map.Entry<Integer, List<LocCache>> entry : locationsByRow.entrySet()) {
            Integer row = entry.getKey();
            List<LocCache> rowLocs = entry.getValue();
            // 检查该排的1、2、3列是否都有空库位
            boolean hasBay1 = rowLocs.stream().anyMatch(loc -> loc.getBay1() != null && loc.getBay1() == 1);
            boolean hasBay2 = rowLocs.stream().anyMatch(loc -> loc.getBay1() != null && loc.getBay1() == 2);
            boolean hasBay3 = rowLocs.stream().anyMatch(loc -> loc.getBay1() != null && loc.getBay1() == 3);
            if (hasBay1 && hasBay2 && hasBay3) {
                // 该排的1、2、3列都是空的,分配第三列
                List<LocCache> bay3Locs = rowLocs.stream()
                        .filter(loc -> loc.getBay1() != null && loc.getBay1() == 3)
                        .sorted(Comparator.comparing(loc -> loc.getLev1() != null ? loc.getLev1() : 0))
                        .collect(Collectors.toList());
                if (!bay3Locs.isEmpty()) {
                    log.debug("优先级1:分配排{}的第三列,库位:{}", row, bay3Locs.get(0).getLocNo());
                    return bay3Locs.get(0);
                }
            }
        }
        // 优先级2:分配第二列(bay1=2),且该排的1、2列都是空的
        for (Map.Entry<Integer, List<LocCache>> entry : locationsByRow.entrySet()) {
            Integer row = entry.getKey();
            List<LocCache> rowLocs = entry.getValue();
            boolean hasBay1 = rowLocs.stream().anyMatch(loc -> loc.getBay1() != null && loc.getBay1() == 1);
            boolean hasBay2 = rowLocs.stream().anyMatch(loc -> loc.getBay1() != null && loc.getBay1() == 2);
            if (hasBay1 && hasBay2) {
                List<LocCache> bay2Locs = rowLocs.stream()
                        .filter(loc -> loc.getBay1() != null && loc.getBay1() == 2)
                        .sorted(Comparator.comparing(loc -> loc.getLev1() != null ? loc.getLev1() : 0))
                        .collect(Collectors.toList());
                if (!bay2Locs.isEmpty()) {
                    log.debug("优先级2:分配排{}的第二列,库位:{}", row, bay2Locs.get(0).getLocNo());
                    return bay2Locs.get(0);
                }
            }
        }
        // 优先级3:分配第一列(bay1=1)
        for (Map.Entry<Integer, List<LocCache>> entry : locationsByRow.entrySet()) {
            Integer row = entry.getKey();
            List<LocCache> rowLocs = entry.getValue();
            List<LocCache> bay1Locs = rowLocs.stream()
                    .filter(loc -> loc.getBay1() != null && loc.getBay1() == 1)
                    .sorted(Comparator.comparing(loc -> loc.getLev1() != null ? loc.getLev1() : 0))
                    .collect(Collectors.toList());
            if (!bay1Locs.isEmpty()) {
                log.debug("优先级3:分配排{}的第一列,库位:{}", row, bay1Locs.get(0).getLocNo());
                return bay1Locs.get(0);
            }
        }
        // 优先级4:分配第二列(不要求该排的1、2列都是空的)
        for (Map.Entry<Integer, List<LocCache>> entry : locationsByRow.entrySet()) {
            Integer row = entry.getKey();
            List<LocCache> rowLocs = entry.getValue();
            List<LocCache> bay2Locs = rowLocs.stream()
                    .filter(loc -> loc.getBay1() != null && loc.getBay1() == 2)
                    .sorted(Comparator.comparing(loc -> loc.getLev1() != null ? loc.getLev1() : 0))
                    .collect(Collectors.toList());
            if (!bay2Locs.isEmpty()) {
                log.debug("优先级4:分配排{}的第二列,库位:{}", row, bay2Locs.get(0).getLocNo());
                return bay2Locs.get(0);
            }
        }
        // 优先级5:分配第三列(不要求该排的1、2、3列都是空的)
        for (Map.Entry<Integer, List<LocCache>> entry : locationsByRow.entrySet()) {
            Integer row = entry.getKey();
            List<LocCache> rowLocs = entry.getValue();
            List<LocCache> bay3Locs = rowLocs.stream()
                    .filter(loc -> loc.getBay1() != null && loc.getBay1() == 3)
                    .sorted(Comparator.comparing(loc -> loc.getLev1() != null ? loc.getLev1() : 0))
                    .collect(Collectors.toList());
            if (!bay3Locs.isEmpty()) {
                log.debug("优先级5:分配排{}的第三列,库位:{}", row, bay3Locs.get(0).getLocNo());
                return bay3Locs.get(0);
            }
        }
        return null;
    }
    /**
     * 为出库到缓存区的任务分配站点(跟随出库逻辑)
     * @param cacheStations 缓存区站点列表
     * @param ioType 任务类型(101=全板出库,110=空板出库)
     * @return 分配的站点编号,如果无法分配则返回null
     */
    private String allocateCacheStationForOutbound(List<String> cacheStations, Integer ioType) {
        if (cacheStations == null || cacheStations.isEmpty()) {
            log.warn("缓存区站点列表为空,无法分配站点");
            return null;
        }
        // 将站点字符串列表转换为整数列表
        List<Integer> siteIntList = cacheStations.stream()
                .map(Integer::parseInt)
                .collect(Collectors.toList());
        // 查询所有缓存区站点的设备信息(包含任务数)
        List<BasDevp> devList = basDevpMapper.selectList(new EntityWrapper<BasDevp>()
                .in("dev_no", siteIntList)
        );
        if (devList.isEmpty()) {
            log.warn("缓存区站点{}在设备表中不存在", cacheStations);
            return cacheStations.get(0); // 降级:返回第一个站点
        }
        // 按入库任务数排序(出库到缓存区也使用in_qty字段)
        devList.sort(Comparator.comparing(BasDevp::getInQty));
        // 获取最少任务数
        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();
        List<BasDevp> orderedSites = new ArrayList<>();
        String groupKey = "west"; // 缓存区使用西侧
        if (minTaskSites.size() > 1 && enableRoundRobin && "round-robin".equals(strategy)) {
            // 轮询分配
            AtomicInteger counter = siteRoundRobinCounters.computeIfAbsent(groupKey, k -> new AtomicInteger(0));
            int startIndex = counter.get() % minTaskSites.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()));
            counter.getAndIncrement();
            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("使用随机分配策略");
        } else {
            // 默认:按入库任务数排序
            orderedSites = devList;
        }
        // 依次检查每个站点是否在搬运,找到第一个空闲站点就分配
        String selectedSite = null;
        List<Integer> checkIoTypes = Arrays.asList(101, 110); // 出库到缓存区的任务类型
        for (BasDevp dev : orderedSites) {
            String staNo = String.valueOf(dev.getDevNo());
            // 检查该站点是否有正在搬运的同类型任务
            List<Task> transportingTasks = taskService.selectList(
                new EntityWrapper<Task>()
                    .eq("sta_no", staNo)
                    .eq("task_type", "agv")
                    .eq("wrk_sts", 8L) // 只检查正在搬运状态的任务
                    .in("io_type", checkIoTypes)
                    .eq("is_deleted", 0)
            );
            if (!transportingTasks.isEmpty()) {
                log.debug("缓存区站点{}有{}个正在搬运的出库AGV任务,检查下一个站点",
                    staNo, transportingTasks.size());
                continue; // 该站点正在搬运,检查下一个站点
            }
            // 找到第一个空闲站点,分配
            selectedSite = staNo;
            log.info("出库到缓存区任务按规则应分配到站点{},该站点空闲,分配成功", staNo);
            break;
        }
        // 如果所有站点都在搬运,则不分配站点
        if (selectedSite == null) {
            return null;
        }
        // 更新站点任务数(出库到缓存区也使用in_qty字段)
        basDevpMapper.incrementInQty(Integer.parseInt(selectedSite));
        return selectedSite;
    }
}
src/main/java/com/zy/asrs/service/impl/OpenServiceImpl.java
@@ -151,7 +151,20 @@
                    }
                    break;
                case "Failed":
                    // Cancelled=取消,取消订单
                    log.warn("AGV订单状态为Failed(失败),失败订单 - 任务ID:{}", taskId);
                    task.setWrkSts(10L);
                    task.setErrorTime(new Date());
                    task.setErrorMemo("回调-AGV订单状态为Failed(失败)");
                    taskService.updateById(task);
                    break;
                case "Cancelled":
                    // Cancelled=取消,取消订单
                    log.warn("AGV订单状态为Cancelled(取消),取消订单 - 任务ID:{}", taskId);
                    task.setWrkSts(10L);
                    task.setErrorTime(new Date());
                    task.setErrorMemo("回调-AGV订单状态为Cancelled(取消)");
                    taskService.updateById(task);
                default:
                    break;
            }
src/main/java/com/zy/common/config/AdminInterceptor.java
@@ -128,8 +128,8 @@
            User user = userService.selectById(userLogin.getUserId());
//            String deToken = Cools.deTokn(token, user.getPassword());
//            long timestamp = Long.parseLong(deToken.substring(0, 13));
            // 15分钟后过期
            if (System.currentTimeMillis() - userLogin.getCreateTime().getTime() > 900000) {
            // 60分钟后过期
            if (System.currentTimeMillis() - userLogin.getCreateTime().getTime() > 3600000) {
                Http.response(response, BaseRes.DENIED);
                return false;
            }
src/main/resources/mapper/ConfigMapper.xml
@@ -17,6 +17,7 @@
        select top 1 * from sys_config
        where 1=1
        and code=#{code}
        and status=1
    </select>
</mapper>
src/main/webapp/static/js/task/task.js
@@ -23,6 +23,7 @@
        cols: [[
            {type: 'checkbox'}
            , {field: 'wrkNo', align: 'center', title: '工作号', sort: true, width: 105}
            , {field: 'agvWrkNo', align: 'center', title: 'AGV工作号', width: 180}
            , {field: 'ioTime$', align: 'center', title: '工作时间', width: 160}
            , {field: 'wrkSts$', align: 'center', title: '工作状态', width: 150}
            , {field: 'ioType$', align: 'center', title: '入出库类型', width: 150}
@@ -221,10 +222,6 @@
                }, function () {
                });
                break;
            //  呼叫AGV
            case 'callAgv':
                callAgvMove(data);
                break;
        }
    });
@@ -322,56 +319,111 @@
    layDateRender();
    // 呼叫AGV搬运
    function callAgvMove(data) {
        var defaultOrgSite = data.sourceStaNo || '';
        var defaultTarSite = data.staNo || '';
        var defaultBarcode = data.barcode || '';
    // 呼叫AGV功能 - 暂时注释掉
    /*
    // 初始化呼叫AGV表单渲染
    form.render('select');
        
        layer.open({
            type: 1,
            title: '呼叫AGV搬运 - 工作号:' + data.wrkNo,
            area: ['450px', '400px'],
            shadeClose: true,
            content: '<form class="layui-form" style="padding: 20px;">' +
                     '<div class="layui-form-item">' +
                     '<label class="layui-form-label"><span style="color: red;">*</span>源站点:</label>' +
                     '<div class="layui-input-block">' +
                     '<input type="text" name="orgSite" value="' + defaultOrgSite + '" placeholder="请输入源站点" class="layui-input" lay-verify="required">' +
                     '</div></div>' +
                     '<div class="layui-form-item">' +
                     '<label class="layui-form-label"><span style="color: red;">*</span>目标站点:</label>' +
                     '<div class="layui-input-block">' +
                     '<input type="text" name="tarSite" value="' + defaultTarSite + '" placeholder="请输入目标站点" class="layui-input" lay-verify="required">' +
                     '</div></div>' +
                     '<div class="layui-form-item">' +
                     '<label class="layui-form-label"><span style="color: red;">*</span>托盘码:</label>' +
                     '<div class="layui-input-block">' +
                     '<input type="text" name="barcode" value="' + defaultBarcode + '" placeholder="请输入托盘码" class="layui-input" lay-verify="required">' +
                     '</div></div>' +
                     '<div class="layui-form-item" style="text-align: center; margin-top: 30px;">' +
                     '<button class="layui-btn" lay-submit lay-filter="callAgvSubmit">确认呼叫</button>' +
                     '<button type="button" class="layui-btn layui-btn-primary" onclick="layer.closeAll()">取消</button>' +
                     '</div></form>',
            success: function(layero, index) {
                form.render();
                form.on('submit(callAgvSubmit)', function(formData) {
    // 呼叫类型切换事件
    form.on('select(callAgvType)', function(data) {
        var callType = data.value;
        if (callType === 'manual') {
            // 手动输入:显示目标站点
            $('#callAgvTarSiteGroup').show();
            $('#callAgvTarLocGroup').hide();
            $('#callAgvTarSite').attr('placeholder', '目标站点');
        } else if (callType === 'outbound') {
            // 出库:隐藏目标站点和库位(自动分配)
            $('#callAgvTarSiteGroup').hide();
            $('#callAgvTarLocGroup').hide();
        } else if (callType === 'inbound') {
            // 入库:显示目标库位,隐藏目标站点(站点自动分配)
            $('#callAgvTarSiteGroup').hide();
            $('#callAgvTarLocGroup').show();
            $('#callAgvTarLoc').attr('placeholder', '目标库位');
        }
    });
    // 呼叫AGV搬运 - 从页面上方表单获取输入
    $('#callAgvBtn').on('click', function() {
        var callType = $('#callAgvType').val();
        var orgSite = $('#callAgvOrgSite').val();
        var tarSite = $('#callAgvTarSite').val();
        var tarLoc = $('#callAgvTarLoc').val();
        var barcode = $('#callAgvBarcode').val();
        // 验证必填项
        if (!orgSite || orgSite.trim() === '') {
            layer.msg('请输入源站点', {icon: 2});
            $('#callAgvOrgSite').focus();
            return;
        }
        if (callType === 'manual') {
            // 手动输入:需要目标站点
            if (!tarSite || tarSite.trim() === '') {
                layer.msg('请输入目标站点', {icon: 2});
                $('#callAgvTarSite').focus();
                return;
            }
        } else if (callType === 'inbound') {
            // 入库:需要目标库位
            if (!tarLoc || tarLoc.trim() === '') {
                layer.msg('请输入目标库位', {icon: 2});
                $('#callAgvTarLoc').focus();
                return;
            }
        }
        if (!barcode || barcode.trim() === '') {
            layer.msg('请输入托盘码', {icon: 2});
            $('#callAgvBarcode').focus();
            return;
        }
        // 构建确认信息
        var confirmMsg = '确认呼叫AGV搬运?<br/>呼叫类型:';
        if (callType === 'manual') {
            confirmMsg += '手动输入<br/>源站点:' + orgSite + '<br/>目标站点:' + tarSite + '<br/>托盘码:' + barcode;
        } else if (callType === 'outbound') {
            confirmMsg += '起点+出库(站点和缓存位自动分配)<br/>源站点:' + orgSite + '<br/>托盘码:' + barcode;
        } else if (callType === 'inbound') {
            confirmMsg += '起点+入库(站点自动分配)<br/>源站点:' + orgSite + '<br/>目标库位:' + tarLoc + '<br/>托盘码:' + barcode;
        }
        layer.confirm(confirmMsg, {
            title: '呼叫AGV确认',
            icon: 3,
            shadeClose: true
        }, function(index) {
                    var loadIndex = layer.load(2);
            var requestData = {
                callType: callType,
                orgSite: orgSite.trim(),
                barcode: barcode.trim()
            };
            if (callType === 'manual' && tarSite) {
                requestData.tarSite = tarSite.trim();
            }
            if (callType === 'inbound' && tarLoc) {
                requestData.tarLoc = tarLoc.trim();
            }
                    $.ajax({
                        url: baseUrl + "/mobile/cache/agv/call",
                        headers: {'token': localStorage.getItem('token')},
                        data: JSON.stringify({
                            orgSite: formData.field.orgSite,
                            tarSite: formData.field.tarSite,
                            barcode: formData.field.barcode
                        }),
                data: JSON.stringify(requestData),
                        contentType: 'application/json;charset=UTF-8',
                        method: 'POST',
                        success: function(res) {
                            layer.close(loadIndex);
                            if (res.code === 200) {
                                layer.close(index);
                    if (res.code === 200) {
                                layer.msg('呼叫AGV成功', {icon: 1});
                        // 清空表单
                        $('#callAgvOrgSite').val('');
                        $('#callAgvTarSite').val('');
                        $('#callAgvTarLoc').val('');
                        $('#callAgvBarcode').val('');
                        // 刷新表格
                                tableReload();
                            } else if (res.code === 403) {
                                top.location.href = baseUrl + "/";
@@ -381,14 +433,13 @@
                        },
                        error: function() {
                            layer.close(loadIndex);
                    layer.close(index);
                            layer.msg('网络请求失败', {icon: 2});
                        }
                    });
                    return false;
                });
            }
        });
    }
    */
});
src/main/webapp/static/js/wrkMast/wrkMast.js
@@ -21,6 +21,7 @@
        cols: [[
            {type: 'checkbox'}
            ,{field: 'wrkNo', align: 'center',title: '工作号',sort: true, width: 95}
            ,{field: 'agvWrkNo', align: 'center',title: 'AGV工作号', width: 180}
            ,{field: 'ioTime$', align: 'center',title: '工作时间',sort: true, width: 170}
            ,{field: 'wrkSts$', align: 'center',title: '工作状态', width: 120}
            ,{field: 'ioType$', align: 'center',title: '入出库类型', width: 140}
src/main/webapp/views/task/task.html
@@ -43,6 +43,46 @@
                        </button>
                    </div>
                </div>
                <!-- 呼叫AGV区域 - 暂时注释掉 -->
                <!--
                <div class="layui-form-item" style="margin-top: 10px; padding: 10px; background-color: #f5f5f5; border-radius: 4px;">
                    <div class="layui-inline">
                        <label class="layui-form-label" style="width: 80px;">呼叫AGV:</label>
                        <div class="layui-input-inline" style="width: 120px;">
                            <select id="callAgvType" lay-filter="callAgvType">
                                <option value="manual">手动输入</option>
                                <option value="outbound">起点+出库</option>
                                <option value="inbound">起点+入库</option>
                            </select>
                        </div>
                    </div>
                    <div class="layui-inline">
                        <div class="layui-input-inline" style="width: 150px;">
                            <input class="layui-input" type="text" id="callAgvOrgSite" placeholder="源站点" autocomplete="off">
                        </div>
                    </div>
                    <div class="layui-inline" id="callAgvTarSiteGroup">
                        <div class="layui-input-inline" style="width: 150px;">
                            <input class="layui-input" type="text" id="callAgvTarSite" placeholder="目标站点" autocomplete="off">
                        </div>
                    </div>
                    <div class="layui-inline" id="callAgvTarLocGroup" style="display: none;">
                        <div class="layui-input-inline" style="width: 150px;">
                            <input class="layui-input" type="text" id="callAgvTarLoc" placeholder="目标库位" autocomplete="off">
                        </div>
                    </div>
                    <div class="layui-inline">
                        <div class="layui-input-inline" style="width: 150px;">
                            <input class="layui-input" type="text" id="callAgvBarcode" placeholder="托盘码" autocomplete="off">
                        </div>
                    </div>
                    <div class="layui-inline">&emsp;
                        <button class="layui-btn layui-btn-normal" id="callAgvBtn">
                            <i class="layui-icon">&#xe608;</i>呼叫AGV
                        </button>
                    </div>
                </div>
                -->
            </div>
            <table class="layui-hide" id="task" lay-filter="task"></table>
        </div>
@@ -64,9 +104,6 @@
    {{# } }}
    {{#if (d.ioType === 107) { }}
    <a class="layui-btn layui-btn-warm layui-btn-xs btn-pick" lay-event="pick">盘</a>
    {{# } }}
    {{#if (d.taskType === 'agv') { }}
    <a class="layui-btn layui-btn-normal layui-btn-xs btn-callAgv" lay-event="callAgv">呼叫AGV</a>
    {{# } }}
</script>