自动化立体仓库 - WMS系统
chen.llin
2 天以前 2816415f539ef54839e331657edae7055c243ad8
agv缓存库位清空和标记功能
14个文件已修改
1306 ■■■■■ 已修改文件
src/main/java/com/zy/asrs/controller/AppVersionController.java 112 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/LocCacheController.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/entity/LocCache.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/enums/LocStsType.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/LocCacheService.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/LocCacheServiceImpl.java 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/service/impl/MobileServiceImpl.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/handler/AgvHandler.java 275 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/task/handler/WorkMastHandler.java 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/common/properties/AgvProperties.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/locCache/locCache.js 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/role/rolePower.js 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/locCache/locCache.html 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/asrs/controller/AppVersionController.java
@@ -233,19 +233,107 @@
    @RequestMapping("/menu/pda/auth")
    @ManagerAuth
    public R menuPda(){
        Long userId = getUserId();
        List<RolePermission> rolePermissions;
        if (userId == 9527L) {
            rolePermissions = rolePermissionService.selectList(new EntityWrapper<>());
        } else {
            Long roleId = getUser().getRoleId();
            rolePermissions = rolePermissionService.selectList(new EntityWrapper<RolePermission>().eq("role_id", roleId));
        // 直接从 sys_permission 表读取所有菜单,不检查权限分配
        // 查询所有状态为1(正常)的权限
        List<Permission> permissions = permissionService.selectList(
            new EntityWrapper<Permission>()
                .eq("status", 1)  // 只返回正常状态的权限
        );
        // 检查是否有层级结构(父菜单:action为空字符串,子菜单:resource_id指向父菜单的permission.id)
        // 查询所有父菜单(action为空字符串的权限)
        List<Permission> parentMenus = permissions.stream()
            .filter(p -> p.getAction() == null || p.getAction().isEmpty())
            .collect(Collectors.toList());
        // 收集所有父菜单ID(用于过滤独立菜单)
        java.util.Set<Long> parentMenuIds = parentMenus.stream()
            .map(Permission::getId)
            .collect(Collectors.toSet());
        if (!parentMenus.isEmpty()) {
            // 构建层级结构
            List<Map<String, Object>> result = new ArrayList<>();
            for (Permission parentMenu : parentMenus) {
                // 查找该父菜单下的子菜单
                // 方式1:resource_id指向父菜单的id
                // 方式2:resource_id为0或null,但根据action路径匹配父菜单
                List<Permission> children = permissions.stream()
                    .filter(p -> {
                        // 排除父菜单本身
                        if (p.getAction() == null || p.getAction().isEmpty()) {
                            return false;
                        }
                        // 方式1:resource_id指向父菜单的id
                        if (p.getResourceId() != null && p.getResourceId().equals(parentMenu.getId())) {
                            return true;
                        }
                        // 方式2:resource_id为0或null,根据action路径匹配
                        if ((p.getResourceId() == null || p.getResourceId() == 0)) {
                            String action = p.getAction();
                            String parentName = parentMenu.getName();
                            // 根据父菜单名称匹配action路径
                            if ("入库管理".equals(parentName)) {
                                return action != null && (action.startsWith("/pakin/") || action.startsWith("/order/"));
                            } else if ("AGV管理".equals(parentName)) {
                                return action != null && action.startsWith("/AGV/");
                            } else if ("库存管理".equals(parentName)) {
                                return action != null && action.startsWith("/stock/");
                            }
                        }
                        return false;
                    })
                    .collect(Collectors.toList());
                // 只有有子菜单的父菜单才返回
                if (!children.isEmpty()) {
                    Map<String, Object> parentMap = new HashMap<>();
                    parentMap.put("id", parentMenu.getId());
                    parentMap.put("name", parentMenu.getName());
                    parentMap.put("action", parentMenu.getAction());
                    parentMap.put("type", "parent"); // 标识为父菜单
                    // 构建子菜单列表
                    List<Map<String, Object>> childrenList = new ArrayList<>();
                    for (Permission child : children) {
                        Map<String, Object> childMap = new HashMap<>();
                        childMap.put("id", child.getId());
                        childMap.put("name", child.getName());
                        childMap.put("action", child.getAction());
                        childMap.put("type", "child"); // 标识为子菜单
                        childrenList.add(childMap);
                    }
                    parentMap.put("children", childrenList);
                    result.add(parentMap);
                }
            }
            // 添加没有父菜单的独立权限(resource_id为NULL的权限)
            List<Permission> standalonePermissions = permissions.stream()
                .filter(p -> {
                    // 只返回有action的权限(排除父菜单)
                    if (p.getAction() == null || p.getAction().isEmpty()) {
                        return false;
                    }
                    // 如果resource_id为NULL,说明是独立菜单
                    return p.getResourceId() == null;
                })
                .collect(Collectors.toList());
            for (Permission permission : standalonePermissions) {
                Map<String, Object> item = new HashMap<>();
                item.put("id", permission.getId());
                item.put("name", permission.getName());
                item.put("action", permission.getAction());
                item.put("type", "standalone"); // 独立菜单
                result.add(item);
            }
            return R.ok().add(result);
        }
        if (Cools.isEmpty(rolePermissions)) {
            return R.ok();
        }
        List<Long> collect = rolePermissions.stream().map(RolePermission::getPermissionId).distinct().collect(Collectors.toList());
        List<Permission> permissions = permissionService.selectBatchIds(collect);
        // 如果没有层级结构,返回原来的平铺结构(兼容旧逻辑)
        return R.ok().add(permissions);
    }
}
src/main/java/com/zy/asrs/controller/LocCacheController.java
@@ -46,19 +46,22 @@
        if (!Cools.isEmpty(orderByField)) {
            wrapper.orderBy(humpToLine(orderByField), "asc".equals(orderByType));
        }
        wrapper.eq("full_plt", "N");
        // 移除 full_plt = "N" 的限制,允许查询所有状态的库位(包括满托和空托)
        // 如果需要在后端管理页面过滤,可以在前端进行过滤
        return R.ok(locCacheService.selectPage(new Page<>(curr, limit), wrapper));
    }
    private <T> void convert(Map<String, Object> map, EntityWrapper<T> wrapper) {
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String val = String.valueOf(entry.getValue());
            // 将驼峰命名转换为下划线命名(如 locNo -> loc_no)
            String columnName = humpToLine(entry.getKey());
            if (val.contains(RANGE_TIME_LINK)) {
                String[] dates = val.split(RANGE_TIME_LINK);
                wrapper.ge(entry.getKey(), DateUtils.convert(dates[0]));
                wrapper.le(entry.getKey(), DateUtils.convert(dates[1]));
                wrapper.ge(columnName, DateUtils.convert(dates[0]));
                wrapper.le(columnName, DateUtils.convert(dates[1]));
            } else {
                wrapper.like(entry.getKey(), val);
                wrapper.like(columnName, val);
            }
        }
    }
