You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
945 lines
36 KiB
945 lines
36 KiB
package com.gaotao.modules.dashboard.task;
|
|
|
|
import com.beust.ah.A;
|
|
import com.fasterxml.jackson.databind.JsonNode;
|
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
import com.gaotao.common.utils.HttpUtils;
|
|
import com.gaotao.modules.automatedWarehouse.entity.tusk.AgvStatus;
|
|
import com.gaotao.modules.automatedWarehouse.entity.tusk.TuskResponse;
|
|
import com.gaotao.modules.automatedWarehouse.service.TuskClientService;
|
|
import com.gaotao.modules.dashboard.service.DashboardWebSocketService;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.scheduling.annotation.Scheduled;
|
|
import org.springframework.stereotype.Component;
|
|
|
|
import java.util.*;
|
|
|
|
/**
|
|
* 看板数据推送定时任务
|
|
*
|
|
* <p><b>功能说明:</b></p>
|
|
* <ul>
|
|
* <li>定时从WCS Board API获取最新数据</li>
|
|
* <li>检测到数据变更后通过WebSocket推送到前端</li>
|
|
* <li>相比轮询,只在数据变更时推送,减少无效传输</li>
|
|
* </ul>
|
|
*
|
|
* <p><b>推送策略:</b></p>
|
|
* <ul>
|
|
* <li>每5秒检查一次机械臂拣选数据</li>
|
|
* <li>数据有变化时立即推送</li>
|
|
* <li>推送失败时记录日志,不影响下次推送</li>
|
|
* </ul>
|
|
*
|
|
* @author System
|
|
* @since 2025-01-23
|
|
*/
|
|
@Slf4j
|
|
@Component
|
|
public class DashboardPushTask {
|
|
|
|
@Autowired
|
|
private DashboardWebSocketService webSocketService;
|
|
|
|
@Value("${custom.wcs-board-api}")
|
|
private String wcsBoardApi;
|
|
|
|
// 看板推送任务开关配置(所有看板共用一个开关)
|
|
@Value("${dashboard.push.enabled:true}")
|
|
private boolean dashboardPushEnabled;
|
|
|
|
/**
|
|
* 上次推送的数据哈希值(用于检测数据变更)
|
|
*/
|
|
private Map<String, Integer> lastDataHash = new HashMap<>();
|
|
|
|
@Autowired
|
|
private com.gaotao.modules.dashboard.dao.DashboardDao dashboardDao;
|
|
|
|
@Autowired(required = false)
|
|
private TuskClientService tuskClientService;
|
|
|
|
/**
|
|
* 每5秒检查机械臂拣选数据并推送
|
|
*
|
|
* <p>注意:这个间隔可以根据实际需求调整</p>
|
|
* <ul>
|
|
* <li>如果数据变化频繁,可以缩短间隔(如2-3秒)</li>
|
|
* <li>如果数据变化不频繁,可以延长间隔(如10-15秒)</li>
|
|
* </ul>
|
|
*
|
|
* <p><b>配置开关:</b></p>
|
|
* <ul>
|
|
* <li>dashboard.push.enabled - 看板推送总开关</li>
|
|
* </ul>
|
|
*/
|
|
@Scheduled(fixedRate = 5000)
|
|
public void pushRobotPickingData() {
|
|
// 检查总开关
|
|
if (!dashboardPushEnabled) {
|
|
log.trace("看板推送已禁用");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 从WCS Board API获取机械臂拣选数据
|
|
Map<String, Object> data = getRobotPickingDataFromWcs();
|
|
|
|
// 如果返回null,转换为空数据(避免前端显示过期数据)
|
|
if (data == null) {
|
|
data = createEmptyData();
|
|
}
|
|
|
|
// 计算数据哈希值
|
|
int currentHash = data.hashCode();
|
|
webSocketService.pushRobotPickingData(data);
|
|
lastDataHash.put("robot-picking", currentHash);
|
|
|
|
} catch (Exception e) {
|
|
log.error("推送机械臂拣选数据失败,推送空数据清空前端列表: {}", e.getMessage(), e);
|
|
// 异常时推送空数据,避免前端显示过期数据
|
|
try {
|
|
Map<String, Object> emptyData = createEmptyData();
|
|
webSocketService.pushRobotPickingData(emptyData);
|
|
lastDataHash.put("robot-picking", emptyData.hashCode());
|
|
} catch (Exception ex) {
|
|
log.error("推送空数据失败: {}", ex.getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 从WCS Board API获取机械臂拣选数据
|
|
*
|
|
* @return 机械臂拣选数据
|
|
*/
|
|
private Map<String, Object> getRobotPickingDataFromWcs() {
|
|
try {
|
|
// 调用WCS Board API
|
|
String url = wcsBoardApi + "WmsDashboard/auto-sorting-info";
|
|
String wcsResponse = HttpUtils.doGet(url, null, null);
|
|
|
|
// 解析JSON数据
|
|
ObjectMapper mapper = new ObjectMapper();
|
|
JsonNode rootNode = mapper.readTree(wcsResponse);
|
|
|
|
// 检查返回码
|
|
int resCode = rootNode.get("resCode").asInt();
|
|
if (resCode != 200) {
|
|
log.warn("WCS API返回错误: code={}", resCode);
|
|
return null;
|
|
}
|
|
|
|
// 获取sortingStations数组
|
|
JsonNode resData = rootNode.get("resData");
|
|
if (resData == null || !resData.has("sortingStations")) {
|
|
log.warn("WCS返回数据中没有sortingStations");
|
|
return createEmptyData();
|
|
}
|
|
|
|
JsonNode sortingStations = resData.get("sortingStations");
|
|
|
|
// 按照sortingStation分类处理数据
|
|
List<Map<String, Object>> containerList = new ArrayList<>(); // 1071-周转箱
|
|
List<Map<String, Object>> materialList = new ArrayList<>(); // 1060-原材
|
|
|
|
for (JsonNode station : sortingStations) {
|
|
String sortingStation = station.get("sortingStation").asText();
|
|
JsonNode materials = station.get("materials");
|
|
|
|
if (materials != null && materials.isArray()) {
|
|
for (JsonNode material : materials) {
|
|
Map<String, Object> item = convertMaterialToItem(material, sortingStation);
|
|
|
|
// 根据工作站分类
|
|
if ("1071".equals(sortingStation)) {
|
|
containerList.add(item);
|
|
} else if ("1060".equals(sortingStation)) {
|
|
materialList.add(item);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 构造返回数据
|
|
Map<String, Object> resultData = new HashMap<>();
|
|
resultData.put("containerList", containerList);
|
|
resultData.put("materialList", materialList);
|
|
|
|
return resultData;
|
|
|
|
} catch (Exception e) {
|
|
log.error("从WCS获取机械臂拣选数据失败: {}", e.getMessage());
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 将WCS返回的material数据转换为前端需要的格式
|
|
*
|
|
* @param material WCS返回的物料数据
|
|
* @param sortingStation 工作站编号
|
|
* @return 前端表格数据格式
|
|
*/
|
|
private Map<String, Object> convertMaterialToItem(JsonNode material, String sortingStation) {
|
|
Map<String, Object> item = new HashMap<>();
|
|
|
|
// 拣选托盘码 (源托盘)
|
|
item.put("pickingBatchNo", material.has("sourcePalletCode") ?
|
|
material.get("sourcePalletCode").asText() : "");
|
|
|
|
// 拣选物料名称 (SKU)
|
|
item.put("pickingMaterialName", material.has("sku") ?
|
|
material.get("sku").asText() : "");
|
|
|
|
// RFID条码
|
|
item.put("rfidBarcode", material.has("rfidBarcode") ?
|
|
material.get("rfidBarcode").asText() : "");
|
|
|
|
// 状态 (根据isCompleted判断)
|
|
boolean isCompleted = material.has("isCompleted") && material.get("isCompleted").asBoolean();
|
|
item.put("status", isCompleted ? "完成" : "等待分拣");
|
|
|
|
// 存放托盘码 (目标托盘)
|
|
item.put("storageBatchNo", material.has("targetPalletCode") ?
|
|
material.get("targetPalletCode").asText() : "");
|
|
|
|
// 存放位置 (工作站编号)
|
|
item.put("storageLocation", material.has("sortingStation") ?
|
|
material.get("sortingStation").asText() : sortingStation);
|
|
|
|
return item;
|
|
}
|
|
|
|
/**
|
|
* 创建空数据
|
|
*
|
|
* @return 空的数据结构
|
|
*/
|
|
private Map<String, Object> createEmptyData() {
|
|
Map<String, Object> emptyData = new HashMap<>();
|
|
emptyData.put("containerList", new ArrayList<>());
|
|
emptyData.put("materialList", new ArrayList<>());
|
|
return emptyData;
|
|
}
|
|
|
|
/**
|
|
* 判断数据是否为空
|
|
*
|
|
* @param data 待检查的数据
|
|
* @return true=数据为空,false=数据不为空
|
|
*/
|
|
private boolean isDataEmpty(Map<String, Object> data) {
|
|
if (data == null || data.isEmpty()) {
|
|
return true;
|
|
}
|
|
|
|
List<?> containerList = (List<?>) data.get("containerList");
|
|
List<?> materialList = (List<?>) data.get("materialList");
|
|
|
|
return (containerList == null || containerList.isEmpty())
|
|
&& (materialList == null || materialList.isEmpty());
|
|
}
|
|
|
|
/**
|
|
* 每5秒检查分切区看板数据并推送
|
|
*
|
|
* <p><b>数据来源:</b></p>
|
|
* <ul>
|
|
* <li>view_board_slitting_assist_arm - 助力臂区数据</li>
|
|
* <li>view_board_slitting_inbound - 分切入库区数据</li>
|
|
* </ul>
|
|
*/
|
|
@Scheduled(fixedRate = 5000)
|
|
public void pushSlittingBoardData() {
|
|
// 检查总开关
|
|
if (!dashboardPushEnabled) {
|
|
log.trace("看板推送已禁用");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 从数据库视图获取分切区数据
|
|
Map<String, Object> data = getSlittingBoardDataFromDb();
|
|
|
|
// 如果返回null,转换为空数据
|
|
if (data == null) {
|
|
data = createEmptySlittingData();
|
|
}
|
|
|
|
// 计算数据哈希值
|
|
int currentHash = data.hashCode();
|
|
webSocketService.pushSlittingBoardData(data);
|
|
lastDataHash.put("slitting-board", currentHash);
|
|
|
|
} catch (Exception e) {
|
|
log.error("推送分切区看板数据失败,推送空数据清空前端列表: {}", e.getMessage(), e);
|
|
// 异常时推送空数据,避免前端显示过期数据
|
|
try {
|
|
Map<String, Object> emptyData = createEmptySlittingData();
|
|
webSocketService.pushSlittingBoardData(emptyData);
|
|
lastDataHash.put("slitting-board", emptyData.hashCode());
|
|
} catch (Exception ex) {
|
|
log.error("推送空数据失败: {}", ex.getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 从数据库视图获取分切区看板数据
|
|
*
|
|
* @return 分切区看板数据
|
|
*/
|
|
private Map<String, Object> getSlittingBoardDataFromDb() {
|
|
try {
|
|
// 查询助力臂区数据
|
|
List<Map<String, Object>> assistArmList = dashboardDao.querySlittingAssistArmData();
|
|
log.debug("查询到助力臂区数据: {}条", assistArmList != null ? assistArmList.size() : 0);
|
|
|
|
// 查询分切入库区数据
|
|
List<Map<String, Object>> inboundList = dashboardDao.querySlittingInboundData();
|
|
log.debug("查询到分切入库区数据: {}条", inboundList != null ? inboundList.size() : 0);
|
|
|
|
// 构造返回数据
|
|
Map<String, Object> resultData = new HashMap<>();
|
|
resultData.put("assistArmList", assistArmList != null ? assistArmList : new ArrayList<>());
|
|
resultData.put("slittingInboundList", inboundList != null ? inboundList : new ArrayList<>());
|
|
|
|
return resultData;
|
|
|
|
} catch (Exception e) {
|
|
log.error("从数据库获取分切区看板数据失败: {}", e.getMessage(), e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 创建空的分切区数据
|
|
*
|
|
* @return 空的分切区数据结构
|
|
*/
|
|
private Map<String, Object> createEmptySlittingData() {
|
|
Map<String, Object> emptyData = new HashMap<>();
|
|
emptyData.put("assistArmList", new ArrayList<>());
|
|
emptyData.put("slittingInboundList", new ArrayList<>());
|
|
return emptyData;
|
|
}
|
|
|
|
/**
|
|
* 判断分切区数据是否为空
|
|
*
|
|
* @param data 待检查的数据
|
|
* @return true=数据为空,false=数据不为空
|
|
*/
|
|
private boolean isSlittingDataEmpty(Map<String, Object> data) {
|
|
if (data == null || data.isEmpty()) {
|
|
return true;
|
|
}
|
|
|
|
List<?> assistArmList = (List<?>) data.get("assistArmList");
|
|
List<?> inboundList = (List<?>) data.get("slittingInboundList");
|
|
|
|
return (assistArmList == null || assistArmList.isEmpty())
|
|
&& (inboundList == null || inboundList.isEmpty());
|
|
}
|
|
|
|
/**
|
|
* 每5秒检查智能立体仓库看板数据并推送
|
|
*
|
|
* <p><b>数据来源:</b></p>
|
|
* <ul>
|
|
* <li>任务统计数据 - queryWarehouseTaskStats</li>
|
|
* <li>库位利用率 - queryWarehouseStorageUtilization</li>
|
|
* <li>机器人状态 - queryWarehouseRobotStatus</li>
|
|
* <li>AGV状态 - queryWarehouseAgvStatus</li>
|
|
* <li>领料申请单统计 - queryWarehouseMaterialRequestStats</li>
|
|
* <li>发货统计 - queryWarehouseShipmentStats</li>
|
|
* </ul>
|
|
*/
|
|
@Scheduled(fixedRate = 5000)
|
|
public void pushWarehouse3dBoardData() {
|
|
// 检查总开关
|
|
if (!dashboardPushEnabled) {
|
|
log.trace("看板推送已禁用");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 从数据库获取立体仓库看板数据
|
|
Map<String, Object> data = getWarehouse3dBoardDataFromDb();
|
|
|
|
// 如果返回null,转换为空数据
|
|
if (data == null) {
|
|
data = createEmptyWarehouse3dData();
|
|
}
|
|
|
|
// 计算数据哈希值
|
|
int currentHash = data.hashCode();
|
|
webSocketService.pushWarehouse3dBoardData(data);
|
|
lastDataHash.put("warehouse-3d", currentHash);
|
|
|
|
} catch (Exception e) {
|
|
log.error("推送智能立体仓库看板数据失败,推送空数据: {}", e.getMessage(), e);
|
|
// 异常时推送空数据,避免前端显示过期数据
|
|
try {
|
|
Map<String, Object> emptyData = createEmptyWarehouse3dData();
|
|
webSocketService.pushWarehouse3dBoardData(emptyData);
|
|
lastDataHash.put("warehouse-3d", emptyData.hashCode());
|
|
} catch (Exception ex) {
|
|
log.error("推送空数据失败: {}", ex.getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 从数据库获取智能立体仓库看板数据
|
|
*
|
|
* @return 智能立体仓库看板数据
|
|
*/
|
|
private Map<String, Object> getWarehouse3dBoardDataFromDb() {
|
|
try {
|
|
log.debug("开始从数据库获取智能立体仓库看板数据");
|
|
|
|
// 查询任务统计数据
|
|
Map<String, Object> taskStats = dashboardDao.queryWarehouseTaskStats();
|
|
log.debug("任务统计数据: {}", taskStats);
|
|
|
|
// 查询库位利用率数据(从WCS Board API获取)
|
|
Map<String, Object> storageUtilization = getInventoryStatsFromWcs();
|
|
log.debug("库位利用率数据: {}", storageUtilization);
|
|
|
|
// 查询机器人状态数据
|
|
//List<Map<String, Object>> robotStatus = dashboardDao.queryWarehouseRobotStatus();
|
|
List<Map<String, Object>> robotStatus = new ArrayList<>();
|
|
log.debug("查询到机器人状态数据: {}条", robotStatus != null ? robotStatus.size() : 0);
|
|
|
|
// 查询AGV状态数据(从TUSK系统获取)
|
|
List<Map<String, Object>> agvStatus = getAgvStatusFromTusk();
|
|
log.debug("查询到AGV状态数据: {}条", agvStatus != null ? agvStatus.size() : 0);
|
|
|
|
// 查询领料申请单统计
|
|
//Map<String, Object> materialRequestStats = dashboardDao.queryWarehouseMaterialRequestStats();
|
|
Map<String, Object> materialRequestStats = new HashMap<>();
|
|
log.debug("领料申请单统计: {}", materialRequestStats);
|
|
|
|
// 查询发货统计
|
|
//Map<String, Object> shipmentStats = dashboardDao.queryWarehouseShipmentStats();
|
|
Map<String, Object> shipmentStats = new HashMap<>();
|
|
log.debug("发货统计: {}", shipmentStats);
|
|
|
|
// 查询库存趋势数据
|
|
List<Map<String, Object>> rawMaterialTrend = dashboardDao.queryRawMaterialInventoryTrend();
|
|
log.debug("查询到原材料库存趋势数据: {}条", rawMaterialTrend != null ? rawMaterialTrend.size() : 0);
|
|
|
|
List<Map<String, Object>> specifiedMaterialTrend = dashboardDao.querySpecifiedMaterialInventoryTrend();
|
|
log.debug("查询到规格料库存趋势数据: {}条", specifiedMaterialTrend != null ? specifiedMaterialTrend.size() : 0);
|
|
|
|
List<Map<String, Object>> finishedGoodsTrend = dashboardDao.queryFinishedGoodsInventoryTrend();
|
|
log.debug("查询到产成品库存趋势数据: {}条", finishedGoodsTrend != null ? finishedGoodsTrend.size() : 0);
|
|
|
|
// 构造返回数据
|
|
Map<String, Object> resultData = new HashMap<>();
|
|
resultData.put("taskData", taskStats != null ? taskStats : new HashMap<>());
|
|
resultData.put("storageData", storageUtilization != null ? storageUtilization : new HashMap<>());
|
|
resultData.put("robotData", robotStatus != null ? robotStatus : new ArrayList<>());
|
|
resultData.put("agvData", agvStatus != null ? agvStatus : new ArrayList<>());
|
|
resultData.put("materialRequestData", materialRequestStats != null ? materialRequestStats : new HashMap<>());
|
|
resultData.put("shipmentData", shipmentStats != null ? shipmentStats : new HashMap<>());
|
|
|
|
// 添加库存趋势数据
|
|
resultData.put("rawMaterialTrend", rawMaterialTrend != null ? rawMaterialTrend : new ArrayList<>());
|
|
resultData.put("specifiedMaterialTrend", specifiedMaterialTrend != null ? specifiedMaterialTrend : new ArrayList<>());
|
|
resultData.put("finishedGoodsTrend", finishedGoodsTrend != null ? finishedGoodsTrend : new ArrayList<>());
|
|
|
|
log.debug("智能立体仓库看板数据组装完成");
|
|
return resultData;
|
|
|
|
} catch (Exception e) {
|
|
log.error("从数据库获取智能立体仓库看板数据失败: {}", e.getMessage(), e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 从WCS Board API获取库存统计数据
|
|
*
|
|
* <p><b>API说明:</b></p>
|
|
* <ul>
|
|
* <li>接口地址: /api/WmsDashboard/inventory-stats</li>
|
|
* <li>返回托盘库存统计:平托、框架托、钢托</li>
|
|
* <li>返回空容器库存数量</li>
|
|
* </ul>
|
|
*
|
|
* <p><b>数据转换:</b></p>
|
|
* <ul>
|
|
* <li>flatPallet(平托) -> flatPallet</li>
|
|
* <li>framePallet(框架托) -> guardPallet(围挡托盘)</li>
|
|
* <li>steelPallet(钢托) -> steelPallet</li>
|
|
* <li>emptyContainerInventory -> otherPallet(其他)</li>
|
|
* </ul>
|
|
*
|
|
* @return 库存统计数据
|
|
*/
|
|
private Map<String, Object> getInventoryStatsFromWcs() {
|
|
try {
|
|
// 调用WCS Board API
|
|
String url = wcsBoardApi + "WmsDashboard/inventory-stats";
|
|
log.debug("调用WCS库存统计API: {}", url);
|
|
|
|
String wcsResponse = HttpUtils.doGet(url, null, null);
|
|
log.debug("WCS API返回数据: {}", wcsResponse);
|
|
|
|
// 解析JSON数据
|
|
ObjectMapper mapper = new ObjectMapper();
|
|
JsonNode rootNode = mapper.readTree(wcsResponse);
|
|
|
|
// 检查返回码
|
|
int resCode = rootNode.get("resCode").asInt();
|
|
if (resCode != 200) {
|
|
String resMsg = rootNode.has("resMsg") ? rootNode.get("resMsg").asText() : "未知错误";
|
|
log.warn("WCS API返回错误: code={}, msg={}", resCode, resMsg);
|
|
return createEmptyStorageData();
|
|
}
|
|
|
|
// 获取resData数据
|
|
JsonNode resData = rootNode.get("resData");
|
|
if (resData == null || resData.isNull()) {
|
|
log.warn("WCS返回数据中没有resData");
|
|
return createEmptyStorageData();
|
|
}
|
|
|
|
// 获取materialInventory(物料库存)
|
|
JsonNode materialInventory = resData.get("materialInventory");
|
|
if (materialInventory == null || materialInventory.isNull()) {
|
|
log.warn("WCS返回数据中没有materialInventory");
|
|
return createEmptyStorageData();
|
|
}
|
|
|
|
// 提取各类托盘数量
|
|
int flatPallet = materialInventory.has("flatPallet") ?
|
|
materialInventory.get("flatPallet").asInt() : 0;
|
|
int framePallet = materialInventory.has("framePallet") ?
|
|
materialInventory.get("framePallet").asInt() : 0;
|
|
int steelPallet = materialInventory.has("steelPallet") ?
|
|
materialInventory.get("steelPallet").asInt() : 0;
|
|
|
|
// 提取空容器库存
|
|
int emptyContainer = resData.has("emptyContainerInventory") ?
|
|
resData.get("emptyContainerInventory").asInt() : 0;
|
|
|
|
// 计算总使用库位和利用率
|
|
int usedSlots = flatPallet + framePallet + steelPallet;
|
|
int totalSlots = 1960; // 固定值
|
|
double utilizationRate = totalSlots > 0 ?
|
|
Math.round((double) usedSlots / totalSlots * 1000.0) / 10.0 : 0.0;
|
|
|
|
// 构造返回数据
|
|
Map<String, Object> storageData = new HashMap<>();
|
|
storageData.put("totalSlots", totalSlots); // 总库位数
|
|
storageData.put("usedSlots", usedSlots); // 已使用库位数
|
|
storageData.put("utilizationRate", utilizationRate); // 利用率
|
|
storageData.put("flatPallet", flatPallet); // 平托
|
|
storageData.put("guardPallet", framePallet); // 围挡托盘(对应framePallet)
|
|
storageData.put("steelPallet", steelPallet); // 钢托盘
|
|
storageData.put("otherPallet", emptyContainer); // 其他(空容器)
|
|
|
|
log.info("库存统计数据获取成功 - 平托:{}, 框架托:{}, 钢托:{}, 空容器:{}, 总使用:{}, 利用率:{}%",
|
|
flatPallet, framePallet, steelPallet, emptyContainer, usedSlots, utilizationRate);
|
|
|
|
return storageData;
|
|
|
|
} catch (Exception e) {
|
|
log.error("从WCS获取库存统计数据失败: {}", e.getMessage(), e);
|
|
return createEmptyStorageData();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 创建空的库存统计数据
|
|
*
|
|
* @return 空的库存统计数据结构
|
|
*/
|
|
private Map<String, Object> createEmptyStorageData() {
|
|
Map<String, Object> emptyData = new HashMap<>();
|
|
emptyData.put("totalSlots", 1960);
|
|
emptyData.put("usedSlots", 0);
|
|
emptyData.put("utilizationRate", 0.0);
|
|
emptyData.put("flatPallet", 0);
|
|
emptyData.put("guardPallet", 0);
|
|
emptyData.put("steelPallet", 0);
|
|
emptyData.put("otherPallet", 0);
|
|
return emptyData;
|
|
}
|
|
|
|
/**
|
|
* 创建空的智能立体仓库数据
|
|
*
|
|
* @return 空的智能立体仓库数据结构
|
|
*/
|
|
private Map<String, Object> createEmptyWarehouse3dData() {
|
|
Map<String, Object> emptyData = new HashMap<>();
|
|
emptyData.put("taskData", new HashMap<>());
|
|
emptyData.put("storageData", new HashMap<>());
|
|
emptyData.put("robotData", new ArrayList<>());
|
|
emptyData.put("agvData", new ArrayList<>());
|
|
emptyData.put("materialRequestData", new HashMap<>());
|
|
emptyData.put("shipmentData", new HashMap<>());
|
|
return emptyData;
|
|
}
|
|
|
|
/**
|
|
* 判断智能立体仓库数据是否为空
|
|
*
|
|
* @param data 待检查的数据
|
|
* @return true=数据为空,false=数据不为空
|
|
*/
|
|
private boolean isWarehouse3dDataEmpty(Map<String, Object> data) {
|
|
if (data == null || data.isEmpty()) {
|
|
return true;
|
|
}
|
|
|
|
Map<?, ?> taskData = (Map<?, ?>) data.get("taskData");
|
|
Map<?, ?> storageData = (Map<?, ?>) data.get("storageData");
|
|
List<?> robotData = (List<?>) data.get("robotData");
|
|
List<?> agvData = (List<?>) data.get("agvData");
|
|
|
|
return (taskData == null || taskData.isEmpty())
|
|
&& (storageData == null || storageData.isEmpty())
|
|
&& (robotData == null || robotData.isEmpty())
|
|
&& (agvData == null || agvData.isEmpty());
|
|
}
|
|
|
|
/**
|
|
* 从TUSK系统获取AGV状态数据
|
|
*
|
|
* <p><b>数据转换说明:</b></p>
|
|
* <ul>
|
|
* <li>从TUSK获取原始AGV状态</li>
|
|
* <li>转换为看板需要的格式</li>
|
|
* <li>映射状态码为状态文本</li>
|
|
* </ul>
|
|
*
|
|
* @return AGV状态列表
|
|
*/
|
|
private List<Map<String, Object>> getAgvStatusFromTusk() {
|
|
List<Map<String, Object>> agvList = new ArrayList<>();
|
|
|
|
try {
|
|
// 如果TUSK客户端服务未配置,返回空列表
|
|
if (tuskClientService == null) {
|
|
log.debug("TUSK客户端服务未配置,跳过AGV状态查询");
|
|
return agvList;
|
|
}
|
|
|
|
// 调用TUSK接口获取在线AGV列表
|
|
TuskResponse<List<AgvStatus>> response = tuskClientService.getOnlineRobots();
|
|
|
|
if (!response.isSuccess() || response.getData() == null) {
|
|
log.warn("从TUSK获取AGV状态失败: {}", response.getMsg());
|
|
return agvList;
|
|
}
|
|
|
|
// 转换TUSK数据为看板格式
|
|
List<AgvStatus> tuskAgvList = response.getData();
|
|
for (AgvStatus agvStatus : tuskAgvList) {
|
|
Map<String, Object> agv = new HashMap<>();
|
|
|
|
// AGV编号
|
|
agv.put("id", agvStatus.getId());
|
|
agv.put("name", "AGV#" + agvStatus.getId());
|
|
|
|
// 状态转换
|
|
String status = convertAgvStatus(agvStatus.getAgvStat());
|
|
agv.put("status", status.toLowerCase()); // working/idle/charging/error
|
|
agv.put("statusText", getAgvStatusText(agvStatus.getAgvStat()));
|
|
|
|
// 电量
|
|
agv.put("battery", agvStatus.getSoc());
|
|
|
|
// 当前任务数(根据状态判断:运行中为1,否则为0)
|
|
int tasks = (agvStatus.getAgvStat() >= 1 && agvStatus.getAgvStat() <= 12) ? 1 : 0;
|
|
agv.put("tasks", tasks);
|
|
|
|
agvList.add(agv);
|
|
}
|
|
|
|
log.debug("成功从TUSK获取{}个AGV状态", agvList.size());
|
|
|
|
} catch (Exception e) {
|
|
log.error("从TUSK获取AGV状态异常: {}", e.getMessage(), e);
|
|
}
|
|
|
|
return agvList;
|
|
}
|
|
|
|
/**
|
|
* 转换AGV状态码为标准状态
|
|
*
|
|
* @param agvStat TUSK系统的AGV状态码
|
|
* @return 标准状态 (working/idle/charging/error)
|
|
*/
|
|
private String convertAgvStatus(Integer agvStat) {
|
|
if (agvStat == null) {
|
|
return "idle";
|
|
}
|
|
|
|
if (agvStat == 0) {
|
|
return "idle"; // 空闲
|
|
} else if (agvStat >= 1 && agvStat <= 12) {
|
|
return "working"; // 运行中
|
|
} else if (agvStat == 13) {
|
|
return "charging"; // 充电中
|
|
} else if (agvStat >= 128) {
|
|
return "error"; // 异常状态
|
|
}
|
|
|
|
return "idle";
|
|
}
|
|
|
|
/**
|
|
* 获取AGV状态文本(中文)
|
|
*
|
|
* @param agvStat TUSK系统的AGV状态码
|
|
* @return 状态文本
|
|
*/
|
|
private String getAgvStatusText(Integer agvStat) {
|
|
if (agvStat == null) {
|
|
return "空闲";
|
|
}
|
|
|
|
if (agvStat == 0) {
|
|
return "空闲";
|
|
} else if (agvStat == 1) {
|
|
return "运行中";
|
|
} else if (agvStat == 2) {
|
|
return "直线运动中";
|
|
} else if (agvStat == 3) {
|
|
return "旋转中";
|
|
} else if (agvStat == 13) {
|
|
return "充电中";
|
|
} else if (agvStat == 23) {
|
|
return "暂停";
|
|
} else if (agvStat == 128) {
|
|
return "异常状态";
|
|
} else if (agvStat == 129) {
|
|
return "急停";
|
|
} else if (agvStat == 130) {
|
|
return "碰撞告警";
|
|
} else if (agvStat == 131) {
|
|
return "告警";
|
|
} else if (agvStat >= 1 && agvStat <= 12) {
|
|
return "运行中";
|
|
} else if (agvStat >= 128) {
|
|
return "异常";
|
|
}
|
|
|
|
return "未知状态";
|
|
}
|
|
|
|
/**
|
|
* 每5秒检查成品入库出库区看板数据并推送
|
|
*
|
|
* <p><b>数据来源:</b></p>
|
|
* <ul>
|
|
* <li>view_board_finish_package - 成品包装区数据</li>
|
|
* <li>view_board_finish_inbound - 成品入库区数据</li>
|
|
* </ul>
|
|
*/
|
|
@Scheduled(fixedRate = 5000)
|
|
public void pushFinishedProductBoardData() {
|
|
// 检查总开关
|
|
if (!dashboardPushEnabled) {
|
|
log.trace("看板推送已禁用");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 从数据库视图获取成品区数据
|
|
Map<String, Object> data = getFinishedProductBoardDataFromDb();
|
|
|
|
// 如果返回null,转换为空数据
|
|
if (data == null) {
|
|
data = createEmptyFinishedProductData();
|
|
}
|
|
|
|
// 计算数据哈希值
|
|
int currentHash = data.hashCode();
|
|
webSocketService.pushFinishedProductBoardData(data);
|
|
lastDataHash.put("finished-product", currentHash);
|
|
|
|
} catch (Exception e) {
|
|
log.error("推送成品入库出库区看板数据失败,推送空数据清空前端列表: {}", e.getMessage(), e);
|
|
// 异常时推送空数据,避免前端显示过期数据
|
|
try {
|
|
Map<String, Object> emptyData = createEmptyFinishedProductData();
|
|
webSocketService.pushFinishedProductBoardData(emptyData);
|
|
lastDataHash.put("finished-product", emptyData.hashCode());
|
|
} catch (Exception ex) {
|
|
log.error("推送空数据失败: {}", ex.getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 从数据库视图获取成品入库出库区看板数据
|
|
*
|
|
* @return 成品入库出库区看板数据
|
|
*/
|
|
private Map<String, Object> getFinishedProductBoardDataFromDb() {
|
|
try {
|
|
// 查询成品包装区数据
|
|
List<Map<String, Object>> packageList = dashboardDao.queryFinishPackageData();
|
|
log.debug("查询到成品包装区数据: {}条", packageList != null ? packageList.size() : 0);
|
|
|
|
// 查询成品入库区数据
|
|
List<Map<String, Object>> inboundList = dashboardDao.queryFinishInboundData();
|
|
log.debug("查询到成品入库区数据: {}条", inboundList != null ? inboundList.size() : 0);
|
|
|
|
// 构造返回数据
|
|
Map<String, Object> resultData = new HashMap<>();
|
|
resultData.put("packagingList", packageList != null ? packageList : new ArrayList<>());
|
|
resultData.put("inboundList", inboundList != null ? inboundList : new ArrayList<>());
|
|
|
|
return resultData;
|
|
|
|
} catch (Exception e) {
|
|
log.error("从数据库获取成品入库出库区看板数据失败: {}", e.getMessage(), e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 创建空的成品入库出库区数据
|
|
*
|
|
* @return 空的成品入库出库区数据结构
|
|
*/
|
|
private Map<String, Object> createEmptyFinishedProductData() {
|
|
Map<String, Object> emptyData = new HashMap<>();
|
|
emptyData.put("packagingList", new ArrayList<>());
|
|
emptyData.put("inboundList", new ArrayList<>());
|
|
return emptyData;
|
|
}
|
|
|
|
/**
|
|
* 判断成品入库出库区数据是否为空
|
|
*
|
|
* @param data 待检查的数据
|
|
* @return true=数据为空,false=数据不为空
|
|
*/
|
|
private boolean isFinishedProductDataEmpty(Map<String, Object> data) {
|
|
if (data == null || data.isEmpty()) {
|
|
return true;
|
|
}
|
|
|
|
List<?> packageList = (List<?>) data.get("packagingList");
|
|
List<?> inboundList = (List<?>) data.get("inboundList");
|
|
|
|
return (packageList == null || packageList.isEmpty())
|
|
&& (inboundList == null || inboundList.isEmpty());
|
|
}
|
|
|
|
/**
|
|
* 每5秒检查原材收货区看板数据并推送
|
|
*
|
|
* <p><b>数据来源:</b></p>
|
|
* <ul>
|
|
* <li>view_board_receiving_receive - 原材收货区数据</li>
|
|
* <li>view_board_receiving_inbound - 原材入库区数据</li>
|
|
* </ul>
|
|
*/
|
|
@Scheduled(fixedRate = 5000)
|
|
public void pushMaterialReceivingBoardData() {
|
|
// 检查总开关
|
|
if (!dashboardPushEnabled) {
|
|
log.trace("看板推送已禁用");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 从数据库视图获取原材收货区数据
|
|
Map<String, Object> data = getMaterialReceivingBoardDataFromDb();
|
|
|
|
// 如果返回null,转换为空数据
|
|
if (data == null) {
|
|
data = createEmptyMaterialReceivingData();
|
|
}
|
|
|
|
// 计算数据哈希值
|
|
int currentHash = data.hashCode();
|
|
webSocketService.pushMaterialReceivingBoardData(data);
|
|
lastDataHash.put("material-receiving", currentHash);
|
|
|
|
} catch (Exception e) {
|
|
log.error("推送原材收货区看板数据失败,推送空数据清空前端列表: {}", e.getMessage(), e);
|
|
// 异常时推送空数据,避免前端显示过期数据
|
|
try {
|
|
Map<String, Object> emptyData = createEmptyMaterialReceivingData();
|
|
webSocketService.pushMaterialReceivingBoardData(emptyData);
|
|
lastDataHash.put("material-receiving", emptyData.hashCode());
|
|
} catch (Exception ex) {
|
|
log.error("推送空数据失败: {}", ex.getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 从数据库视图获取原材收货区看板数据
|
|
*
|
|
* @return 原材收货区看板数据
|
|
*/
|
|
private Map<String, Object> getMaterialReceivingBoardDataFromDb() {
|
|
try {
|
|
// 查询原材收货区数据
|
|
List<Map<String, Object>> receiveList = dashboardDao.queryReceivingReceiveData();
|
|
log.debug("查询到原材收货区数据: {}条", receiveList != null ? receiveList.size() : 0);
|
|
|
|
// 查询原材入库区数据
|
|
List<Map<String, Object>> inboundList = dashboardDao.queryReceivingInboundData();
|
|
log.debug("查询到原材入库区数据: {}条", inboundList != null ? inboundList.size() : 0);
|
|
|
|
// 构造返回数据
|
|
Map<String, Object> resultData = new HashMap<>();
|
|
resultData.put("receivingList", receiveList != null ? receiveList : new ArrayList<>());
|
|
resultData.put("inboundList", inboundList != null ? inboundList : new ArrayList<>());
|
|
|
|
return resultData;
|
|
|
|
} catch (Exception e) {
|
|
log.error("从数据库获取原材收货区看板数据失败: {}", e.getMessage(), e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 创建空的原材收货区数据
|
|
*
|
|
* @return 空的原材收货区数据结构
|
|
*/
|
|
private Map<String, Object> createEmptyMaterialReceivingData() {
|
|
Map<String, Object> emptyData = new HashMap<>();
|
|
emptyData.put("receivingList", new ArrayList<>());
|
|
emptyData.put("inboundList", new ArrayList<>());
|
|
return emptyData;
|
|
}
|
|
|
|
/**
|
|
* 判断原材收货区数据是否为空
|
|
*
|
|
* @param data 待检查的数据
|
|
* @return true=数据为空,false=数据不为空
|
|
*/
|
|
private boolean isMaterialReceivingDataEmpty(Map<String, Object> data) {
|
|
if (data == null || data.isEmpty()) {
|
|
return true;
|
|
}
|
|
|
|
List<?> receiveList = (List<?>) data.get("receivingList");
|
|
List<?> inboundList = (List<?>) data.get("inboundList");
|
|
|
|
return (receiveList == null || receiveList.isEmpty())
|
|
&& (inboundList == null || inboundList.isEmpty());
|
|
}
|
|
}
|
|
|