package com.zy.asrs.task.handler;
|
|
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSONObject;
|
import com.baomidou.mybatisplus.mapper.EntityWrapper;
|
import com.core.common.Cools;
|
import com.zy.asrs.entity.Task;
|
import com.zy.asrs.entity.TaskLog;
|
import com.zy.asrs.entity.WrkMast;
|
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.asrs.service.WrkMastService;
|
import com.zy.asrs.service.WrkMastLogService;
|
import com.zy.asrs.entity.WrkMastLog;
|
import com.zy.common.constant.ApiInterfaceConstant;
|
import com.zy.common.properties.AgvProperties;
|
import com.zy.common.utils.HttpHandler;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.beans.BeanUtils;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import javax.annotation.Resource;
|
import java.util.*;
|
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.stream.Collectors;
|
|
/**
|
* @author pang.jiabao
|
* @description AGV交互相关定时任务处理类
|
* @createDate 2025/11/18 14:42
|
*/
|
@Slf4j
|
@Service
|
public class AgvHandler {
|
|
@Resource
|
private WrkMastMapper wrkMastMapper;
|
|
@Resource
|
private ApiLogService apiLogService;
|
|
@Resource
|
private TaskService taskService;
|
|
@Resource
|
private TaskLogService taskLogService;
|
|
@Resource
|
private BasStationMapper basStationMapper;
|
|
@Resource
|
private BasDevpMapper basDevpMapper;
|
|
@Resource
|
private LocCacheService locCacheService;
|
|
@Resource
|
private AgvProperties agvProperties;
|
|
@Resource
|
private WrkMastService wrkMastService;
|
|
@Resource
|
private WrkMastLogService wrkMastLogService;
|
|
/**
|
* 站点轮询计数器,用于平均分配站点
|
* Key: 站点组标识(如 "east" 或 "west"),Value: 当前轮询索引
|
*/
|
private final Map<String, AtomicInteger> siteRoundRobinCounters = new ConcurrentHashMap<>();
|
|
/**
|
* 呼叫AGV
|
*
|
* 重要:此方法只能从 AgvScheduler.callAgv() 定时任务中调用!
|
* 所有AGV呼叫请求必须通过定时任务统一处理,确保:
|
* 1. 任务按顺序处理,避免并发冲突
|
* 2. 站点分配和AGV呼叫分离,职责清晰
|
* 3. 统一的错误处理和重试机制
|
*
|
* @param taskList 任务列表(通常只包含一个任务)
|
* @return 是否成功处理了任务(成功呼叫AGV,状态从7变为8)
|
*/
|
public boolean callAgv(List<Task> taskList) {
|
// 记录调用堆栈,确保只能从定时任务调用
|
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
boolean calledFromScheduler = false;
|
for (StackTraceElement element : stackTrace) {
|
if (element.getClassName().contains("AgvScheduler") &&
|
element.getMethodName().equals("callAgv")) {
|
calledFromScheduler = true;
|
break;
|
}
|
}
|
|
if (!calledFromScheduler) {
|
log.error("严重错误:callAgv方法只能从AgvScheduler.callAgv()定时任务中调用!当前调用堆栈:{}",
|
Arrays.stream(stackTrace).limit(5).map(StackTraceElement::toString).collect(Collectors.joining("\n")));
|
return false;
|
}
|
|
if (!agvProperties.isSendTask()) {
|
log.warn("AGV呼叫:配置isSendTask=false,不发送AGV任务");
|
return false;
|
}
|
|
// 每次只处理一个任务,避免高并发执行
|
if (taskList == null || taskList.isEmpty()) {
|
return false;
|
}
|
|
// 只处理第一个任务
|
Task task = taskList.get(0);
|
|
// 呼叫AGV定时任务只处理已分配站点的任务,站点分配由单独的定时任务处理
|
String staNo = task.getStaNo();
|
String displayTaskId = (task.getWrkNo() != null) ? String.valueOf(task.getWrkNo()) : String.valueOf(task.getId());
|
|
// 检查站点是否有效(不为空、不为空字符串、不为0)
|
if (staNo == null || staNo.isEmpty() || staNo.equals("0")) {
|
// 没有有效站点,跳过(站点分配由allocateSite定时任务处理)
|
log.warn("定时任务callAgv:任务ID:{}没有有效站点分配(sta_no={}),跳过发送,等待分配站点定时任务处理", displayTaskId, staNo);
|
return false; // 返回false,表示未成功处理
|
}
|
|
// 检查站点是否有效(不为0且存在)
|
try {
|
Integer siteNo = Integer.parseInt(staNo);
|
if (siteNo == null || siteNo == 0) {
|
log.warn("定时任务callAgv:任务ID:{}的目标站点{}无效(为0),清空站点分配,将重新分配", displayTaskId, staNo);
|
task.setStaNo(null);
|
taskService.updateById(task);
|
return false; // 返回false,让分配站点定时任务重新分配
|
}
|
// 检查站点是否存在
|
List<BasDevp> basDevpList = basDevpMapper.selectList(new EntityWrapper<BasDevp>().eq("dev_no", siteNo));
|
if (basDevpList == null || basDevpList.isEmpty()) {
|
log.warn("定时任务callAgv:任务ID:{}的目标站点{}不存在,清空站点分配,将重新分配", displayTaskId, staNo);
|
task.setStaNo(null);
|
taskService.updateById(task);
|
return false; // 返回false,让分配站点定时任务重新分配
|
}
|
} catch (NumberFormatException e) {
|
// 站点格式错误,清空站点,让分配站点定时任务重新分配
|
log.warn("定时任务callAgv:任务ID:{}的目标站点{}格式错误,清空站点分配,将重新分配", displayTaskId, staNo);
|
task.setStaNo(null);
|
taskService.updateById(task);
|
return false; // 返回false,让分配站点定时任务重新分配
|
}
|
|
log.info("定时任务callAgv:任务ID:{}已有站点分配:{},准备发送AGV命令", displayTaskId, staNo);
|
|
|
// 检查站点是否有状态8的同类型任务,有则跳过(不清空站点)
|
// 规则:最多给每个站点分配1条任务,未完成则等待
|
// 判断任务是否完成:通过查询agv工作档(wrk_mast),如果查不到就是完成了
|
if (staNo != null && !staNo.isEmpty() && task.getIoType() != null) {
|
// 根据当前任务类型,只检查同类型的正在搬运任务(状态8)
|
// 入库任务(ioType < 100):只检查入库类型的正在搬运任务
|
// 出库任务(ioType >= 100):只检查出库类型的正在搬运任务
|
List<Integer> ioTypes;
|
String taskType;
|
if (task.getIoType() < 100) {
|
// 入库任务:只检查入库类型(1, 10, 53, 57)
|
ioTypes = Arrays.asList(1, 10, 53, 57);
|
taskType = "入库";
|
} else {
|
// 出库任务:只检查出库类型(101, 110, 103, 107)
|
ioTypes = Arrays.asList(101, 110, 103, 107);
|
taskType = "出库";
|
}
|
|
// 通过SQL查询该站点所有AGV任务(包括状态7和8)
|
// 规则:最多给每个站点分配1条任务,未完成则等待
|
// 判断任务是否完成:通过查询agv工作档(wrk_mast),如果查不到就是完成了
|
List<Task> allTasks = taskService.selectList(
|
new EntityWrapper<Task>()
|
.eq("sta_no", staNo)
|
.eq("task_type", "agv")
|
.in("wrk_sts", 7L, 8L)
|
.in("io_type", ioTypes)
|
.ne("id", task.getId()) // 排除当前任务本身
|
.eq("is_deleted", 0) // 排除已删除的任务
|
);
|
|
int taskCount = allTasks != null ? allTasks.size() : 0;
|
log.info("定时任务:任务ID:{},站点{}查询到{}个AGV任务(排除当前任务),开始检查是否有未完成的任务",
|
displayTaskId, staNo, taskCount);
|
|
// 检查是否有有效的未完成任务(通过查询agv工作档判断)
|
boolean hasValidTask = false;
|
if (taskCount > 0) {
|
for (Task agvTask : allTasks) {
|
if (agvTask.getWrkNo() != null) {
|
// 查询工作档,如果查不到就是完成了
|
WrkMast wrkMast = wrkMastService.selectOne(
|
new EntityWrapper<WrkMast>().eq("wrk_no", agvTask.getWrkNo())
|
);
|
if (wrkMast != null) {
|
// 工作档存在,检查是否已完成
|
Long wrkSts = wrkMast.getWrkSts();
|
Integer ioType = agvTask.getIoType();
|
|
if (wrkSts != null && ioType != null) {
|
// 入库任务:状态4或5表示完成
|
boolean isCompleted = false;
|
if ((ioType == 1 || ioType == 10 || ioType == 53 || ioType == 57) &&
|
(wrkSts == 4L || wrkSts == 5L)) {
|
isCompleted = true;
|
}
|
// 出库任务:状态14或15表示完成
|
else if ((ioType == 101 || ioType == 110 || ioType == 103 || ioType == 107) &&
|
(wrkSts == 14L || wrkSts == 15L)) {
|
isCompleted = true;
|
}
|
|
if (!isCompleted) {
|
// 工作档存在且未完成,视为有效任务
|
hasValidTask = true;
|
log.info("定时任务:任务ID:{},站点{}已有1条未完成的{}AGV任务(工作号:{},工作档状态:{}),跳过当前任务,等待完成",
|
displayTaskId, staNo, taskType, agvTask.getWrkNo(), wrkSts);
|
break;
|
} else {
|
log.info("定时任务:任务ID:{},站点{}的任务{}(工作号:{})已完成(工作档状态:{}),不算有效任务",
|
displayTaskId, staNo, agvTask.getId(), agvTask.getWrkNo(), wrkSts);
|
}
|
} else {
|
// 工作档存在但状态未知,视为有效任务
|
hasValidTask = true;
|
log.info("定时任务:任务ID:{},站点{}已有1条{}AGV任务(工作号:{},工作档状态未知),跳过当前任务,等待完成",
|
displayTaskId, staNo, taskType, agvTask.getWrkNo());
|
break;
|
}
|
} else {
|
// 如果工作档查不到,视为已完成,不算有效任务
|
log.info("定时任务:任务ID:{},站点{}的任务{}(工作号:{})工作档查不到,视为已完成,不算有效任务",
|
displayTaskId, staNo, agvTask.getId(), agvTask.getWrkNo());
|
}
|
}
|
}
|
} else {
|
log.info("定时任务:任务ID:{},站点{}没有其他AGV任务,可以分配", displayTaskId, staNo);
|
}
|
|
// 如果站点有有效的未完成任务,跳过当前任务
|
if (hasValidTask) {
|
log.warn("定时任务:任务ID:{},站点{}有未完成的{}AGV任务,跳过当前任务,等待完成。下次将重新尝试处理此任务",
|
displayTaskId, staNo, taskType);
|
return false; // 返回false,表示未成功处理,下次会重新尝试
|
}
|
|
// 检查是否有状态8且已收到AGV确认的任务(用于日志记录)
|
List<Task> transportingTasks = taskService.selectList(
|
new EntityWrapper<Task>()
|
.eq("sta_no", staNo)
|
.eq("task_type", "agv")
|
.eq("wrk_sts", 8L) // 只检查正在搬运(8)的任务
|
.isNotNull("plc_str_time") // 只检查已收到AGV确认的任务(plc_str_time不为空)
|
.in("io_type", ioTypes)
|
.ne("id", task.getId()) // 排除当前任务本身
|
.eq("is_deleted", 0) // 排除已删除的任务
|
);
|
|
// 检查并自动结束已完成工作档的AGV任务
|
int originalCount = transportingTasks.size();
|
List<Task> validTransportingTasks = checkAndCompleteFinishedTasks(transportingTasks, taskType);
|
int completedCount = originalCount - validTransportingTasks.size();
|
|
if (completedCount > 0) {
|
log.info("定时任务:站点{}自动结束了{}个已完成工作档的AGV任务,剩余{}个正在搬运的任务",
|
staNo, completedCount, validTransportingTasks.size());
|
}
|
|
if (!validTransportingTasks.isEmpty()) {
|
List<Integer> transportingTaskIds = validTransportingTasks.stream()
|
.map(Task::getWrkNo)
|
.filter(wrkNo -> wrkNo != null) // 过滤掉null值
|
.collect(Collectors.toList());
|
// 记录被占用任务的详细信息
|
StringBuilder taskDetailInfo = new StringBuilder();
|
for (Task t : validTransportingTasks) {
|
if (taskDetailInfo.length() > 0) {
|
taskDetailInfo.append("; ");
|
}
|
String tDisplayTaskId = (t.getWrkNo() != null) ? String.valueOf(t.getWrkNo()) : String.valueOf(t.getId());
|
taskDetailInfo.append("任务").append(tDisplayTaskId)
|
.append("(wrk_no=").append(t.getWrkNo())
|
.append(",确认时间=").append(t.getPlcStrTime() != null ? t.getPlcStrTime().toString() : "未确认")
|
.append(",创建时间=").append(t.getAppeTime() != null ? t.getAppeTime().toString() : "未知")
|
.append(")");
|
}
|
log.info("定时任务:站点{}有{}个正在搬运的{}AGV任务(工作号:{}),跳过当前任务ID:{},下次将重新尝试。任务详情:{}",
|
staNo, validTransportingTasks.size(), taskType, transportingTaskIds, displayTaskId, taskDetailInfo.toString());
|
return false; // 返回false,表示未成功处理,下次会重新尝试
|
} else if (completedCount > 0) {
|
// 所有占用任务都已结束,站点已释放,可以继续处理当前任务
|
log.info("定时任务:站点{}的所有占用任务已自动结束,站点已释放,可以分配给当前任务ID:{}", staNo, task.getId());
|
}
|
}
|
|
log.info("定时任务:任务ID:{},站点{}检查通过,准备发送AGV命令", displayTaskId, staNo);
|
|
// 呼叫agv
|
String response = "";
|
boolean success = false;
|
String url = ApiInterfaceConstant.AGV_IP + ApiInterfaceConstant.AGV_CREATE_TASK_PATH;
|
String namespace = "";
|
switch (task.getIoType()) {
|
case 1:
|
case 10:
|
case 53:
|
case 57:
|
namespace = "入库";
|
break;
|
case 3:
|
namespace = "转移";
|
break;
|
case 101:
|
case 110:
|
case 103:
|
case 107:
|
namespace = "出库";
|
break;
|
default:
|
}
|
String body = getRequest(task,namespace);
|
|
// 获取当前重试次数
|
int currentRetryCount = getRetryCount(task);
|
int maxRetryCount = agvProperties.getCallRetry().getMaxRetryCount();
|
boolean retryEnabled = agvProperties.getCallRetry().isEnabled();
|
|
// 如果重试次数已达到最大值,跳过本次发送
|
if (retryEnabled && currentRetryCount >= maxRetryCount) {
|
// log.warn("{}呼叫agv搬运 - 任务ID:{}已达到最大重试次数({}),停止重试",
|
// namespace, task.getId(), maxRetryCount);
|
// 记录最终失败信息
|
task.setErrorTime(new Date());
|
task.setErrorMemo(String.format("AGV呼叫失败,已达到最大重试次数(%d次)", maxRetryCount));
|
taskService.updateById(task);
|
return true; // 已达到最大重试次数,不再重试,返回true表示已处理(虽然失败)
|
}
|
|
// 打印请求信息(包含重试次数)
|
// if (currentRetryCount > 0) {
|
// log.info("{}呼叫agv搬运(第{}次重试) - 请求地址:{}", namespace, currentRetryCount + 1, url);
|
// } else {
|
// log.info("{}呼叫agv搬运 - 请求地址:{}", namespace, url);
|
// }
|
// log.info("{}呼叫agv搬运 - 请求参数:{}", namespace, body);
|
|
boolean result = false; // 默认返回false,表示未成功处理
|
try {
|
// 使用仙工M4接口
|
response = new HttpHandler.Builder()
|
.setUri(ApiInterfaceConstant.AGV_IP)
|
.setPath(ApiInterfaceConstant.AGV_CREATE_TASK_PATH)
|
.setJson(body)
|
.build()
|
.doPost();
|
// 打印返回参数
|
log.info("{}呼叫agv搬运,请求参数「{}」 - 返回参数:{}", namespace,body, response);
|
|
// 检查响应是否为空
|
if (response == null || response.trim().isEmpty()) {
|
String errorMsg = "AGV接口返回为空";
|
log.warn("定时任务:{}呼叫agv搬运失败 - 任务ID:{},{}", namespace, displayTaskId, errorMsg);
|
handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
|
// 如果达到最大重试次数,返回true表示已处理(虽然失败)
|
// 否则返回false,让定时任务重新尝试
|
if (retryEnabled && currentRetryCount >= maxRetryCount) {
|
result = true; // 已达到最大重试次数,返回true表示已处理(虽然失败)
|
log.info("定时任务:任务ID:{},AGV呼叫失败且已达到最大重试次数({}次),标记为已处理,不再重试",
|
displayTaskId, maxRetryCount);
|
} else {
|
result = false; // 返回false,让定时任务重新尝试
|
log.info("定时任务:任务ID:{},下次将重新尝试发送AGV命令", displayTaskId);
|
}
|
} else {
|
// 尝试解析JSON响应,捕获JSON解析异常
|
JSONObject jsonObject = null;
|
try {
|
jsonObject = JSON.parseObject(response);
|
} catch (com.alibaba.fastjson.JSONException e) {
|
// JSON解析失败,响应可能不是有效的JSON格式(如"Server Error"等)
|
String errorMsg = String.format("AGV接口返回非JSON格式响应,响应内容:%s,解析错误:%s", response, e.getMessage());
|
log.error("定时任务:{}呼叫agv搬运失败 - 任务ID:{},{}", namespace, displayTaskId, errorMsg);
|
|
// 服务器错误时,标记站点为不可用,清空站点分配,不再为当前任务分配站点
|
try {
|
Integer siteNo = Integer.parseInt(staNo);
|
// 查询站点信息
|
List<BasDevp> basDevpList = basDevpMapper.selectList(new EntityWrapper<BasDevp>().eq("dev_no", siteNo));
|
if (basDevpList != null && !basDevpList.isEmpty()) {
|
BasDevp basDevp = basDevpList.get(0);
|
// 标记站点为不可用(设置canining='N')
|
basDevp.setCanining("N");
|
basDevpMapper.updateById(basDevp);
|
log.warn("定时任务:任务ID:{},AGV接口返回服务器错误,已标记站点{}为不可用(canining='N')",
|
displayTaskId, siteNo);
|
|
// 减少站点的入库任务数(之前分配站点时已经增加了in_qty)
|
basDevpMapper.decrementInQty(siteNo);
|
log.debug("定时任务:任务ID:{},站点{}的in_qty已减少", displayTaskId, siteNo);
|
}
|
} catch (Exception ex) {
|
log.error("定时任务:任务ID:{},标记站点{}为不可用时发生异常:{}", displayTaskId, staNo, ex.getMessage());
|
}
|
|
// 清空当前任务的站点分配
|
log.warn("定时任务:任务ID:{},AGV接口返回服务器错误,清空站点分配:{},不再为当前任务分配站点",
|
displayTaskId, staNo);
|
task.setStaNo(null);
|
taskService.updateById(task);
|
|
// 标记任务为失败,不再尝试分配
|
task.setErrorTime(new Date());
|
task.setErrorMemo(String.format("AGV接口返回服务器错误,站点已标记为不可用:%s", errorMsg));
|
taskService.updateById(task);
|
|
handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
|
// 服务器错误时,不再尝试分配,直接标记为已处理
|
result = true; // 返回true表示已处理(虽然失败),不再尝试分配
|
log.info("定时任务:任务ID:{},AGV呼叫失败(服务器错误),站点已标记为不可用,任务已标记为失败,不再尝试分配",
|
displayTaskId);
|
}
|
|
// 如果JSON解析成功,继续处理
|
if (jsonObject != null) {
|
Integer code = jsonObject.getInteger("code");
|
if (code != null && code.equals(200)) {
|
// 呼叫成功,清除重试次数和错误信息
|
success = true;
|
// 如果当前状态不是8,更新为8;如果已经是8,保持不变(可能是重试成功)
|
Long currentStatus = task.getWrkSts();
|
if (currentStatus == null || currentStatus != 8L) {
|
task.setWrkSts(8L);
|
log.info("定时任务:{}呼叫agv搬运成功 - 任务ID:{},状态从{}更新为8", namespace, displayTaskId, currentStatus);
|
} else {
|
log.info("定时任务:{}呼叫agv搬运成功(重试) - 任务ID:{},状态保持为8", namespace, displayTaskId);
|
}
|
task.setMemo(clearRetryInfo(task.getMemo())); // 清除重试信息
|
task.setErrorTime(null);
|
task.setErrorMemo(null);
|
taskService.updateById(task);
|
log.info("定时任务:{}呼叫agv搬运成功 - 任务ID:{}", namespace, displayTaskId);
|
result = true; // 返回true,表示成功处理
|
} else {
|
String message = jsonObject.getString("message");
|
String errorMsg = String.format("错误码:%s,错误信息:%s", code, message);
|
log.warn("定时任务:{}呼叫agv搬运失败 - 任务ID:{},{}", namespace, displayTaskId, errorMsg);
|
|
// 检查是否是站点或库位相关的错误,如果是,清空站点分配,让定时任务重新分配
|
boolean shouldReallocateSite = false;
|
if (message != null) {
|
String lowerMessage = message.toLowerCase();
|
// 库位不存在、站点不存在等错误,应该重新分配站点
|
if (lowerMessage.contains("库位不存在") ||
|
lowerMessage.contains("站点不存在") ||
|
lowerMessage.contains("位置不存在") ||
|
lowerMessage.contains("库位无效") ||
|
lowerMessage.contains("站点无效")) {
|
shouldReallocateSite = true;
|
}
|
}
|
|
if (shouldReallocateSite) {
|
// 清空站点分配,让定时任务重新分配站点
|
log.warn("定时任务:任务ID:{},AGV呼叫失败({}),清空站点分配:{},下次将重新分配站点",
|
displayTaskId, errorMsg, staNo);
|
task.setStaNo(null);
|
taskService.updateById(task);
|
}
|
|
handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
|
|
// 如果达到最大重试次数,返回true表示已处理(虽然失败)
|
// 否则返回false,让定时任务重新尝试(如果站点被清空,会重新分配站点;如果站点未清空,会重新发送AGV)
|
if (retryEnabled && currentRetryCount >= maxRetryCount) {
|
result = true; // 已达到最大重试次数,返回true表示已处理(虽然失败)
|
log.info("定时任务:任务ID:{},AGV呼叫失败且已达到最大重试次数({}次),标记为已处理,不再重试",
|
displayTaskId, maxRetryCount);
|
} else {
|
result = false; // 返回false,让定时任务重新尝试(重新分配站点或重新发送AGV)
|
if (shouldReallocateSite) {
|
log.info("定时任务:任务ID:{},站点已清空,下次将重新分配站点", displayTaskId);
|
} else {
|
log.info("定时任务:任务ID:{},下次将重新尝试发送AGV命令", displayTaskId);
|
}
|
}
|
}
|
}
|
}
|
} catch (Exception e) {
|
String errorMsg = "异常信息:" + e.getMessage();
|
log.error("定时任务:{}呼叫agv搬运异常 - 任务ID:{},请求地址:{},请求参数:{},{}",
|
namespace, displayTaskId, url, body, errorMsg, e);
|
handleCallFailure(task, namespace, errorMsg, retryEnabled, maxRetryCount, currentRetryCount);
|
// 如果达到最大重试次数,返回true表示已处理(虽然失败)
|
// 否则返回false,让定时任务重新尝试
|
if (retryEnabled && currentRetryCount >= maxRetryCount) {
|
result = true; // 已达到最大重试次数,返回true表示已处理(虽然失败)
|
log.info("定时任务:任务ID:{},AGV呼叫异常且已达到最大重试次数({}次),标记为已处理,不再重试",
|
displayTaskId, maxRetryCount);
|
} else {
|
result = false; // 返回false,让定时任务重新尝试
|
log.info("定时任务:任务ID:{},下次将重新尝试发送AGV命令", displayTaskId);
|
}
|
} finally {
|
try {
|
// 保存接口日志
|
apiLogService.save(
|
namespace + "呼叫agv搬运",
|
url,
|
null,
|
"127.0.0.1",
|
body,
|
response,
|
success
|
);
|
} catch (Exception e) {
|
// log.error(namespace + "呼叫agv保存接口日志异常:", e);
|
}
|
}
|
return result;
|
}
|
|
/**
|
* 处理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);
|
}
|
}
|
|
/**
|
* 检查并自动结束已完成工作档的AGV任务
|
* 如果任务对应的工作档已经完成(入库成功),则自动结束该AGV任务
|
* @param transportingTasks 正在搬运的任务列表
|
* @param taskTypeName 任务类型名称(用于日志)
|
* @return 仍然有效的正在搬运的任务列表(已完成的已被移除)
|
*/
|
@Transactional(rollbackFor = Exception.class)
|
public List<Task> checkAndCompleteFinishedTasks(List<Task> transportingTasks, String taskTypeName) {
|
if (transportingTasks == null || transportingTasks.isEmpty()) {
|
return transportingTasks;
|
}
|
|
List<Task> validTasks = new ArrayList<>();
|
Date now = new Date();
|
|
for (Task agvTask : transportingTasks) {
|
boolean isCompleted = false;
|
String reason = "";
|
|
// 检查工作档是否存在
|
WrkMast wrkMast = null;
|
if (agvTask.getWrkNo() != null) {
|
wrkMast = wrkMastService.selectOne(
|
new EntityWrapper<WrkMast>().eq("wrk_no", agvTask.getWrkNo())
|
);
|
}
|
|
// 检查历史档是否存在
|
WrkMastLog wrkMastLog = null;
|
if (agvTask.getWrkNo() != null) {
|
wrkMastLog = wrkMastLogService.selectOne(
|
new EntityWrapper<WrkMastLog>().eq("wrk_no", agvTask.getWrkNo())
|
);
|
}
|
// 如果通过wrk_no没找到,且有条码,则通过条码查询
|
if (wrkMastLog == null && !Cools.isEmpty(agvTask.getBarcode())) {
|
List<WrkMastLog> logList = wrkMastLogService.selectList(
|
new EntityWrapper<WrkMastLog>().eq("barcode", agvTask.getBarcode())
|
);
|
if (!logList.isEmpty()) {
|
wrkMastLog = logList.get(0);
|
}
|
}
|
|
// 如果工作档存在,检查是否已完成
|
if (wrkMast != null) {
|
Long wrkSts = wrkMast.getWrkSts();
|
Integer ioType = agvTask.getIoType();
|
|
if (wrkSts != null && ioType != null) {
|
// 入库任务:状态4(入库完成)或5(库存更新完成)
|
if ((ioType == 1 || ioType == 10 || ioType == 53 || ioType == 57) &&
|
(wrkSts == 4L || wrkSts == 5L)) {
|
isCompleted = true;
|
reason = String.format("工作档已完成,状态:%d", wrkSts);
|
}
|
// 出库任务:状态14(已出库未确认)或15(出库更新完成)
|
else if ((ioType == 101 || ioType == 110 || ioType == 103 || ioType == 107) &&
|
(wrkSts == 14L || wrkSts == 15L)) {
|
isCompleted = true;
|
reason = String.format("工作档已完成,状态:%d", wrkSts);
|
}
|
}
|
}
|
|
// 1. 如果工作档进入历史档,立即结束AGV任务(只要历史档存在就结束)
|
if (!isCompleted && wrkMastLog != null) {
|
isCompleted = true;
|
reason = String.format("工作档已转历史档,立即结束AGV任务,历史档状态:%d", wrkMastLog.getWrkSts());
|
}
|
|
// 如果已完成,更新AGV任务状态为完成
|
if (isCompleted) {
|
agvTask.setWrkSts(9L);
|
agvTask.setModiTime(now);
|
if (taskService.updateById(agvTask)) {
|
// taskId使用工作号(wrk_no),如果工作号为空则使用任务ID
|
String displayTaskId = (agvTask.getWrkNo() != null) ? String.valueOf(agvTask.getWrkNo()) : String.valueOf(agvTask.getId());
|
log.info("{},自动结束AGV任务,taskId:{},wrkNo:{},barcode:{},站点:{},释放站点供新任务使用",
|
reason, displayTaskId, agvTask.getWrkNo(), agvTask.getBarcode(), agvTask.getStaNo());
|
// 转移到历史表(会自动减少站点的in_qty)
|
try {
|
moveTaskToHistory(Collections.singletonList(agvTask));
|
log.info("自动结束AGV任务后已转移到历史表,taskId:{},站点:{}已释放", displayTaskId, agvTask.getStaNo());
|
} catch (Exception e) {
|
log.error("自动结束AGV任务后转移历史表失败,taskId:{}", displayTaskId, e);
|
}
|
} else {
|
log.error("自动结束AGV任务失败,更新任务状态失败,taskId:{}",
|
(agvTask.getWrkNo() != null) ? String.valueOf(agvTask.getWrkNo()) : String.valueOf(agvTask.getId()));
|
}
|
} else {
|
// 任务仍然有效,保留在列表中
|
// 记录任务仍然有效的原因(用于调试)
|
String displayTaskId = (agvTask.getWrkNo() != null) ? String.valueOf(agvTask.getWrkNo()) : String.valueOf(agvTask.getId());
|
if (wrkMast == null && wrkMastLog == null) {
|
log.debug("任务ID:{}(站点:{})仍然有效:工作档和历史档都不存在,可能工作档还未创建", displayTaskId, agvTask.getStaNo());
|
} else if (wrkMast != null) {
|
log.debug("任务ID:{}(站点:{})仍然有效:工作档状态={},任务类型={},未达到完成条件",
|
displayTaskId, agvTask.getStaNo(), wrkMast.getWrkSts(), agvTask.getIoType());
|
}
|
validTasks.add(agvTask);
|
}
|
}
|
|
return validTasks;
|
}
|
|
|
|
/**
|
* 从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;
|
}
|
|
/**
|
* 构造请求内容(仙工M4格式)
|
*/
|
public String getRequest(Task task, String nameSpace) {
|
JSONObject object = new JSONObject();
|
// taskId使用工作号(wrk_no),格式:T + 工作号
|
// 如果工作号为空,则使用任务ID作为备选
|
String taskIdValue = (task.getWrkNo() != null) ? "T" + task.getWrkNo() : "T" + task.getId();
|
object.put("taskId", taskIdValue);
|
// fromBin使用源库位编号(sourceLocNo),如果为空则使用源站点编号(sourceStaNo)作为备选
|
String fromBin = task.getSourceLocNo();
|
if (fromBin == null || fromBin.isEmpty()) {
|
fromBin = task.getSourceStaNo();
|
}
|
if (fromBin == null || fromBin.isEmpty() || "0".equals(fromBin)) {
|
// log.warn("任务{}的源库位和源站点都为空,使用默认值", task.getId());
|
fromBin = "0";
|
}
|
object.put("fromBin", fromBin);
|
// toBin使用目标站点编号
|
object.put("toBin", task.getStaNo());
|
// robotGroup从invWh字段获取,如果没有则根据站点编号判断
|
String robotGroup = task.getInvWh();
|
if (robotGroup == null || robotGroup.isEmpty()) {
|
robotGroup = determineRobotGroupByStation(task.getStaNo());
|
}
|
object.put("robotGroup", robotGroup);
|
// kind根据任务类型映射
|
String kind = "";
|
switch (nameSpace) {
|
case "入库":
|
// 判断是否为空托入库:ioType=10 或 emptyMk="Y"
|
if (task.getIoType() == 10 || "Y".equals(task.getEmptyMk())) {
|
kind = "空托入库";
|
} else {
|
kind = "实托入库";
|
}
|
break;
|
case "出库":
|
kind = "实托出库";
|
break;
|
case "转移":
|
kind = "货物转运";
|
break;
|
default:
|
kind = "货物转运";
|
}
|
object.put("kind", kind);
|
return object.toJSONString();
|
}
|
|
|
/**
|
* 为任务分配站点(定时任务中调用)
|
* 注意:只会分配一个站点,找到第一个符合条件的站点就分配并退出
|
* @param task 任务对象
|
* @return 如果无法分配站点,返回错误信息;如果分配成功,返回null并更新task的staNo
|
*/
|
@Transactional(rollbackFor = Exception.class)
|
public String allocateSiteForTask(Task task) {
|
// taskId使用工作号(wrk_no),如果工作号为空则使用任务ID
|
String displayTaskId = (task.getWrkNo() != null) ? String.valueOf(task.getWrkNo()) : String.valueOf(task.getId());
|
log.debug("开始为任务ID:{}分配站点,任务类型:{},机器人组:{}",
|
displayTaskId, task.getIoType(), task.getInvWh());
|
// 根据任务的invWh(机器人组)判断是东侧还是西侧
|
String robotGroup = task.getInvWh();
|
List<String> targetStations;
|
String groupKey;
|
|
if (robotGroup != null && robotGroup.equals(agvProperties.getRobotGroupEast())) {
|
// 东侧站点
|
targetStations = agvProperties.getEastStations();
|
groupKey = "east";
|
} else if (robotGroup != null && robotGroup.equals(agvProperties.getRobotGroupWest())) {
|
// 西侧站点
|
targetStations = agvProperties.getWestStations();
|
groupKey = "west";
|
} else {
|
// 默认使用东侧
|
targetStations = agvProperties.getEastStations();
|
groupKey = "east";
|
log.warn("任务ID:{}的机器人组{}未识别,使用默认东侧站点", displayTaskId, robotGroup);
|
}
|
|
if (targetStations.isEmpty()) {
|
String errorMsg = "没有可用的目标站点配置";
|
log.warn("任务ID:{},{}", displayTaskId, errorMsg);
|
return errorMsg;
|
}
|
|
// 将站点字符串列表转换为整数列表
|
List<Integer> siteIntList = targetStations.stream()
|
.map(Integer::parseInt)
|
.collect(Collectors.toList());
|
|
log.info("任务ID:{},{}站点组配置的站点:{},共{}个站点",
|
displayTaskId, groupKey.equals("east") ? agvProperties.getEastDisplayName() : agvProperties.getWestDisplayName(),
|
targetStations, targetStations.size());
|
|
// 判断能入站点(in_enable="Y"表示能入),排除dev_no=0的无效站点
|
List<BasDevp> allDevList = basDevpMapper.selectList(
|
new EntityWrapper<BasDevp>()
|
.in("dev_no", siteIntList)
|
.ne("dev_no", 0) // 排除dev_no=0的无效站点
|
);
|
// 记录所有站点的状态信息
|
StringBuilder siteStatusInfo = new StringBuilder();
|
for (BasDevp dev : allDevList) {
|
if (siteStatusInfo.length() > 0) {
|
siteStatusInfo.append("; ");
|
}
|
siteStatusInfo.append("站点").append(dev.getDevNo())
|
.append("(in_enable=").append(dev.getInEnable())
|
.append(",canining=").append(dev.getCanining()).append(")");
|
}
|
log.info("任务ID:{},候选站点状态:{}", displayTaskId, siteStatusInfo.toString());
|
|
List<Integer> sites = allDevList.stream()
|
.filter(dev -> "Y".equals(dev.getInEnable()))
|
.map(BasDevp::getDevNo)
|
.filter(devNo -> devNo != null && devNo != 0) // 再次过滤,确保不为null或0
|
.collect(Collectors.toList());
|
|
// 检查是否有站点不可用,如果有,说明需要在可用的站点之间平均分配
|
List<Integer> unavailableSites = new ArrayList<>(siteIntList);
|
unavailableSites.removeAll(sites);
|
if (!unavailableSites.isEmpty()) {
|
log.info("任务ID:{},{}站点组中有{}个站点不可用(in_enable!='Y'):{},将在{}个可用站点之间平均分配",
|
displayTaskId, groupKey.equals("east") ? agvProperties.getEastDisplayName() : agvProperties.getWestDisplayName(),
|
unavailableSites.size(), unavailableSites, sites.size());
|
}
|
|
if (sites.isEmpty()) {
|
String errorMsg = "没有能入站点(in_enable='Y')";
|
log.warn("任务ID:{},{},候选站点列表:{},站点状态:{}", displayTaskId, errorMsg, targetStations, siteStatusInfo.toString());
|
return errorMsg;
|
}
|
|
// 先检查站点配置(canining="Y"可入),排除dev_no=0的无效站点
|
List<BasDevp> devListWithConfig = basDevpMapper.selectList(new EntityWrapper<BasDevp>()
|
.in("dev_no", sites)
|
.eq("in_enable", "Y")
|
.eq("canining", "Y")
|
.eq("loading", "N")
|
.ne("dev_no", 0) // 排除dev_no=0的无效站点
|
);
|
|
if (devListWithConfig==null || devListWithConfig.isEmpty()) {
|
log.warn("任务ID:{}没有可入站点(站点未开通可入允许:canining='Y'),暂不分配站点,等待配置开通。能入站点列表:{}",
|
displayTaskId, sites);
|
return null;
|
}
|
|
// 先按规则排序(入库任务数排序)
|
devListWithConfig.sort(Comparator.comparing(BasDevp::getInQty));
|
// 根据任务类型确定要检查的io_type列表
|
Integer taskIoType = task.getIoType();
|
List<Integer> checkIoTypes = null;
|
String taskTypeName = "";
|
if (taskIoType != null) {
|
if (taskIoType < 100) {
|
// 入库任务:只检查入库类型(1, 10, 53, 57)
|
checkIoTypes = Arrays.asList(1, 10, 53, 57);
|
taskTypeName = "入库";
|
} else {
|
// 出库任务:只检查出库类型(101, 110, 103, 107)
|
checkIoTypes = Arrays.asList(101, 110, 103, 107);
|
taskTypeName = "出库";
|
}
|
}
|
|
// 先查询agv工作档中未被分配站点的站点
|
// 查询agv工作档中所有已分配站点的任务(sta_no不为空、不为空字符串、不为0)
|
final List<String> allocatedSiteNos;
|
if (checkIoTypes != null && !checkIoTypes.isEmpty()) {
|
List<Task> allocatedTasks = taskService.selectList(
|
new EntityWrapper<Task>()
|
.eq("task_type", "agv")
|
.in("wrk_sts", 7L, 8L) // 待呼叫AGV和正在搬运的任务
|
.in("io_type", checkIoTypes)
|
.isNotNull("sta_no")
|
.ne("sta_no", "")
|
.ne("sta_no", "0")
|
.andNew("(is_deleted = 0)")
|
);
|
// 获取已分配的站点编号列表
|
allocatedSiteNos = allocatedTasks.stream()
|
.map(Task::getStaNo)
|
.filter(staNo -> staNo != null && !staNo.isEmpty() && !staNo.equals("0"))
|
.distinct()
|
.collect(Collectors.toList());
|
} else {
|
allocatedSiteNos = new ArrayList<>();
|
}
|
|
// 从可用站点中筛选出未被分配的站点
|
List<BasDevp> unallocatedSites = devListWithConfig.stream()
|
.filter(dev -> {
|
String staNo = String.valueOf(dev.getDevNo());
|
return !allocatedSiteNos.contains(staNo);
|
})
|
.collect(Collectors.toList());
|
List<BasDevp> unallocatedSites2= new ArrayList<>();
|
for(int i=0;devListWithConfig.size()>i;i++){
|
unallocatedSites2.add(devListWithConfig.get(i));
|
}
|
// if(unallocatedSites==null || unallocatedSites.isEmpty()){
|
// unallocatedSites=unallocatedSites2;
|
//
|
// }
|
// 只使用未分配站点
|
if (unallocatedSites.isEmpty()) {
|
// 未分配站点为空:不分配站点
|
StringBuilder allocatedSitesInfo = new StringBuilder();
|
for (String staNo : allocatedSiteNos) {
|
if (allocatedSitesInfo.length() > 0) {
|
allocatedSitesInfo.append("; ");
|
}
|
allocatedSitesInfo.append("站点").append(staNo).append("已被分配");
|
}
|
log.warn("任务ID:{},所有可用站点都已被分配,暂不分配站点,等待下次定时任务再尝试。已分配站点:{}",
|
displayTaskId, allocatedSitesInfo.length() > 0 ? allocatedSitesInfo.toString() : "无详细信息");
|
return null; // 返回null,表示暂不分配,等待下次定时任务再尝试
|
}
|
|
// 存在未分配站点:根据配置的分配策略选择具体站点
|
// 先按规则排序(入库任务数排序)
|
unallocatedSites.sort(Comparator.comparing(BasDevp::getInQty));
|
|
// 根据配置选择分配策略,确定优先分配的站点顺序
|
List<BasDevp> orderedSites = new ArrayList<>();
|
String strategy = agvProperties.getSiteAllocation().getStrategy();
|
boolean enableRoundRobin = agvProperties.getSiteAllocation().isEnableRoundRobin();
|
|
// 记录是否使用轮询策略,以及轮询计数器(用于在成功分配站点后递增)
|
AtomicInteger roundRobinCounter = null;
|
int roundRobinStartIndex = 0;
|
if (unallocatedSites.size() > 1 && enableRoundRobin && "round-robin".equals(strategy)) {
|
// 轮询分配:先按轮询策略排序
|
roundRobinCounter = siteRoundRobinCounters.computeIfAbsent(groupKey, k -> new AtomicInteger(0));
|
roundRobinStartIndex = roundRobinCounter.get() % unallocatedSites.size(); // 获取当前索引,但不递增(成功分配后再递增)
|
// 将轮询选中的站点放在最前面
|
orderedSites.addAll(unallocatedSites.subList(roundRobinStartIndex, unallocatedSites.size()));
|
orderedSites.addAll(unallocatedSites.subList(0, roundRobinStartIndex));
|
log.info("任务ID:{},使用轮询分配策略,站点组:{},轮询起始索引:{},候选站点:{}(共{}个未分配站点)",
|
displayTaskId, groupKey, roundRobinStartIndex,
|
unallocatedSites.stream().map(d -> String.valueOf(d.getDevNo())).collect(Collectors.joining(",")),
|
unallocatedSites.size());
|
} else if (unallocatedSites.size() > 1 && enableRoundRobin && "random".equals(strategy)) {
|
// 随机分配:先随机排序未分配站点
|
List<BasDevp> shuffledSites = new ArrayList<>(unallocatedSites);
|
Collections.shuffle(shuffledSites);
|
orderedSites.addAll(shuffledSites);
|
log.info("任务ID:{},使用随机分配策略,候选站点:{}",
|
displayTaskId, unallocatedSites.stream().map(d -> String.valueOf(d.getDevNo())).collect(Collectors.joining(",")));
|
} else {
|
// 默认:按入库任务数排序(已经排序好了)
|
orderedSites = unallocatedSites;
|
}
|
|
// 既然已经筛选出了未分配站点,直接根据分配策略选择第一个站点即可
|
// 未分配站点在AGV工作档中都没有已分配的任务,可以直接分配
|
BasDevp selectedSite = orderedSites.get(0);
|
Integer endSite = selectedSite.getDevNo();
|
String staNo = String.valueOf(endSite);
|
|
log.info("任务ID:{},从{}个未分配站点中选择站点{}(入库任务数:{}),候选站点:{}",
|
displayTaskId, orderedSites.size(), staNo, selectedSite.getInQty(),
|
orderedSites.stream().map(d -> String.valueOf(d.getDevNo())).collect(Collectors.joining(",")));
|
|
// 如果使用轮询策略且成功分配站点,递增轮询计数器(确保下次从下一个站点开始)
|
if (roundRobinCounter != null && unallocatedSites.size() > 1) {
|
roundRobinCounter.getAndIncrement();
|
log.debug("任务ID:{}成功分配到站点{},轮询计数器已递增,下次将从下一个站点开始轮询", displayTaskId, staNo);
|
}
|
|
// 检查站点是否有效(不能为0或null)
|
if (endSite == null || endSite == 0) {
|
String errorMsg = String.format("分配的站点无效(dev_no=%s)", endSite);
|
log.error("任务ID:{},{}", displayTaskId, errorMsg);
|
return errorMsg;
|
}
|
|
// 入库暂存+1
|
basDevpMapper.incrementInQty(endSite);
|
|
// 更新任务的站点编号,并确保状态为7(待呼叫AGV)
|
task.setStaNo(String.valueOf(endSite));
|
if (task.getWrkSts() == null || task.getWrkSts() != 7L) {
|
task.setWrkSts(7L); // 确保状态为7(待呼叫AGV)
|
log.debug("任务ID:{}分配站点时,状态不是7,已更新为7(待呼叫AGV)", displayTaskId);
|
}
|
taskService.updateById(task);
|
|
log.info("任务ID:{}已分配站点:{},机器人组:{},任务类型:{}", displayTaskId, endSite, robotGroup, taskTypeName);
|
return null; // 分配成功,返回null
|
}
|
/**
|
* 根据站点编号判断机器人组
|
* @param staNo 站点编号
|
* @return 机器人组名称
|
*/
|
private String determineRobotGroupByStation(String staNo) {
|
if (staNo == null || staNo.isEmpty()) {
|
return agvProperties.getRobotGroupEast(); // 默认使用东侧机器人组
|
}
|
|
// 从配置中获取站点列表
|
Set<String> eastStations = new HashSet<>(agvProperties.getEastStations());
|
Set<String> westStations = new HashSet<>(agvProperties.getWestStations());
|
|
// 判断站点属于哪一侧
|
if (eastStations.contains(staNo)) {
|
return agvProperties.getRobotGroupEast(); // 东侧机器人
|
} else if (westStations.contains(staNo)) {
|
return agvProperties.getRobotGroupWest(); // 西侧机器人
|
} else {
|
log.warn("站点编号不在配置列表中,使用默认机器人组:{}", staNo);
|
return agvProperties.getRobotGroupEast(); // 默认使用东侧机器人组
|
}
|
}
|
|
|
/**
|
* 任务完成转历史 释放暂存点
|
*/
|
@Transactional(rollbackFor = Exception.class)
|
public void moveTaskToHistory(List<Task> taskList) {
|
|
// 写入历史表,保持ID一致
|
for (Task task : taskList) {
|
TaskLog log = new TaskLog();
|
BeanUtils.copyProperties(task, log);
|
// 保持ID一致,不设置为null
|
taskLogService.insert(log);
|
}
|
|
// 批量删除原任务
|
List<Long> taskIds = taskList.stream().map(Task::getId).collect(Collectors.toList());
|
taskService.delete(new EntityWrapper<Task>().in("id",taskIds));
|
|
// 批量更新暂存点状态
|
List<String> locOList = new ArrayList<>();
|
List<String> locFList = new ArrayList<>();
|
// 收集需要减少in_qty的站点(入库任务)
|
Set<Integer> sitesToDecrement = new HashSet<>();
|
for (Task task : taskList) {
|
String sourceStaNo = task.getSourceStaNo();
|
String staNo = task.getStaNo();
|
if (task.getIoType() == 3) {
|
locOList.add(sourceStaNo);
|
locFList.add(staNo);
|
} else if (task.getIoType() < 100) {
|
// 入库任务:减少目标站点的in_qty
|
locOList.add(sourceStaNo);
|
if (staNo != null && !staNo.isEmpty()) {
|
try {
|
Integer siteNo = Integer.parseInt(staNo);
|
if (siteNo != null && siteNo > 0) {
|
sitesToDecrement.add(siteNo);
|
}
|
} catch (NumberFormatException e) {
|
log.warn("任务ID:{}的站点编号格式错误:{},跳过减少in_qty", task.getId(), staNo);
|
}
|
}
|
} else {
|
locFList.add(staNo);
|
}
|
}
|
|
// 减少站点的入库任务数(in_qty)
|
for (Integer siteNo : sitesToDecrement) {
|
try {
|
basDevpMapper.decrementInQty(siteNo);
|
log.debug("任务转移到历史表,站点{}的in_qty已减少", siteNo);
|
} catch (Exception e) {
|
log.error("任务转移到历史表,减少站点{}的in_qty失败", siteNo, e);
|
}
|
}
|
|
if (!locOList.isEmpty()) {
|
basStationMapper.updateLocStsBatch(locOList, "O");
|
}
|
if (!locFList.isEmpty()) {
|
basStationMapper.updateLocStsBatch(locFList, "F");
|
}
|
|
log.info("agv任务档转历史成功:{}", taskIds);
|
}
|
@Transactional(rollbackFor = Exception.class)
|
public void moveTaskToHistory(Task agvTask) {
|
moveTaskToHistory(Collections.singletonList(agvTask));
|
|
}
|
/**
|
* 货物到达出库口,生成agv任务
|
*/
|
public void createAgvOutTasks(List<String> sites) {
|
|
// 获取到可用出库站点的任务
|
List<WrkMast> wrkMastList = wrkMastMapper.selectList(new EntityWrapper<WrkMast>().eq("call_agv", 1).in("sta_no",sites));
|
|
for(WrkMast wrkMast:wrkMastList) {
|
// todo 计算agv目标暂存位
|
int endSite = 2004;
|
|
// 插入agv任务
|
Task task = new Task(wrkMast.getWrkNo(), 7L, wrkMast.getIoType(), String.valueOf(wrkMast.getStaNo()), String.valueOf(endSite), null, wrkMast.getBarcode());
|
taskService.insert(task);
|
// 更新任务档agv搬运标识
|
wrkMast.setCallAgv(2);
|
wrkMastMapper.updateById(wrkMast);
|
|
// 更新暂存位状态 S.入库预约
|
basStationMapper.updateLocStsBatch( Collections.singletonList(String.valueOf(endSite)), "S");
|
}
|
}
|
|
/**
|
* 取消AGV任务(仙工M4接口)
|
* @param task 任务对象
|
* @return 是否成功
|
*/
|
public boolean cancelAgvTask(Task task) {
|
if (!agvProperties.isSendTask()) {
|
return false;
|
}
|
|
if (task == null || task.getId() == null) {
|
log.error("取消AGV任务失败:任务或任务ID为空");
|
return false;
|
}
|
|
String response = "";
|
boolean success = false;
|
String url = ApiInterfaceConstant.AGV_IP + ApiInterfaceConstant.AGV_CANCEL_TASK_PATH;
|
String namespace = "";
|
String kind = "";
|
|
// 根据任务类型确定kind和namespace
|
switch (task.getIoType()) {
|
case 1:
|
case 10:
|
case 53:
|
case 57:
|
namespace = "入库";
|
kind = "实托入库";
|
break;
|
case 3:
|
namespace = "转移";
|
kind = "货物转运";
|
break;
|
case 101:
|
case 110:
|
case 103:
|
case 107:
|
namespace = "出库";
|
kind = "实托出库";
|
break;
|
default:
|
kind = "货物转运";
|
}
|
|
// 构造取消任务请求
|
JSONObject cancelRequest = new JSONObject();
|
// taskId使用工作号(wrk_no),格式:T + 工作号
|
// 如果工作号为空,则使用任务ID作为备选
|
String taskIdValue = (task.getWrkNo() != null) ? "T" + task.getWrkNo() : "T" + task.getId();
|
cancelRequest.put("taskId", taskIdValue);
|
cancelRequest.put("kind", kind);
|
String body = cancelRequest.toJSONString();
|
|
try {
|
response = new HttpHandler.Builder()
|
.setUri(ApiInterfaceConstant.AGV_IP)
|
.setPath(ApiInterfaceConstant.AGV_CANCEL_TASK_PATH)
|
.setJson(body)
|
.build()
|
.doPost();
|
|
JSONObject jsonObject = JSON.parseObject(response);
|
if (jsonObject.getInteger("code") != null && jsonObject.getInteger("code").equals(200)) {
|
success = true;
|
// taskId使用工作号(wrk_no),如果工作号为空则使用任务ID
|
String displayTaskId = (task.getWrkNo() != null) ? String.valueOf(task.getWrkNo()) : String.valueOf(task.getId());
|
log.info(namespace + "取消AGV任务成功:{}", displayTaskId);
|
} else {
|
log.error(namespace + "取消AGV任务失败!!!url:{};request:{};response:{}", url, body, response);
|
}
|
} catch (Exception e) {
|
log.error(namespace + "取消AGV任务异常", e);
|
} finally {
|
try {
|
// 保存接口日志
|
apiLogService.save(
|
namespace + "取消AGV任务",
|
url,
|
null,
|
"127.0.0.1",
|
body,
|
response,
|
success
|
);
|
} catch (Exception e) {
|
log.error(namespace + "取消AGV任务保存接口日志异常:", e);
|
}
|
}
|
|
return success;
|
}
|
}
|