@@ -136,4 +139,80 @@
        return locCacheService.initLocCache(param, getUserId());
    }
    /**
     * 锁定/解锁缓存库位
     * @param locNo 库位号
     * @param lock 是否锁定,true-锁定,false-解锁
     * @param fullPlt 是否满托,true-满托(设置为F),false-空托(设置为D),解锁时可为空
     * @return 操作结果
     */
    @RequestMapping(value = "/locCache/lockOrUnlock/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R lockOrUnlock(@RequestParam String locNo, @RequestParam Boolean lock, @RequestParam(required = false) Boolean fullPlt) {
        if (Cools.isEmpty(locNo)) {
            return R.error("库位号不能为空");
        }
        if (lock == null) {
            return R.error("锁定参数不能为空");
        }
        return locCacheService.lockOrUnlockLocCache(locNo, lock, fullPlt, getUserId());
    }
    /**
     * 锁定/解锁当前排的所有库位
     * @param locNo 库位号(用于获取排号)
     * @param lock 是否锁定,true-锁定,false-解锁
     * @param fullPlt 是否满托,true-满托(设置为F),false-空托(设置为D),解锁时可为空
     * @return 操作结果
     */
    @RequestMapping(value = "/locCache/lockOrUnlockRow/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R lockOrUnlockRow(@RequestParam String locNo, @RequestParam Boolean lock, @RequestParam(required = false) Boolean fullPlt) {
        if (Cools.isEmpty(locNo)) {
            return R.error("库位号不能为空");
        }
        if (lock == null) {
            return R.error("锁定参数不能为空");
        }
        return locCacheService.lockOrUnlockRowLocCache(locNo, lock, fullPlt, getUserId());
    }
    /**
     * 锁定/解锁当前列的所有库位
     * @param locNo 库位号(用于获取列号bay1)
     * @param lock 是否锁定,true-锁定,false-解锁
     * @param fullPlt 是否满托,true-满托(设置为F),false-空托(设置为D),解锁时可为空
     * @return 操作结果
     */
    @RequestMapping(value = "/locCache/lockOrUnlockBay/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R lockOrUnlockBay(@RequestParam String locNo, @RequestParam Boolean lock, @RequestParam(required = false) Boolean fullPlt) {
        if (Cools.isEmpty(locNo)) {
            return R.error("库位号不能为空");
        }
        if (lock == null) {
            return R.error("锁定参数不能为空");
        }
        return locCacheService.lockOrUnlockBayLocCache(locNo, lock, fullPlt, getUserId());
    }
    /**
     * 清空整排的所有库位(所有列)
     * @param locNo 库位号(用于获取排号row1)
     * @param lock 是否锁定,true-锁定,false-解锁(清空)
     * @param fullPlt 是否满托,true-满托(设置为F),false-空托(设置为D),解锁时可为空
     * @return 操作结果
     */
    @RequestMapping(value = "/locCache/clearAllColumnsInRow/auth", method = RequestMethod.POST)
    @ManagerAuth
    public R clearAllColumnsInRow(@RequestParam String locNo, @RequestParam Boolean lock, @RequestParam(required = false) Boolean fullPlt) {
        if (Cools.isEmpty(locNo)) {
            return R.error("库位号不能为空");
        }
        if (lock == null) {
            return R.error("锁定参数不能为空");
        }
        return locCacheService.clearAllColumnsInRow(locNo, lock, fullPlt, getUserId());
    }
}
src/main/java/com/zy/asrs/entity/LocCache.java
@@ -211,18 +211,27 @@
    }
    public String getLocSts$() {
        if (this.locSts.equals(LocStsType.LOC_STS_TYPE_F.type)) {
            return LocStsType.LOC_STS_TYPE_F.desc;
        } else if (locSts.equals(LocStsType.LOC_STS_TYPE_D.type)) {
        if (this.locSts == null) {
            return null;
        }
        if (this.locSts.equals(LocStsType.LOC_STS_TYPE_D.type)) {
            return LocStsType.LOC_STS_TYPE_D.desc;
        } else if (locSts.equals(LocStsType.LOC_STS_TYPE_O.type)) {
        } else if (locSts.equals(LocStsType.LOC_STS_TYPE_F.type)) {
            return LocStsType.LOC_STS_TYPE_F.desc;
        } else if (locSts.equals(LocStsType.LOC_STS_TYPE_O.type)) {
            return LocStsType.LOC_STS_TYPE_O.desc;
        } else if (locSts.equals(LocStsType.LOC_STS_TYPE_P.type)) {
            return LocStsType.LOC_STS_TYPE_P.desc;
        } else if (locSts.equals(LocStsType.LOC_STS_TYPE_Q.type)) {
            return LocStsType.LOC_STS_TYPE_Q.desc;
        } else if (locSts.equals(LocStsType.LOC_STS_TYPE_R.type)) {
            return LocStsType.LOC_STS_TYPE_R.desc;
        } else if (locSts.equals(LocStsType.LOC_STS_TYPE_S.type)) {
            return LocStsType.LOC_STS_TYPE_S.desc;
        } else if (locSts.equals(LocStsType.LOC_STS_TYPE_X.type)) {
            return LocStsType.LOC_STS_TYPE_X.desc;
        } else if (locSts.equals(LocStsType.LOC_STS_TYPE_Y.type)) {
            return LocStsType.LOC_STS_TYPE_Y.desc;
        } else {
            return null;
        }
src/main/java/com/zy/asrs/enums/LocStsType.java
@@ -2,18 +2,24 @@
public enum LocStsType {
    //空板
    LOC_STS_TYPE_D("D", "空板"),
    //空桶/空栈板
    LOC_STS_TYPE_D("D", "空桶/空栈板"),
    //在库
    LOC_STS_TYPE_F("F", "在库"),
    //空库
    LOC_STS_TYPE_O("O", "空库"),
    //禁用
    LOC_STS_TYPE_X("X", "禁用"),
    //入库预约
    LOC_STS_TYPE_S("S", "入库预约"),
    //空库位
    LOC_STS_TYPE_O("O", "空库位"),
    //拣料/盘点/并板出库中
    LOC_STS_TYPE_P("P", "拣料/盘点/并板出库中"),
    //拣料/盘点/并板再入库
    LOC_STS_TYPE_Q("Q", "拣料/盘点/并板再入库"),
    //出库预约
    LOC_STS_TYPE_R("R", "出库预约"),
    //入库预约
    LOC_STS_TYPE_S("S", "入库预约"),
    //禁用
    LOC_STS_TYPE_X("X", "禁用"),
    //被合并
    LOC_STS_TYPE_Y("Y", "被合并"),
    ;
    public String type;
src/main/java/com/zy/asrs/service/LocCacheService.java
@@ -15,4 +15,44 @@
     * @version 1.0
     */
    R initLocCache(LocMastInitParam param, Long userId);
    /**
     * 锁定/解锁缓存库位
     * @param locNo 库位号
     * @param lock 是否锁定,true-锁定,false-解锁
     * @param fullPlt 是否满托,true-满托(设置为F),false-空托(设置为D),解锁时忽略此参数
     * @param userId 用户ID
     * @return 操作结果
     */
    R lockOrUnlockLocCache(String locNo, Boolean lock, Boolean fullPlt, Long userId);
    /**
     * 锁定/解锁当前排的所有库位
     * @param locNo 库位号(用于获取排号)
     * @param lock 是否锁定,true-锁定,false-解锁
     * @param fullPlt 是否满托,true-满托(设置为F),false-空托(设置为D),解锁时忽略此参数
     * @param userId 用户ID
     * @return 操作结果
     */
    R lockOrUnlockRowLocCache(String locNo, Boolean lock, Boolean fullPlt, Long userId);
    /**
     * 锁定/解锁当前列的所有库位
     * @param locNo 库位号(用于获取列号bay1)
     * @param lock 是否锁定,true-锁定,false-解锁
     * @param fullPlt 是否满托,true-满托(设置为F),false-空托(设置为D),解锁时忽略此参数
     * @param userId 用户ID
     * @return 操作结果
     */
    R lockOrUnlockBayLocCache(String locNo, Boolean lock, Boolean fullPlt, Long userId);
    /**
     * 清空整排的所有库位(所有列)
     * @param locNo 库位号(用于获取排号row1)
     * @param lock 是否锁定,true-锁定,false-解锁(清空)
     * @param fullPlt 是否满托,true-满托(设置为F),false-空托(设置为D),解锁时忽略此参数
     * @param userId 用户ID
     * @return 操作结果
     */
    R clearAllColumnsInRow(String locNo, Boolean lock, Boolean fullPlt, Long userId);
}
src/main/java/com/zy/asrs/service/impl/LocCacheServiceImpl.java
@@ -17,6 +17,7 @@
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
@@ -98,4 +99,304 @@
            return R.error("初始化失败===>" + e.getMessage());
        }
    }
    /**
     * 锁定/解锁缓存库位(复用AGV搬货后的库位有货逻辑)
     * 锁定:满托设置为 "F"(在库),空托设置为 "D"(空桶/空栈板)
     * 解锁:统一设置为 "O"(空库位)
     * @param locNo 库位号
     * @param lock 是否锁定,true-锁定,false-解锁
     * @param fullPlt 是否满托,true-满托(设置为F),false-空托(设置为D),解锁时忽略此参数
     * @param userId 用户ID
     * @return 操作结果
     */
    @Override
    public R lockOrUnlockLocCache(String locNo, Boolean lock, Boolean fullPlt, Long userId) {
        try {
            if (Cools.isEmpty(locNo)) {
                return R.error("库位号不能为空");
            }
            // 根据库位号查询库位
            EntityWrapper<LocCache> wrapper = new EntityWrapper<>();
            wrapper.eq("loc_no", locNo);
            LocCache locCache = this.selectOne(wrapper);
            if (Cools.isEmpty(locCache)) {
                return R.error("库位不存在:" + locNo);
            }
            // 更新库位状态
            String locSts;
            if (lock) {
                // 锁定:满托设置为"F"(在库),空托设置为"D"(空桶/空栈板)
                if (fullPlt == null) {
                    return R.error("锁定操作时,满托参数不能为空");
                }
                locSts = fullPlt ? "F" : "D";
                locCache.setFullPlt(fullPlt ? "Y" : "N");
            } else {
                // 解锁:统一设置为"O"(空库位)
                locSts = "O";
                locCache.setFullPlt("N");
            }
            locCache.setLocSts(locSts);
            locCache.setModiUser(userId);
            locCache.setModiTime(new Date());
            boolean success = this.updateById(locCache);
            if (success) {
                String action = lock ? "锁定" : "解锁";
                String detail = lock ? (fullPlt ? "满托" : "空托") : "";
                log.info("{}库位成功:{},库位状态:{},{},操作人:{}", action, locNo, locCache.getLocSts(), detail, userId);
                return R.ok(action + "成功");
            } else {
                return R.error("操作失败");
            }
        } catch (Exception e) {
            log.error("锁定/解锁库位失败", e);
            return R.error("操作失败:" + e.getMessage());
        }
    }
    /**
     * 锁定/解锁当前排的所有库位
     * @param locNo 库位号(用于获取排号)
     * @param lock 是否锁定,true-锁定,false-解锁
     * @param userId 用户ID
     * @return 操作结果
     */
    @Override
    public R lockOrUnlockRowLocCache(String locNo, Boolean lock, Boolean fullPlt, Long userId) {
        try {
            if (Cools.isEmpty(locNo)) {
                return R.error("库位号不能为空");
            }
            // 根据库位号查询库位,获取排号
            EntityWrapper<LocCache> wrapper = new EntityWrapper<>();
            wrapper.eq("loc_no", locNo);
            LocCache locCache = this.selectOne(wrapper);
            if (Cools.isEmpty(locCache)) {
                return R.error("库位不存在:" + locNo);
            }
            Integer row = locCache.getRow1();
            if (Cools.isEmpty(row)) {
                return R.error("库位排号为空:" + locNo);
            }
            // 查询当前排的1、2、3列的所有库位(解锁整排时只清空1、2、3列)
            EntityWrapper<LocCache> rowWrapper = new EntityWrapper<>();
            rowWrapper.eq("row1", row)
                    .in("bay1", Arrays.asList(1, 2, 3)); // 只查询1、2、3列
            // 如果库位有库区ID,也加上库区条件
            if (!Cools.isEmpty(locCache.getAreaId())) {
                rowWrapper.eq("area_id", locCache.getAreaId());
            }
            List<LocCache> locCacheList = this.selectList(rowWrapper);
            if (Cools.isEmpty(locCacheList)) {
                return R.error("当前排没有找到库位");
            }
            // 批量更新库位状态
            Date now = new Date();
            int successCount = 0;
            String locSts;
            if (lock) {
                // 锁定:满托设置为"F"(在库),空托设置为"D"(空桶/空栈板)
                if (fullPlt == null) {
                    return R.error("锁定操作时,满托参数不能为空");
                }
                locSts = fullPlt ? "F" : "D";
            } else {
                // 解锁:统一设置为"O"(空库位)
                locSts = "O";
            }
            for (LocCache cache : locCacheList) {
                cache.setLocSts(locSts);
                if (lock) {
                    cache.setFullPlt(fullPlt ? "Y" : "N");
                } else {
                    cache.setFullPlt("N");
                }
                cache.setModiUser(userId);
                cache.setModiTime(now);
                if (this.updateById(cache)) {
                    successCount++;
                }
            }
            String action = lock ? "锁定" : "解锁";
            log.info("{}排{}所有库位成功,共{}个库位,操作人:{}", action, row, successCount, userId);
            return R.ok(String.format("%s成功,共处理 %d 个库位", action, successCount));
        } catch (Exception e) {
            log.error("锁定/解锁排库位失败", e);
            return R.error("操作失败:" + e.getMessage());
        }
    }
    /**
     * 锁定/解锁当前列的所有库位
     * @param locNo 库位号(用于获取列号bay1)
     * @param lock 是否锁定,true-锁定,false-解锁
     * @param fullPlt 是否满托,true-满托(设置为F),false-空托(设置为D),解锁时忽略此参数
     * @param userId 用户ID
     * @return 操作结果
     */
    @Override
    public R lockOrUnlockBayLocCache(String locNo, Boolean lock, Boolean fullPlt, Long userId) {
        try {
            if (Cools.isEmpty(locNo)) {
                return R.error("库位号不能为空");
            }
            // 根据库位号查询库位,获取列号
            EntityWrapper<LocCache> wrapper = new EntityWrapper<>();
            wrapper.eq("loc_no", locNo);
            LocCache locCache = this.selectOne(wrapper);
            if (Cools.isEmpty(locCache)) {
                return R.error("库位不存在:" + locNo);
            }
            Integer bay = locCache.getBay1();
            if (Cools.isEmpty(bay)) {
                return R.error("库位列号为空:" + locNo);
            }
            // 查询当前列的所有库位
            EntityWrapper<LocCache> bayWrapper = new EntityWrapper<>();
            bayWrapper.eq("bay1", bay);
            // 如果库位有库区ID,也加上库区条件
            if (!Cools.isEmpty(locCache.getAreaId())) {
                bayWrapper.eq("area_id", locCache.getAreaId());
            }
            List<LocCache> locCacheList = this.selectList(bayWrapper);
            if (Cools.isEmpty(locCacheList)) {
                return R.error("当前列没有找到库位");
            }
            // 批量更新库位状态
            Date now = new Date();
            int successCount = 0;
            String locSts;
            if (lock) {
                // 锁定:满托设置为"F"(在库),空托设置为"D"(空桶/空栈板)
                if (fullPlt == null) {
                    return R.error("锁定操作时,满托参数不能为空");
                }
                locSts = fullPlt ? "F" : "D";
            } else {
                // 解锁:统一设置为"O"(空库位)
                locSts = "O";
            }
            for (LocCache cache : locCacheList) {
                cache.setLocSts(locSts);
                if (lock) {
                    cache.setFullPlt(fullPlt ? "Y" : "N");
                } else {
                    cache.setFullPlt("N");
                }
                cache.setModiUser(userId);
                cache.setModiTime(now);
                if (this.updateById(cache)) {
                    successCount++;
                }
            }
            String action = lock ? "锁定" : "解锁";
            log.info("{}列{}所有库位成功,共{}个库位,操作人:{}", action, bay, successCount, userId);
            return R.ok(String.format("%s成功,共处理 %d 个库位", action, successCount));
        } catch (Exception e) {
            log.error("锁定/解锁列库位失败", e);
            return R.error("操作失败:" + e.getMessage());
        }
    }
    /**
     * 清空整排的所有库位(所有列)
     * @param locNo 库位号(用于获取排号row1)
     * @param lock 是否锁定,true-锁定,false-解锁(清空)
     * @param fullPlt 是否满托,true-满托(设置为F),false-空托(设置为D),解锁时忽略此参数
     * @param userId 用户ID
     * @return 操作结果
     */
    @Override
    public R clearAllColumnsInRow(String locNo, Boolean lock, Boolean fullPlt, Long userId) {
        try {
            if (Cools.isEmpty(locNo)) {
                return R.error("库位号不能为空");
            }
            // 根据库位号查询库位,获取排号
            EntityWrapper<LocCache> wrapper = new EntityWrapper<>();
            wrapper.eq("loc_no", locNo);
            LocCache locCache = this.selectOne(wrapper);
            if (Cools.isEmpty(locCache)) {
                return R.error("库位不存在:" + locNo);
            }
            Integer row = locCache.getRow1();
            if (Cools.isEmpty(row)) {
                return R.error("库位排号为空:" + locNo);
            }
            // 查询当前排的所有库位(所有列,不限制bay1)
            EntityWrapper<LocCache> rowWrapper = new EntityWrapper<>();
            rowWrapper.eq("row1", row);
            // 如果库位有库区ID,也加上库区条件
            if (!Cools.isEmpty(locCache.getAreaId())) {
                rowWrapper.eq("area_id", locCache.getAreaId());
            }
            List<LocCache> locCacheList = this.selectList(rowWrapper);
            if (Cools.isEmpty(locCacheList)) {
                return R.error("当前排没有找到库位");
            }
            // 批量更新库位状态
            Date now = new Date();
            int successCount = 0;
            String locSts;
            if (lock) {
                // 锁定:满托设置为"F"(在库),空托设置为"D"(空桶/空栈板)
                if (fullPlt == null) {
                    return R.error("锁定操作时,满托参数不能为空");
                }
                locSts = fullPlt ? "F" : "D";
            } else {
                // 解锁:统一设置为"O"(空库位)
                locSts = "O";
            }
            for (LocCache cache : locCacheList) {
                cache.setLocSts(locSts);
                if (lock) {
                    cache.setFullPlt(fullPlt ? "Y" : "N");
                } else {
                    cache.setFullPlt("N");
                }
                cache.setModiUser(userId);
                cache.setModiTime(now);
                if (this.updateById(cache)) {
                    successCount++;
                }
            }
            String action = lock ? "锁定" : "清空";
            log.info("{}排{}所有库位(所有列)成功,共{}个库位,操作人:{}", action, row, successCount, userId);
            return R.ok(String.format("%s成功,共处理 %d 个库位", action, successCount));
        } catch (Exception e) {
            log.error("清空整排库位失败", e);
            return R.error("操作失败:" + e.getMessage());
        }
    }
}
src/main/java/com/zy/asrs/service/impl/MobileServiceImpl.java
@@ -140,10 +140,9 @@
        String sourceSite = param.getSourceSite();
        String barcode = param.getBarcode();
        int ioType;
        // 查询源站点(库位)信息,但不检查是否存在,允许下单成功
        // 站点不存在的检查将在定时任务(AgvHandler.callAgv)中进行
        LocCache locCache = locCacheService.selectOne(new EntityWrapper<LocCache>().eq("loc_no", sourceSite));
        if (null == locCache) {
            throw new CoolException("站点不存在:" + sourceSite);
        }
        switch (type) {
            case 1:
                // 判断有没有组托
@@ -151,10 +150,13 @@
                if (count == 0) {
                    throw new CoolException("条码未组托:" + barcode);
                }
                ioType = 101;
                ioType = 1; // AGV容器入库(实托入库)
                locCache.setLocSts(LocStsType.LOC_STS_TYPE_R.type);
                locCacheService.updateById(locCache);
                // 如果库位存在,更新状态为入库预约;不存在则跳过,由定时任务处理
                if (locCache != null) {
                    locCache.setLocSts(LocStsType.LOC_STS_TYPE_S.type); // S.入库预约
                    locCacheService.updateById(locCache);
                }
                break;
            case 2:
                // 判断是拣选回库托盘
@@ -165,10 +167,13 @@
                if (wrkMast.getIoType() != 103 && wrkMast.getIoType() != 107) {
                    throw new CoolException("条码不需要回库:" + barcode);
                }
                ioType = wrkMast.getIoType() - 50;
                ioType = wrkMast.getIoType() - 50; // 103->53(拣料入库), 107->57(盘点入库)
                locCache.setLocSts(LocStsType.LOC_STS_TYPE_R.type);
                locCacheService.updateById(locCache);
                // 如果库位存在,更新状态为入库预约;不存在则跳过,由定时任务处理
                if (locCache != null) {
                    locCache.setLocSts(LocStsType.LOC_STS_TYPE_S.type); // S.入库预约(容器回库是入库操作)
                    locCacheService.updateById(locCache);
                }
                break;
            case 3:
                // 判断是否为空托入库:检查条码在wms中不存在,确认为空托盘
@@ -209,8 +214,9 @@
        }
        // 根据whs_type和库位编号前缀选择站点和机器人组
        Long whsType = locCache.getWhsType();
        String locNo = locCache.getLocNo();
        // 如果库位不存在,使用默认逻辑(根据type判断),站点不存在的检查将在定时任务中进行
        Long whsType = locCache != null ? locCache.getWhsType() : null;
        String locNo = locCache != null ? locCache.getLocNo() : sourceSite;
        List<String> targetStations;
        String robotGroup;
        
@@ -237,6 +243,7 @@
                agvProperties.getWestDisplayName(), robotGroup);
        } else {
            // whs_type为空或其他值,根据type判断(兼容旧逻辑)
            // 如果库位不存在,也使用此逻辑
            if (type == 1) {
                targetStations = agvProperties.getEastStations();
                robotGroup = agvProperties.getRobotGroupEast();
@@ -244,7 +251,11 @@
                targetStations = agvProperties.getWestStations();
                robotGroup = agvProperties.getRobotGroupWest();
            }
            log.warn("库位whs_type={}未配置或不在映射范围内,使用type={}的默认逻辑", whsType, type);
            if (locCache == null) {
                log.warn("源站点(库位){}不存在,使用type={}的默认逻辑,站点检查将在定时任务中进行", sourceSite, type);
            } else {
                log.warn("库位whs_type={}未配置或不在映射范围内,使用type={}的默认逻辑", whsType, type);
            }
        }
        
        // 将站点字符串列表转换为整数列表
@@ -252,31 +263,33 @@
                .map(Integer::parseInt)
                .collect(Collectors.toList());
        
        // 判断能入站点(in_enable="Y"表示能入)
        // 判断能入站点(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)
        ).stream().map(BasDevp::getDevNo).collect(Collectors.toList());
        if (sites.isEmpty()) {
            throw new CoolException("没有能入站点,whs_type:" + whsType + ",type:" + type);
        }
                        .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 = basDevpMapper.getCanInSites(sites);
        if (canInSites.isEmpty()) {
            throw new CoolException("请等待出库完成,type:" + type);
        }
        List<Integer> canInSites = sites.isEmpty() ? new ArrayList<>() : basDevpMapper.getCanInSites(sites);
        // 寻找入库任务最少的站点(且必须in_enable="Y"能入 和 canining="Y"可入)
        // 寻找入库任务最少的站点(且必须in_enable="Y"能入 和 canining="Y"可入),排除dev_no=0的无效站点
        // 注意:不在此处检查未完成的AGV任务,允许PDA持续申请下单
        // 未完成任务的检查将在发送AGV请求时进行(AgvHandler.callAgv)
        List<BasDevp> devList = basDevpMapper.selectList(new EntityWrapper<BasDevp>()
        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;
@@ -322,14 +335,20 @@
            }
            
            endSite = basDevp.getDevNo();
            // 入库暂存+1
            basDevpMapper.incrementInQty(endSite);
            // 检查站点是否有效(不能为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.warn("{}可用站点({})中没有可入站点(in_enable='Y'且canining='Y'),暂不分配站点,将在定时任务中分配", groupName, canInSites);
        }
@@ -347,11 +366,11 @@
                .setStaNo(endSite != null ? String.valueOf(endSite) : null) // 如果分配了站点则设置,否则为null,由定时任务分配
                .setSourceStaNo(sourceSite) // 设置源站点
                .setInvWh(robotGroup) // 根据whs_type设置机器人组
                .setFullPlt(ioType != 10 ? "N" : "Y")// 满板:Y
                .setFullPlt(ioType == 10 ? "N" : "Y")// 空托入库(ioType=10)设置为N,其他入库设置为Y(满板)
                .setPicking("N") // 拣料
                .setExitMk("N")// 退出
                .setSourceLocNo(locCache.getLocNo()) // 设置源库位编号,用于AGV fromBin
                .setEmptyMk(ioType == 10 ? "Y" : "N")// 空板
                .setSourceLocNo(locCache != null ? locCache.getLocNo() : sourceSite) // 设置源库位编号,用于AGV fromBin,如果库位不存在则使用sourceSite
                .setEmptyMk(ioType == 10 ? "Y" : "N")// 空托入库(ioType=10)设置为Y,其他设置为N
                .setBarcode(barcode)// 托盘码
                .setLinkMis("N")
                .setAppeTime(now)
@@ -360,8 +379,11 @@
            throw new CoolException("保存工作档失败");
        }
        // 更新暂存位状态为 R.出库预约
        basStationMapper.updateLocStsBatch( Collections.singletonList(String.valueOf(sourceSite)), "R");
        // 如果库位存在,根据ioType更新暂存位状态:入库任务设置为S(入库预约),出库任务设置为R(出库预约)
        if (locCache != null) {
            String locSts = (ioType < 100) ? "S" : "R"; // 入库任务(ioType < 100)设置为S,出库任务设置为R
            basStationMapper.updateLocStsBatch( Collections.singletonList(String.valueOf(sourceSite)), locSts);
        }
        return R.ok("agv任务生成成功!");
    }
src/main/java/com/zy/asrs/task/handler/AgvHandler.java
@@ -7,10 +7,12 @@
import com.zy.asrs.entity.TaskLog;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.entity.BasDevp;
import com.zy.asrs.entity.LocCache;
import com.zy.asrs.mapper.BasDevpMapper;
import com.zy.asrs.mapper.BasStationMapper;
import com.zy.asrs.mapper.WrkMastMapper;
import com.zy.asrs.service.ApiLogService;
import com.zy.asrs.service.LocCacheService;
import com.zy.asrs.service.TaskLogService;
import com.zy.asrs.service.TaskService;
import com.zy.common.constant.ApiInterfaceConstant;
@@ -55,6 +57,9 @@
    private BasDevpMapper basDevpMapper;
    @Resource
    private LocCacheService locCacheService;
    @Resource
    private AgvProperties agvProperties;
    /**
@@ -73,23 +78,56 @@
        }
        for (Task task : taskList) {
            // 如果任务没有分配站点,先分配站点
            // 如果任务状态已经是8(已呼叫AGV,正在搬运),则不再发送指令
            if (task.getWrkSts() != null && task.getWrkSts() == 8L) {
                log.debug("任务ID:{}状态已是8(正在搬运),跳过发送", task.getId());
                continue;
            }
            // 如果任务没有分配站点,先分配站点(只有为空时才分配,已经分配了不要清空)
            String staNo = task.getStaNo();
            if (staNo == null || staNo.isEmpty()) {
                Integer allocatedSite = allocateSiteForTask(task);
                if (allocatedSite == null) {
                    log.warn("任务ID:{}无法分配站点,跳过本次发送", task.getId());
                    continue; // 无法分配站点,跳过本次发送
                String errorMsg = allocateSiteForTask(task);
                if (errorMsg != null) {
                    // 无法分配站点,只在日志中记录,不记录到任务中(app不需要知道)
                    log.warn("任务ID:{}无法分配站点:{}", task.getId(), errorMsg);
                    continue;
                }
                staNo = String.valueOf(allocatedSite);
                task.setStaNo(staNo);
                taskService.updateById(task);
                // 检查是否成功分配了站点(如果返回null且staNo仍为空,说明所有站点都在搬运,等待下次再试)
                staNo = task.getStaNo();
                if (staNo == null || staNo.isEmpty()) {
                    // 所有站点都在搬运,暂不分配,等待下次定时任务再尝试
                    continue;
                }
                log.info("任务ID:{}已分配站点:{}", task.getId(), staNo);
            }
            
            // 检查目标站点是否有正在搬运的同类型AGV任务(出库和入库互不干扰)
            // 只有状态8(已呼叫AGV,正在搬运)的任务才会阻塞,状态7(待呼叫)的任务不阻塞
            // 这样可以避免所有任务都卡在呼叫状态,按id最小的优先呼叫
            // 检查目标站点是否有效(不为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;
                    }
                    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;
            }
            // 检查站点是否有状态8的同类型任务,有则跳过(不清空站点)
            if (staNo != null && !staNo.isEmpty() && task.getIoType() != null) {
                // 根据当前任务类型,只检查同类型的正在搬运任务(状态8)
                // 入库任务(ioType < 100):只检查入库类型的正在搬运任务
@@ -106,7 +144,7 @@
                    taskType = "出库";
                }
                
                // 只检查状态为8(已呼叫AGV,正在搬运)的同类型任务
                // 检查状态为8(已呼叫AGV,正在搬运)的同类型任务
                List<Task> transportingTasks = taskService.selectList(
                    new EntityWrapper<Task>()
                        .eq("sta_no", staNo)
@@ -117,9 +155,9 @@
                );
                
                if (!transportingTasks.isEmpty()) {
                    log.info("站点{}有{}个正在搬运的{}AGV任务,跳过本次发送,等待搬运完成。当前任务ID:{}",
                    log.info("站点{}有{}个正在搬运的{}AGV任务,跳过当前任务ID:{}",
                            staNo, transportingTasks.size(), taskType, task.getId());
                    continue; // 跳过本次发送,等待下次
                    continue;
                }
            }
            
@@ -147,9 +185,31 @@
                default:
            }
            String body = getRequest(task,namespace);
            // 打印请求信息
            log.info("{}呼叫agv搬运 - 请求地址:{}", namespace, url);
            // 获取当前重试次数
            int currentRetryCount = getRetryCount(task);
            int maxRetryCount = agvProperties.getCallRetry().getMaxRetryCount();
            boolean retryEnabled = agvProperties.getCallRetry().isEnabled();
            // 如果重试次数已达到最大值,跳过本次发送
            if (retryEnabled && currentRetryCount >= maxRetryCount) {
                log.warn("{}呼叫agv搬运 - 任务ID:{}已达到最大重试次数({}),停止重试",
                        namespace, task.getId(), maxRetryCount);
                // 记录最终失败信息
                task.setErrorTime(new Date());
                task.setErrorMemo(String.format("AGV呼叫失败,已达到最大重试次数(%d次)", maxRetryCount));
                taskService.updateById(task);
                continue;
            }
            // 打印请求信息(包含重试次数)
            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()
@@ -163,30 +223,41 @@
                
                // 检查响应是否为空
                if (response == null || response.trim().isEmpty()) {
                    log.error("{}呼叫agv搬运失败 - 任务ID:{},AGV接口返回为空", namespace, task.getId());
                    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) {
                    log.error("{}呼叫agv搬运失败 - 任务ID:{},响应JSON解析失败,响应内容:{}", namespace, task.getId(), response);
                    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");
                    log.error("{}呼叫agv搬运失败 - 任务ID:{},错误码:{},错误信息:{}",
                            namespace, task.getId(), code, 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) {
                log.error("{}呼叫agv搬运异常 - 任务ID:{},请求地址:{},请求参数:{},异常信息:{}",
                        namespace, task.getId(), url, body, e.getMessage(), e);
                String errorMsg = "异常信息:" + e.getMessage();
                log.error("{}呼叫agv搬运异常 - 任务ID:{},请求地址:{},请求参数:{},{}",
                        namespace, task.getId(), url, body, errorMsg, e);
                handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
            } finally {
                try {
                    // 保存接口日志
@@ -204,6 +275,106 @@
                }
            }
        }
    }
    /**
     * 处理AGV呼叫失败的情况
     * @param task 任务对象
     * @param namespace 命名空间(入库/出库/转移)
     * @param errorMsg 错误信息
     * @param retryEnabled 是否启用重试
     * @param maxRetryCount 最大重试次数
     * @param currentRetryCount 当前重试次数
     */
    private void handleCallFailure(Task task, String namespace, String errorMsg,
                                    boolean retryEnabled, int maxRetryCount, int currentRetryCount) {
        if (retryEnabled && currentRetryCount < maxRetryCount) {
            // 增加重试次数
            int newRetryCount = currentRetryCount + 1;
            task.setMemo(updateRetryCount(task.getMemo(), newRetryCount));
            task.setErrorTime(new Date());
            task.setErrorMemo(String.format("AGV呼叫失败(第%d次重试):%s", newRetryCount, errorMsg));
            taskService.updateById(task);
            log.info("{}呼叫agv搬运失败 - 任务ID:{},已重试{}次,将在下次定时任务时继续重试(最多{}次)",
                    namespace, task.getId(), newRetryCount, maxRetryCount);
        } else {
            // 不启用重试或已达到最大重试次数,停止重试
            task.setErrorTime(new Date());
            if (retryEnabled) {
                task.setErrorMemo(String.format("AGV呼叫失败,已达到最大重试次数(%d次):%s", maxRetryCount, errorMsg));
            } else {
                task.setErrorMemo(String.format("AGV呼叫失败(重试未启用):%s", errorMsg));
            }
            taskService.updateById(task);
            log.warn("{}呼叫agv搬运失败 - 任务ID:{},停止重试。错误信息:{}",
                    namespace, task.getId(), errorMsg);
        }
    }
    /**
     * 从memo字段中获取重试次数
     * memo格式:如果包含"retryCount:数字",则返回该数字,否则返回0
     * @param task 任务对象
     * @return 重试次数
     */
    private int getRetryCount(Task task) {
        String memo = task.getMemo();
        if (memo == null || memo.trim().isEmpty()) {
            return 0;
        }
        try {
            // 查找 "retryCount:数字" 格式
            String prefix = "retryCount:";
            int index = memo.indexOf(prefix);
            if (index >= 0) {
                int startIndex = index + prefix.length();
                int endIndex = memo.indexOf(",", startIndex);
                if (endIndex < 0) {
                    endIndex = memo.length();
                }
                String countStr = memo.substring(startIndex, endIndex).trim();
                return Integer.parseInt(countStr);
            }
        } catch (Exception e) {
            log.warn("解析任务ID:{}的重试次数失败,memo:{}", task.getId(), memo, e);
        }
        return 0;
    }
    /**
     * 更新memo字段中的重试次数
     * @param memo 原始memo内容
     * @param retryCount 新的重试次数
     * @return 更新后的memo内容
     */
    private String updateRetryCount(String memo, int retryCount) {
        if (memo == null) {
            memo = "";
        }
        // 移除旧的retryCount信息
        String cleanedMemo = clearRetryInfo(memo);
        // 添加新的retryCount信息
        if (cleanedMemo.isEmpty()) {
            return "retryCount:" + retryCount;
        } else {
            return cleanedMemo + ",retryCount:" + retryCount;
        }
    }
    /**
     * 清除memo字段中的重试信息
     * @param memo 原始memo内容
     * @return 清除后的memo内容
     */
    private String clearRetryInfo(String memo) {
        if (memo == null || memo.trim().isEmpty()) {
            return "";
        }
        // 移除 "retryCount:数字" 格式的内容
        String result = memo.replaceAll("retryCount:\\d+", "").trim();
        // 清理多余的逗号
        result = result.replaceAll("^,|,$", "").replaceAll(",,+", ",");
        return result;
    }
    /**
@@ -258,9 +429,9 @@
    /**
     * 为任务分配站点(定时任务中调用)
     * @param task 任务对象
     * @return 分配的站点编号,如果无法分配则返回null
     * @return 如果无法分配站点,返回错误信息;如果分配成功,返回null并更新task的staNo
     */
    private Integer allocateSiteForTask(Task task) {
    private String allocateSiteForTask(Task task) {
        // 根据任务的invWh(机器人组)判断是东侧还是西侧
        String robotGroup = task.getInvWh();
        List<String> targetStations;
@@ -282,8 +453,9 @@
        }
        
        if (targetStations.isEmpty()) {
            log.warn("任务ID:{}没有可用的目标站点配置", task.getId());
            return null;
            String errorMsg = "没有可用的目标站点配置";
            log.warn("任务ID:{}", errorMsg, task.getId());
            return errorMsg;
        }
        
        // 将站点字符串列表转换为整数列表
@@ -291,35 +463,45 @@
                .map(Integer::parseInt)
                .collect(Collectors.toList());
        
        // 判断能入站点(in_enable="Y"表示能入)
        // 判断能入站点(in_enable="Y"表示能入),排除dev_no=0的无效站点
        List<Integer> sites = basDevpMapper.selectList(
                new EntityWrapper<BasDevp>()
                        .eq("in_enable", "Y")
                        .in("dev_no", siteIntList)
        ).stream().map(BasDevp::getDevNo).collect(Collectors.toList());
                        .ne("dev_no", 0) // 排除dev_no=0的无效站点
        ).stream()
                .map(BasDevp::getDevNo)
                .filter(devNo -> devNo != null && devNo != 0) // 再次过滤,确保不为null或0
                .collect(Collectors.toList());
        
        if (sites.isEmpty()) {
            log.warn("任务ID:{}没有能入站点", task.getId());
            return null;
            String errorMsg = "没有能入站点";
            log.warn("任务ID:{}", errorMsg, task.getId());
            return errorMsg;
        }
        
        // 获取没有出库任务的站点
        List<Integer> canInSites = basDevpMapper.getCanInSites(sites);
        if (canInSites.isEmpty()) {
            log.warn("任务ID:{}没有可入站点(请等待出库完成)", task.getId());
            return null;
            String errorMsg = "请等待出库完成";
            log.warn("任务ID:{}没有可入站点({})", task.getId(), errorMsg);
            return errorMsg;
        }
        
        // 寻找入库任务最少的站点(且必须in_enable="Y"能入 和 canining="Y"可入)
        // 寻找入库任务最少的站点(且必须in_enable="Y"能入 和 canining="Y"可入),排除dev_no=0的无效站点
        List<BasDevp> devList = basDevpMapper.selectList(new EntityWrapper<BasDevp>()
                .in("dev_no", canInSites)
                .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 (devList.isEmpty()) {
            log.warn("任务ID:{}没有可入站点(in_enable='Y'且canining='Y')", task.getId());
            return null;
            String errorMsg = "没有可入站点(in_enable='Y'且canining='Y')";
            log.warn("任务ID:{}", errorMsg, task.getId());
            return errorMsg;
        }
        
        // 先按规则排序(入库任务数排序)
@@ -409,20 +591,31 @@
            break;
        }
        
        // 如果所有站点都在搬运,则不分配站点
        // 如果所有站点都在搬运,则不分配站点(只在定时任务中记录日志,不返回错误信息)
        if (selectedSite == null) {
            log.warn("任务ID:{}的所有候选站点都有正在搬运的{}任务,暂不分配站点,等待空闲",
                task.getId(), taskIoType != null && taskIoType < 100 ? "入库" : "出库");
            return null;
//            log.warn("任务ID:{},暂不分配站点,等待空闲 - 所有候选站点都有正在搬运的{}任务",
//                task.getId(), taskIoType != null && taskIoType < 100 ? "入库" : "出库");
            return null; // 返回null,表示暂不分配,等待下次定时任务再尝试
        }
        
        Integer endSite = selectedSite.getDevNo();
        
        // 检查站点是否有效(不能为0或null)
        if (endSite == null || endSite == 0) {
            String errorMsg = String.format("分配的站点无效(dev_no=%s)", endSite);
            log.error("任务ID:{},{}", task.getId(), errorMsg);
            return errorMsg;
        }
        // 入库暂存+1
        basDevpMapper.incrementInQty(endSite);
        
        // 更新任务的站点编号
        task.setStaNo(String.valueOf(endSite));
        taskService.updateById(task);
        log.info("任务ID:{}已分配站点:{}", task.getId(), endSite);
        return endSite;
        return null; // 分配成功,返回null
    }
    /**
src/main/java/com/zy/asrs/task/handler/WorkMastHandler.java
@@ -781,15 +781,9 @@
        }
        
        // 分配缓存库位:只查找WA开头的库位(CA开头只做入库,WA开头才会被出库分配缓存区)
        // 使用新的分配逻辑:按列优先级(第三列→第二列→第一列)分配
        String cacheAreaPrefix = agvProperties.getLocationPrefix().getCacheArea();
        LocCache cacheLoc = locCacheService.selectOne(new EntityWrapper<LocCache>()
                .eq("whs_type", targetWhsType) // 根据出库站点判断的whs_type
                .like("loc_no", cacheAreaPrefix + "%") // 只查找WA开头的库位(从配置读取)
                .eq("frozen", 0)
                .eq("loc_sts", LocStsType.LOC_STS_TYPE_O.type) // O.闲置
                .ne("full_plt", isEmptyPallet ? "Y" : "N") // 空托不选满板库位,满托不选空板库位
                .orderAsc(Arrays.asList("row1", "bay1", "lev1"))
                .last("OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY"));
        LocCache cacheLoc = allocateCacheLocationByPriority(targetWhsType, cacheAreaPrefix, isEmptyPallet);
        
        if (cacheLoc == null) {
            log.warn("{}侧没有可用的{}缓存库位,不生成{}AGV任务,任务ID:{}", 
@@ -905,6 +899,167 @@
    }
    
    /**
     * 按优先级分配缓存库位
     * 优先级规则:
     * 1. 优先分配第三列(bay1=3),且该排的1、2、3列都是空的
     * 2. 如果所有第三列都有货,则分配第二列(bay1=2),且该排的1、2列都是空的
     * 3. 如果所有排的第二第三列都满了,则分配第一列(bay1=1)
     * 4. 如果所有第一列都满了,再检查第二列和第三列
     * 5. 层(lev1)从第一层开始
     *
     * @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、2、3列的状态
        // 列状态:true表示该列有空库位,false表示该列已满
        Map<Integer, Map<Integer, Boolean>> rowColumnStatus = new HashMap<>();
        for (Map.Entry<Integer, List<LocCache>> entry : locationsByRow.entrySet()) {
            Integer row = entry.getKey();
            List<LocCache> rowLocs = entry.getValue();
            Map<Integer, Boolean> columnStatus = new HashMap<>();
            // 检查第1、2、3列是否有空库位
            for (int bay = 1; bay <= 3; bay++) {
                final int bayFinal = bay;  // 创建final副本供lambda使用
                boolean hasEmpty = rowLocs.stream()
                        .anyMatch(loc -> loc.getBay1() != null && loc.getBay1() == bayFinal);
                columnStatus.put(bay, hasEmpty);
            }
            rowColumnStatus.put(row, columnStatus);
        }
        // 优先级1:分配第三列(bay1=3),且该排的1、2、3列都是空的
        for (Map.Entry<Integer, List<LocCache>> entry : locationsByRow.entrySet()) {
            Integer row = entry.getKey();
            Map<Integer, Boolean> columnStatus = rowColumnStatus.get(row);
            // 检查该排的1、2、3列是否都是空的
            if (Boolean.TRUE.equals(columnStatus.get(1)) &&
                Boolean.TRUE.equals(columnStatus.get(2)) &&
                Boolean.TRUE.equals(columnStatus.get(3))) {
                // 分配该排的第三列,从第一层开始
                List<LocCache> bay3Locs = entry.getValue().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();
            Map<Integer, Boolean> columnStatus = rowColumnStatus.get(row);
            // 检查该排的1、2列是否都是空的
            if (Boolean.TRUE.equals(columnStatus.get(1)) &&
                Boolean.TRUE.equals(columnStatus.get(2))) {
                // 分配该排的第二列,从第一层开始
                List<LocCache> bay2Locs = entry.getValue().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();
            Map<Integer, Boolean> columnStatus = rowColumnStatus.get(row);
            // 检查该排的第一列是否有空库位
            if (Boolean.TRUE.equals(columnStatus.get(1))) {
                // 分配该排的第一列,从第一层开始
                List<LocCache> bay1Locs = entry.getValue().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();
            Map<Integer, Boolean> columnStatus = rowColumnStatus.get(row);
            // 检查该排的第二列是否有空库位
            if (Boolean.TRUE.equals(columnStatus.get(2))) {
                List<LocCache> bay2Locs = entry.getValue().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();
            Map<Integer, Boolean> columnStatus = rowColumnStatus.get(row);
            // 检查该排的第三列是否有空库位
            if (Boolean.TRUE.equals(columnStatus.get(3))) {
                List<LocCache> bay3Locs = entry.getValue().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);
                }
            }
        }
        // 如果所有列都满了,返回null
        return null;
    }
    /**
     * 为出库到缓存区的任务分配站点(使用和入库一样的分配策略)
     * @param cacheStations 缓存区站点列表
     * @param ioType 任务类型(101=全板出库,110=空板出库)
@@ -1006,7 +1161,7 @@
        
        // 如果所有站点都在搬运,则不分配站点
        if (selectedSite == null) {
            log.warn("所有缓存区站点都有正在搬运的出库任务,暂不分配站点,等待空闲");
//            log.warn("所有缓存区站点都有正在搬运的出库任务,暂不分配站点,等待空闲");
            return null;
        }
        
@@ -1058,14 +1213,32 @@
                    }
                });
                locCache.setLocSts(LocStsType.LOC_STS_TYPE_F.type);
                // 根据fullPlt设置库位状态:满托设置为"F"(在库),空托设置为"D"(空桶/空栈板)
                boolean isFullPlt = wrkMast.getFullPlt() != null && wrkMast.getFullPlt().equals("Y");
                locCache.setLocSts(isFullPlt ? LocStsType.LOC_STS_TYPE_F.type : LocStsType.LOC_STS_TYPE_D.type);
                locCache.setFullPlt(isFullPlt ? "Y" : "N");
                locCache.setModiTime(new Date());
                locCache.setBarcode(wrkMast.getBarcode());
                locCache.setModiTime(new Date());
                locCache.setIoTime(new Date());
                if (!locCacheService.updateById(locCache)) {
                    throw new CoolException("库位状态修改失败!");
                }
            } else if (ioType == 10 || ioType == 53 || ioType == 57) {
                // 空托入库或其他入库类型,也需要更新缓存库位状态
                LocCache locCache = locCacheService.selectOne(new EntityWrapper<LocCache>().eq("loc_no", wrkMast.getLocNo()));
                if (locCache != null) {
                    // 根据fullPlt设置库位状态:满托设置为"F"(在库),空托设置为"D"(空桶/空栈板)
                    // ioType == 10 是空托入库,默认设置为"D"
                    boolean isFullPlt = (ioType != 10) && (wrkMast.getFullPlt() != null && wrkMast.getFullPlt().equals("Y"));
                    locCache.setLocSts(isFullPlt ? LocStsType.LOC_STS_TYPE_F.type : LocStsType.LOC_STS_TYPE_D.type);
                    locCache.setFullPlt(isFullPlt ? "Y" : "N");
                    locCache.setModiTime(new Date());
                    locCache.setBarcode(wrkMast.getBarcode());
                    locCache.setIoTime(new Date());
                    if (!locCacheService.updateById(locCache)) {
                        throw new CoolException("库位状态修改失败!");
                    }
                }
            }
            
            // 更新任务状态为5(库存更新完成)
src/main/java/com/zy/common/properties/AgvProperties.java
@@ -49,6 +49,11 @@
    private LocationPrefix locationPrefix = new LocationPrefix();
    /**
     * AGV呼叫重试配置
     */
    private AgvCallRetry callRetry = new AgvCallRetry();
    /**
     * whs_type映射配置内部类
     */
    @Data
@@ -167,4 +172,31 @@
         */
        private String cacheArea = "WA";
    }
    /**
     * AGV呼叫重试配置内部类
     */
    @Data
    public static class AgvCallRetry {
        /**
         * 是否启用重试机制
         * true: 启用重试,失败后会自动重试
         * false: 不启用重试,失败后直接停止(默认)
         */
        private boolean enabled = false;
        /**
         * 最大重试次数
         * 当呼叫AGV失败时,最多重试多少次后停止
         * 默认值:3次
         */
        private int maxRetryCount = 3;
        /**
         * 重试间隔时间(秒)
         * 每次重试之间的等待时间
         * 默认值:5秒
         */
        private int retryIntervalSeconds = 5;
    }
}
src/main/resources/application-dev.yml
@@ -68,6 +68,16 @@
Agv:
  sendTask: false
  # AGV呼叫重试配置
  callRetry:
    # 是否启用重试机制
    # true: 启用重试,失败后会自动重试
    # false: 不启用重试,失败后直接停止
    enabled: true
    # 最大重试次数(失败后最多重试多少次后停止)
    maxRetryCount: 3
    # 重试间隔时间(秒,每次重试之间的等待时间)
    retryIntervalSeconds: 5
  # 东侧配置
  east:
    robotGroup: "Group-001"
@@ -104,6 +114,25 @@
    inboundOnly: "CA"
    # WA前缀:会被出库分配缓存区的库位前缀
    cacheArea: "WA"
  # 缓存库位分配规则配置
  cacheLocationAllocation:
    # 分配优先级说明:
    # 优先级1:分配第三列(bay1=3),且该排的1、2、3列都是空的
    # 优先级2:分配第二列(bay1=2),且该排的1、2列都是空的
    # 优先级3:分配第一列(bay1=1),所有排的第二第三列都满了
    # 优先级4:如果所有第一列都满了,再检查第二列
    # 优先级5:最后检查第三列
    # 层(lev1)从第一层开始
    priority:
      # 列优先级顺序(从高到低)
      bayPriority:
        - 3  # 第三列优先级最高
        - 2  # 第二列
        - 1  # 第一列
      # 层优先级:从第一层开始
      levStart: 1
      # 是否要求排的所有列都为空才分配(优先级1和2的要求)
      requireAllColumnsEmpty: true
# 越库配置
cross-dock:
src/main/webapp/static/js/locCache/locCache.js
@@ -36,7 +36,7 @@
            , { field: 'appeTime$', align: 'center', title: '添加时间', hide: true }
            , { field: 'frozen', align: 'center', title: '是否冻结', hide: true }
            , { field: 'frozenMemo', align: 'center', title: '冻结备注', hide: true }
            , { fixed: 'right', title: '操作', align: 'center', toolbar: '#operate', width: 200 }]
            , { fixed: 'right', title: '操作', align: 'center', toolbar: '#operate', width: 350 }]
        ],
        request: {
            pageName: 'curr', pageSize: 'limit'
@@ -144,6 +144,12 @@
                break;
            case "del":
                del([data.id]);
                break;
            case "clearLoc":
                clearLocation(data);
                break;
            case "setInStock":
                setInStock(data);
                break;
        }
    });
@@ -320,6 +326,71 @@
    layDateRender();
    // 清空库位
    function clearLocation(data) {
        layer.confirm('确认清空库位:' + data.locNo + '?', {
            skin: 'layui-layer-admin', shade: .1
        }, function (i) {
            layer.close(i);
            var loadIndex = layer.load(2);
            $.ajax({
                url: baseUrl + "/locCache/lockOrUnlock/auth",
                headers: { 'token': localStorage.getItem('token') },
                data: {
                    locNo: data.locNo,
                    lock: false  // false表示解锁/清空
                },
                method: 'POST',
                success: function (res) {
                    layer.close(loadIndex);
                    if (res.code === 200) {
                        layer.msg(res.msg || '清空库位成功', { icon: 1 });
                        tableReload();
                    } else if (res.code === 403) {
                        top.location.href = baseUrl + "/";
                    } else {
                        layer.msg(res.msg || '清空库位失败', { icon: 2 });
                    }
                }
            })
        });
    }
    // 改为有货状态
    function setInStock(data) {
        layer.prompt({
            title: '请选择满托/空托',
            formType: 2,
            content: '<div style="padding: 20px;"><label><input type="radio" name="fullPlt" value="true" checked> 满托</label><br><br><label><input type="radio" name="fullPlt" value="false"> 空托</label></div>',
            area: ['300px', '200px']
        }, function(value, index, elem){
            var fullPlt = $(elem).find('input[name="fullPlt"]:checked').val() === 'true';
            layer.close(index);
            var loadIndex = layer.load(2);
            $.ajax({
                url: baseUrl + "/locCache/lockOrUnlock/auth",
                headers: { 'token': localStorage.getItem('token') },
                data: {
                    locNo: data.locNo,
                    lock: true,  // true表示锁定/改为有货
                    fullPlt: fullPlt
                },
                method: 'POST',
                success: function (res) {
                    layer.close(loadIndex);
                    if (res.code === 200) {
                        layer.msg(res.msg || '改为有货状态成功', { icon: 1 });
                        tableReload();
                    } else if (res.code === 403) {
                        top.location.href = baseUrl + "/";
                    } else {
                        layer.msg(res.msg || '改为有货状态失败', { icon: 2 });
                    }
                }
            })
        });
    }
});
// 关闭动作
src/main/webapp/static/js/role/rolePower.js
@@ -39,19 +39,28 @@
    form.on('submit(save)', function () {
        var param = [];
        var checkData = tree.getChecked('powerTree');
        if (!checkData || !Array.isArray(checkData)) {
            layer.msg('没有选中的权限数据');
            return false;
        }
        checkData.map(function (obj) {
            obj.children.map(function (resource) {
                var childrens = [];
                resource.children.map(function (resource) {
                    childrens.push(resource.id);
            // 检查 obj.children 是否存在且为数组
            if (obj.children && Array.isArray(obj.children)) {
                obj.children.map(function (resource) {
                    var childrens = [];
                    // 检查 resource.children 是否存在且为数组
                    if (resource.children && Array.isArray(resource.children)) {
                        resource.children.map(function (resource) {
                            childrens.push(resource.id);
                        });
                    }
                    var one = {
                        'two': resource.id,
                        'three': childrens
                    };
                    param.push(one);
                });
                var one = {
                    'two': resource.id,
                    'three': childrens
                };
                param.push(one);
            })
            }
        });
        $.ajax({
            url: baseUrl+"/power/auth",
src/main/webapp/views/locCache/locCache.html
@@ -263,6 +263,8 @@
    <script type="text/html" id="operate">
    <a class="layui-btn layui-btn-xs btn-detlShow" lay-event="showDetl">明细</a>
   <a class="layui-btn layui-btn-primary layui-btn-xs btn-edit" lay-event="edit">修改</a>
    <a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="clearLoc">清空库位</a>
    <a class="layui-btn layui-btn-xs layui-btn-warm" lay-event="setInStock">改为有货</a>
    <a class="layui-btn layui-btn-danger layui-btn-xs btn-edit" lay-event="del">删除</a>
</script>