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

3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
3 months ago
  1. package com.gaotao.modules.dashboard.task;
  2. import com.beust.ah.A;
  3. import com.fasterxml.jackson.databind.JsonNode;
  4. import com.fasterxml.jackson.databind.ObjectMapper;
  5. import com.gaotao.common.utils.HttpUtils;
  6. import com.gaotao.modules.automatedWarehouse.entity.tusk.AgvStatus;
  7. import com.gaotao.modules.automatedWarehouse.entity.tusk.TuskResponse;
  8. import com.gaotao.modules.automatedWarehouse.service.TuskClientService;
  9. import com.gaotao.modules.dashboard.service.DashboardWebSocketService;
  10. import lombok.extern.slf4j.Slf4j;
  11. import org.springframework.beans.factory.annotation.Autowired;
  12. import org.springframework.beans.factory.annotation.Value;
  13. import org.springframework.scheduling.annotation.Scheduled;
  14. import org.springframework.stereotype.Component;
  15. import java.util.*;
  16. /**
  17. * 看板数据推送定时任务
  18. *
  19. * <p><b>功能说明</b></p>
  20. * <ul>
  21. * <li>定时从WCS Board API获取最新数据</li>
  22. * <li>检测到数据变更后通过WebSocket推送到前端</li>
  23. * <li>相比轮询只在数据变更时推送减少无效传输</li>
  24. * </ul>
  25. *
  26. * <p><b>推送策略</b></p>
  27. * <ul>
  28. * <li>每5秒检查一次机械臂拣选数据</li>
  29. * <li>数据有变化时立即推送</li>
  30. * <li>推送失败时记录日志不影响下次推送</li>
  31. * </ul>
  32. *
  33. * @author System
  34. * @since 2025-01-23
  35. */
  36. @Slf4j
  37. @Component
  38. public class DashboardPushTask {
  39. @Autowired
  40. private DashboardWebSocketService webSocketService;
  41. @Value("${custom.wcs-board-api}")
  42. private String wcsBoardApi;
  43. // 看板推送任务开关配置(所有看板共用一个开关)
  44. @Value("${dashboard.push.enabled:true}")
  45. private boolean dashboardPushEnabled;
  46. /**
  47. * 上次推送的数据哈希值用于检测数据变更
  48. */
  49. private Map<String, Integer> lastDataHash = new HashMap<>();
  50. @Autowired
  51. private com.gaotao.modules.dashboard.dao.DashboardDao dashboardDao;
  52. @Autowired(required = false)
  53. private TuskClientService tuskClientService;
  54. /**
  55. * 每5秒检查机械臂拣选数据并推送
  56. *
  57. * <p>注意这个间隔可以根据实际需求调整</p>
  58. * <ul>
  59. * <li>如果数据变化频繁可以缩短间隔如2-3秒</li>
  60. * <li>如果数据变化不频繁可以延长间隔如10-15秒</li>
  61. * </ul>
  62. *
  63. * <p><b>配置开关</b></p>
  64. * <ul>
  65. * <li>dashboard.push.enabled - 看板推送总开关</li>
  66. * </ul>
  67. */
  68. @Scheduled(fixedRate = 5000)
  69. public void pushRobotPickingData() {
  70. // 检查总开关
  71. if (!dashboardPushEnabled) {
  72. log.trace("看板推送已禁用");
  73. return;
  74. }
  75. try {
  76. // 从WCS Board API获取机械臂拣选数据
  77. Map<String, Object> data = getRobotPickingDataFromWcs();
  78. // 如果返回null,转换为空数据(避免前端显示过期数据)
  79. if (data == null) {
  80. data = createEmptyData();
  81. }
  82. // 计算数据哈希值
  83. int currentHash = data.hashCode();
  84. webSocketService.pushRobotPickingData(data);
  85. lastDataHash.put("robot-picking", currentHash);
  86. } catch (Exception e) {
  87. log.error("推送机械臂拣选数据失败,推送空数据清空前端列表: {}", e.getMessage(), e);
  88. // 异常时推送空数据,避免前端显示过期数据
  89. try {
  90. Map<String, Object> emptyData = createEmptyData();
  91. webSocketService.pushRobotPickingData(emptyData);
  92. lastDataHash.put("robot-picking", emptyData.hashCode());
  93. } catch (Exception ex) {
  94. log.error("推送空数据失败: {}", ex.getMessage());
  95. }
  96. }
  97. }
  98. /**
  99. * 从WCS Board API获取机械臂拣选数据
  100. *
  101. * @return 机械臂拣选数据
  102. */
  103. private Map<String, Object> getRobotPickingDataFromWcs() {
  104. try {
  105. // 调用WCS Board API
  106. String url = wcsBoardApi + "WmsDashboard/auto-sorting-info";
  107. String wcsResponse = HttpUtils.doGet(url, null, null);
  108. // 解析JSON数据
  109. ObjectMapper mapper = new ObjectMapper();
  110. JsonNode rootNode = mapper.readTree(wcsResponse);
  111. // 检查返回码
  112. int resCode = rootNode.get("resCode").asInt();
  113. if (resCode != 200) {
  114. log.warn("WCS API返回错误: code={}", resCode);
  115. return null;
  116. }
  117. // 获取sortingStations数组
  118. JsonNode resData = rootNode.get("resData");
  119. if (resData == null || !resData.has("sortingStations")) {
  120. log.warn("WCS返回数据中没有sortingStations");
  121. return createEmptyData();
  122. }
  123. JsonNode sortingStations = resData.get("sortingStations");
  124. // 按照sortingStation分类处理数据
  125. List<Map<String, Object>> containerList = new ArrayList<>(); // 1071-周转箱
  126. List<Map<String, Object>> materialList = new ArrayList<>(); // 1060-原材
  127. for (JsonNode station : sortingStations) {
  128. String sortingStation = station.get("sortingStation").asText();
  129. JsonNode materials = station.get("materials");
  130. if (materials != null && materials.isArray()) {
  131. for (JsonNode material : materials) {
  132. Map<String, Object> item = convertMaterialToItem(material, sortingStation);
  133. // 根据工作站分类
  134. if ("1071".equals(sortingStation)) {
  135. containerList.add(item);
  136. } else if ("1060".equals(sortingStation)) {
  137. materialList.add(item);
  138. }
  139. }
  140. }
  141. }
  142. // 构造返回数据
  143. Map<String, Object> resultData = new HashMap<>();
  144. resultData.put("containerList", containerList);
  145. resultData.put("materialList", materialList);
  146. return resultData;
  147. } catch (Exception e) {
  148. log.error("从WCS获取机械臂拣选数据失败: {}", e.getMessage());
  149. return null;
  150. }
  151. }
  152. /**
  153. * 将WCS返回的material数据转换为前端需要的格式
  154. *
  155. * @param material WCS返回的物料数据
  156. * @param sortingStation 工作站编号
  157. * @return 前端表格数据格式
  158. */
  159. private Map<String, Object> convertMaterialToItem(JsonNode material, String sortingStation) {
  160. Map<String, Object> item = new HashMap<>();
  161. // 拣选托盘码 (源托盘)
  162. item.put("pickingBatchNo", material.has("sourcePalletCode") ?
  163. material.get("sourcePalletCode").asText() : "");
  164. // 拣选物料名称 (SKU)
  165. item.put("pickingMaterialName", material.has("sku") ?
  166. material.get("sku").asText() : "");
  167. // RFID条码
  168. item.put("rfidBarcode", material.has("rfidBarcode") ?
  169. material.get("rfidBarcode").asText() : "");
  170. // 状态 (根据isCompleted判断)
  171. boolean isCompleted = material.has("isCompleted") && material.get("isCompleted").asBoolean();
  172. item.put("status", isCompleted ? "完成" : "等待分拣");
  173. // 存放托盘码 (目标托盘)
  174. item.put("storageBatchNo", material.has("targetPalletCode") ?
  175. material.get("targetPalletCode").asText() : "");
  176. // 存放位置 (工作站编号)
  177. item.put("storageLocation", material.has("sortingStation") ?
  178. material.get("sortingStation").asText() : sortingStation);
  179. return item;
  180. }
  181. /**
  182. * 创建空数据
  183. *
  184. * @return 空的数据结构
  185. */
  186. private Map<String, Object> createEmptyData() {
  187. Map<String, Object> emptyData = new HashMap<>();
  188. emptyData.put("containerList", new ArrayList<>());
  189. emptyData.put("materialList", new ArrayList<>());
  190. return emptyData;
  191. }
  192. /**
  193. * 判断数据是否为空
  194. *
  195. * @param data 待检查的数据
  196. * @return true=数据为空false=数据不为空
  197. */
  198. private boolean isDataEmpty(Map<String, Object> data) {
  199. if (data == null || data.isEmpty()) {
  200. return true;
  201. }
  202. List<?> containerList = (List<?>) data.get("containerList");
  203. List<?> materialList = (List<?>) data.get("materialList");
  204. return (containerList == null || containerList.isEmpty())
  205. && (materialList == null || materialList.isEmpty());
  206. }
  207. /**
  208. * 每5秒检查分切区看板数据并推送
  209. *
  210. * <p><b>数据来源</b></p>
  211. * <ul>
  212. * <li>view_board_slitting_assist_arm - 助力臂区数据</li>
  213. * <li>view_board_slitting_inbound - 分切入库区数据</li>
  214. * </ul>
  215. */
  216. @Scheduled(fixedRate = 5000)
  217. public void pushSlittingBoardData() {
  218. // 检查总开关
  219. if (!dashboardPushEnabled) {
  220. log.trace("看板推送已禁用");
  221. return;
  222. }
  223. try {
  224. // 从数据库视图获取分切区数据
  225. Map<String, Object> data = getSlittingBoardDataFromDb();
  226. // 如果返回null,转换为空数据
  227. if (data == null) {
  228. data = createEmptySlittingData();
  229. }
  230. // 计算数据哈希值
  231. int currentHash = data.hashCode();
  232. webSocketService.pushSlittingBoardData(data);
  233. lastDataHash.put("slitting-board", currentHash);
  234. } catch (Exception e) {
  235. log.error("推送分切区看板数据失败,推送空数据清空前端列表: {}", e.getMessage(), e);
  236. // 异常时推送空数据,避免前端显示过期数据
  237. try {
  238. Map<String, Object> emptyData = createEmptySlittingData();
  239. webSocketService.pushSlittingBoardData(emptyData);
  240. lastDataHash.put("slitting-board", emptyData.hashCode());
  241. } catch (Exception ex) {
  242. log.error("推送空数据失败: {}", ex.getMessage());
  243. }
  244. }
  245. }
  246. /**
  247. * 从数据库视图获取分切区看板数据
  248. *
  249. * @return 分切区看板数据
  250. */
  251. private Map<String, Object> getSlittingBoardDataFromDb() {
  252. try {
  253. // 查询助力臂区数据
  254. List<Map<String, Object>> assistArmList = dashboardDao.querySlittingAssistArmData();
  255. log.debug("查询到助力臂区数据: {}条", assistArmList != null ? assistArmList.size() : 0);
  256. // 查询分切入库区数据
  257. List<Map<String, Object>> inboundList = dashboardDao.querySlittingInboundData();
  258. log.debug("查询到分切入库区数据: {}条", inboundList != null ? inboundList.size() : 0);
  259. // 构造返回数据
  260. Map<String, Object> resultData = new HashMap<>();
  261. resultData.put("assistArmList", assistArmList != null ? assistArmList : new ArrayList<>());
  262. resultData.put("slittingInboundList", inboundList != null ? inboundList : new ArrayList<>());
  263. return resultData;
  264. } catch (Exception e) {
  265. log.error("从数据库获取分切区看板数据失败: {}", e.getMessage(), e);
  266. return null;
  267. }
  268. }
  269. /**
  270. * 创建空的分切区数据
  271. *
  272. * @return 空的分切区数据结构
  273. */
  274. private Map<String, Object> createEmptySlittingData() {
  275. Map<String, Object> emptyData = new HashMap<>();
  276. emptyData.put("assistArmList", new ArrayList<>());
  277. emptyData.put("slittingInboundList", new ArrayList<>());
  278. return emptyData;
  279. }
  280. /**
  281. * 判断分切区数据是否为空
  282. *
  283. * @param data 待检查的数据
  284. * @return true=数据为空false=数据不为空
  285. */
  286. private boolean isSlittingDataEmpty(Map<String, Object> data) {
  287. if (data == null || data.isEmpty()) {
  288. return true;
  289. }
  290. List<?> assistArmList = (List<?>) data.get("assistArmList");
  291. List<?> inboundList = (List<?>) data.get("slittingInboundList");
  292. return (assistArmList == null || assistArmList.isEmpty())
  293. && (inboundList == null || inboundList.isEmpty());
  294. }
  295. /**
  296. * 每5秒检查智能立体仓库看板数据并推送
  297. *
  298. * <p><b>数据来源</b></p>
  299. * <ul>
  300. * <li>任务统计数据 - queryWarehouseTaskStats</li>
  301. * <li>库位利用率 - queryWarehouseStorageUtilization</li>
  302. * <li>机器人状态 - queryWarehouseRobotStatus</li>
  303. * <li>AGV状态 - queryWarehouseAgvStatus</li>
  304. * <li>领料申请单统计 - queryWarehouseMaterialRequestStats</li>
  305. * <li>发货统计 - queryWarehouseShipmentStats</li>
  306. * </ul>
  307. */
  308. @Scheduled(fixedRate = 5000)
  309. public void pushWarehouse3dBoardData() {
  310. // 检查总开关
  311. if (!dashboardPushEnabled) {
  312. log.trace("看板推送已禁用");
  313. return;
  314. }
  315. try {
  316. // 从数据库获取立体仓库看板数据
  317. Map<String, Object> data = getWarehouse3dBoardDataFromDb();
  318. // 如果返回null,转换为空数据
  319. if (data == null) {
  320. data = createEmptyWarehouse3dData();
  321. }
  322. // 计算数据哈希值
  323. int currentHash = data.hashCode();
  324. webSocketService.pushWarehouse3dBoardData(data);
  325. lastDataHash.put("warehouse-3d", currentHash);
  326. } catch (Exception e) {
  327. log.error("推送智能立体仓库看板数据失败,推送空数据: {}", e.getMessage(), e);
  328. // 异常时推送空数据,避免前端显示过期数据
  329. try {
  330. Map<String, Object> emptyData = createEmptyWarehouse3dData();
  331. webSocketService.pushWarehouse3dBoardData(emptyData);
  332. lastDataHash.put("warehouse-3d", emptyData.hashCode());
  333. } catch (Exception ex) {
  334. log.error("推送空数据失败: {}", ex.getMessage());
  335. }
  336. }
  337. }
  338. /**
  339. * 从数据库获取智能立体仓库看板数据
  340. *
  341. * @return 智能立体仓库看板数据
  342. */
  343. private Map<String, Object> getWarehouse3dBoardDataFromDb() {
  344. try {
  345. log.debug("开始从数据库获取智能立体仓库看板数据");
  346. // 查询任务统计数据
  347. Map<String, Object> taskStats = dashboardDao.queryWarehouseTaskStats();
  348. log.debug("任务统计数据: {}", taskStats);
  349. // 查询库位利用率数据(从WCS Board API获取)
  350. Map<String, Object> storageUtilization = getInventoryStatsFromWcs();
  351. log.debug("库位利用率数据: {}", storageUtilization);
  352. // 查询机器人状态数据
  353. //List<Map<String, Object>> robotStatus = dashboardDao.queryWarehouseRobotStatus();
  354. List<Map<String, Object>> robotStatus = new ArrayList<>();
  355. log.debug("查询到机器人状态数据: {}条", robotStatus != null ? robotStatus.size() : 0);
  356. // 查询AGV状态数据(从TUSK系统获取)
  357. List<Map<String, Object>> agvStatus = getAgvStatusFromTusk();
  358. log.debug("查询到AGV状态数据: {}条", agvStatus != null ? agvStatus.size() : 0);
  359. // 查询领料申请单统计
  360. //Map<String, Object> materialRequestStats = dashboardDao.queryWarehouseMaterialRequestStats();
  361. Map<String, Object> materialRequestStats = new HashMap<>();
  362. log.debug("领料申请单统计: {}", materialRequestStats);
  363. // 查询发货统计
  364. //Map<String, Object> shipmentStats = dashboardDao.queryWarehouseShipmentStats();
  365. Map<String, Object> shipmentStats = new HashMap<>();
  366. log.debug("发货统计: {}", shipmentStats);
  367. // 查询库存趋势数据
  368. List<Map<String, Object>> rawMaterialTrend = dashboardDao.queryRawMaterialInventoryTrend();
  369. log.debug("查询到原材料库存趋势数据: {}条", rawMaterialTrend != null ? rawMaterialTrend.size() : 0);
  370. List<Map<String, Object>> specifiedMaterialTrend = dashboardDao.querySpecifiedMaterialInventoryTrend();
  371. log.debug("查询到规格料库存趋势数据: {}条", specifiedMaterialTrend != null ? specifiedMaterialTrend.size() : 0);
  372. List<Map<String, Object>> finishedGoodsTrend = dashboardDao.queryFinishedGoodsInventoryTrend();
  373. log.debug("查询到产成品库存趋势数据: {}条", finishedGoodsTrend != null ? finishedGoodsTrend.size() : 0);
  374. // 构造返回数据
  375. Map<String, Object> resultData = new HashMap<>();
  376. resultData.put("taskData", taskStats != null ? taskStats : new HashMap<>());
  377. resultData.put("storageData", storageUtilization != null ? storageUtilization : new HashMap<>());
  378. resultData.put("robotData", robotStatus != null ? robotStatus : new ArrayList<>());
  379. resultData.put("agvData", agvStatus != null ? agvStatus : new ArrayList<>());
  380. resultData.put("materialRequestData", materialRequestStats != null ? materialRequestStats : new HashMap<>());
  381. resultData.put("shipmentData", shipmentStats != null ? shipmentStats : new HashMap<>());
  382. // 添加库存趋势数据
  383. resultData.put("rawMaterialTrend", rawMaterialTrend != null ? rawMaterialTrend : new ArrayList<>());
  384. resultData.put("specifiedMaterialTrend", specifiedMaterialTrend != null ? specifiedMaterialTrend : new ArrayList<>());
  385. resultData.put("finishedGoodsTrend", finishedGoodsTrend != null ? finishedGoodsTrend : new ArrayList<>());
  386. log.debug("智能立体仓库看板数据组装完成");
  387. return resultData;
  388. } catch (Exception e) {
  389. log.error("从数据库获取智能立体仓库看板数据失败: {}", e.getMessage(), e);
  390. return null;
  391. }
  392. }
  393. /**
  394. * 从WCS Board API获取库存统计数据
  395. *
  396. * <p><b>API说明</b></p>
  397. * <ul>
  398. * <li>接口地址: /api/WmsDashboard/inventory-stats</li>
  399. * <li>返回托盘库存统计平托框架托钢托</li>
  400. * <li>返回空容器库存数量</li>
  401. * </ul>
  402. *
  403. * <p><b>数据转换</b></p>
  404. * <ul>
  405. * <li>flatPallet平托 -> flatPallet</li>
  406. * <li>framePallet框架托 -> guardPallet围挡托盘</li>
  407. * <li>steelPallet钢托 -> steelPallet</li>
  408. * <li>emptyContainerInventory -> otherPallet其他</li>
  409. * </ul>
  410. *
  411. * @return 库存统计数据
  412. */
  413. private Map<String, Object> getInventoryStatsFromWcs() {
  414. try {
  415. // 调用WCS Board API
  416. String url = wcsBoardApi + "WmsDashboard/inventory-stats";
  417. log.debug("调用WCS库存统计API: {}", url);
  418. String wcsResponse = HttpUtils.doGet(url, null, null);
  419. log.debug("WCS API返回数据: {}", wcsResponse);
  420. // 解析JSON数据
  421. ObjectMapper mapper = new ObjectMapper();
  422. JsonNode rootNode = mapper.readTree(wcsResponse);
  423. // 检查返回码
  424. int resCode = rootNode.get("resCode").asInt();
  425. if (resCode != 200) {
  426. String resMsg = rootNode.has("resMsg") ? rootNode.get("resMsg").asText() : "未知错误";
  427. log.warn("WCS API返回错误: code={}, msg={}", resCode, resMsg);
  428. return createEmptyStorageData();
  429. }
  430. // 获取resData数据
  431. JsonNode resData = rootNode.get("resData");
  432. if (resData == null || resData.isNull()) {
  433. log.warn("WCS返回数据中没有resData");
  434. return createEmptyStorageData();
  435. }
  436. // 获取materialInventory(物料库存)
  437. JsonNode materialInventory = resData.get("materialInventory");
  438. if (materialInventory == null || materialInventory.isNull()) {
  439. log.warn("WCS返回数据中没有materialInventory");
  440. return createEmptyStorageData();
  441. }
  442. // 提取各类托盘数量
  443. int flatPallet = materialInventory.has("flatPallet") ?
  444. materialInventory.get("flatPallet").asInt() : 0;
  445. int framePallet = materialInventory.has("framePallet") ?
  446. materialInventory.get("framePallet").asInt() : 0;
  447. int steelPallet = materialInventory.has("steelPallet") ?
  448. materialInventory.get("steelPallet").asInt() : 0;
  449. // 提取空容器库存
  450. int emptyContainer = resData.has("emptyContainerInventory") ?
  451. resData.get("emptyContainerInventory").asInt() : 0;
  452. // 计算总使用库位和利用率
  453. int usedSlots = flatPallet + framePallet + steelPallet;
  454. int totalSlots = 1960; // 固定值
  455. double utilizationRate = totalSlots > 0 ?
  456. Math.round((double) usedSlots / totalSlots * 1000.0) / 10.0 : 0.0;
  457. // 构造返回数据
  458. Map<String, Object> storageData = new HashMap<>();
  459. storageData.put("totalSlots", totalSlots); // 总库位数
  460. storageData.put("usedSlots", usedSlots); // 已使用库位数
  461. storageData.put("utilizationRate", utilizationRate); // 利用率
  462. storageData.put("flatPallet", flatPallet); // 平托
  463. storageData.put("guardPallet", framePallet); // 围挡托盘(对应framePallet)
  464. storageData.put("steelPallet", steelPallet); // 钢托盘
  465. storageData.put("otherPallet", emptyContainer); // 其他(空容器)
  466. log.info("库存统计数据获取成功 - 平托:{}, 框架托:{}, 钢托:{}, 空容器:{}, 总使用:{}, 利用率:{}%",
  467. flatPallet, framePallet, steelPallet, emptyContainer, usedSlots, utilizationRate);
  468. return storageData;
  469. } catch (Exception e) {
  470. log.error("从WCS获取库存统计数据失败: {}", e.getMessage(), e);
  471. return createEmptyStorageData();
  472. }
  473. }
  474. /**
  475. * 创建空的库存统计数据
  476. *
  477. * @return 空的库存统计数据结构
  478. */
  479. private Map<String, Object> createEmptyStorageData() {
  480. Map<String, Object> emptyData = new HashMap<>();
  481. emptyData.put("totalSlots", 1960);
  482. emptyData.put("usedSlots", 0);
  483. emptyData.put("utilizationRate", 0.0);
  484. emptyData.put("flatPallet", 0);
  485. emptyData.put("guardPallet", 0);
  486. emptyData.put("steelPallet", 0);
  487. emptyData.put("otherPallet", 0);
  488. return emptyData;
  489. }
  490. /**
  491. * 创建空的智能立体仓库数据
  492. *
  493. * @return 空的智能立体仓库数据结构
  494. */
  495. private Map<String, Object> createEmptyWarehouse3dData() {
  496. Map<String, Object> emptyData = new HashMap<>();
  497. emptyData.put("taskData", new HashMap<>());
  498. emptyData.put("storageData", new HashMap<>());
  499. emptyData.put("robotData", new ArrayList<>());
  500. emptyData.put("agvData", new ArrayList<>());
  501. emptyData.put("materialRequestData", new HashMap<>());
  502. emptyData.put("shipmentData", new HashMap<>());
  503. return emptyData;
  504. }
  505. /**
  506. * 判断智能立体仓库数据是否为空
  507. *
  508. * @param data 待检查的数据
  509. * @return true=数据为空false=数据不为空
  510. */
  511. private boolean isWarehouse3dDataEmpty(Map<String, Object> data) {
  512. if (data == null || data.isEmpty()) {
  513. return true;
  514. }
  515. Map<?, ?> taskData = (Map<?, ?>) data.get("taskData");
  516. Map<?, ?> storageData = (Map<?, ?>) data.get("storageData");
  517. List<?> robotData = (List<?>) data.get("robotData");
  518. List<?> agvData = (List<?>) data.get("agvData");
  519. return (taskData == null || taskData.isEmpty())
  520. && (storageData == null || storageData.isEmpty())
  521. && (robotData == null || robotData.isEmpty())
  522. && (agvData == null || agvData.isEmpty());
  523. }
  524. /**
  525. * 从TUSK系统获取AGV状态数据
  526. *
  527. * <p><b>数据转换说明</b></p>
  528. * <ul>
  529. * <li>从TUSK获取原始AGV状态</li>
  530. * <li>转换为看板需要的格式</li>
  531. * <li>映射状态码为状态文本</li>
  532. * </ul>
  533. *
  534. * @return AGV状态列表
  535. */
  536. private List<Map<String, Object>> getAgvStatusFromTusk() {
  537. List<Map<String, Object>> agvList = new ArrayList<>();
  538. try {
  539. // 如果TUSK客户端服务未配置,返回空列表
  540. if (tuskClientService == null) {
  541. log.debug("TUSK客户端服务未配置,跳过AGV状态查询");
  542. return agvList;
  543. }
  544. // 调用TUSK接口获取在线AGV列表
  545. TuskResponse<List<AgvStatus>> response = tuskClientService.getOnlineRobots();
  546. if (!response.isSuccess() || response.getData() == null) {
  547. log.warn("从TUSK获取AGV状态失败: {}", response.getMsg());
  548. return agvList;
  549. }
  550. // 转换TUSK数据为看板格式
  551. List<AgvStatus> tuskAgvList = response.getData();
  552. for (AgvStatus agvStatus : tuskAgvList) {
  553. Map<String, Object> agv = new HashMap<>();
  554. // AGV编号
  555. agv.put("id", agvStatus.getId());
  556. agv.put("name", "AGV#" + agvStatus.getId());
  557. // 状态转换
  558. String status = convertAgvStatus(agvStatus.getAgvStat());
  559. agv.put("status", status.toLowerCase()); // working/idle/charging/error
  560. agv.put("statusText", getAgvStatusText(agvStatus.getAgvStat()));
  561. // 电量
  562. agv.put("battery", agvStatus.getSoc());
  563. // 当前任务数(根据状态判断:运行中为1,否则为0)
  564. int tasks = (agvStatus.getAgvStat() >= 1 && agvStatus.getAgvStat() <= 12) ? 1 : 0;
  565. agv.put("tasks", tasks);
  566. agvList.add(agv);
  567. }
  568. log.debug("成功从TUSK获取{}个AGV状态", agvList.size());
  569. } catch (Exception e) {
  570. log.error("从TUSK获取AGV状态异常: {}", e.getMessage(), e);
  571. }
  572. return agvList;
  573. }
  574. /**
  575. * 转换AGV状态码为标准状态
  576. *
  577. * @param agvStat TUSK系统的AGV状态码
  578. * @return 标准状态 (working/idle/charging/error)
  579. */
  580. private String convertAgvStatus(Integer agvStat) {
  581. if (agvStat == null) {
  582. return "idle";
  583. }
  584. if (agvStat == 0) {
  585. return "idle"; // 空闲
  586. } else if (agvStat >= 1 && agvStat <= 12) {
  587. return "working"; // 运行中
  588. } else if (agvStat == 13) {
  589. return "charging"; // 充电中
  590. } else if (agvStat >= 128) {
  591. return "error"; // 异常状态
  592. }
  593. return "idle";
  594. }
  595. /**
  596. * 获取AGV状态文本中文
  597. *
  598. * @param agvStat TUSK系统的AGV状态码
  599. * @return 状态文本
  600. */
  601. private String getAgvStatusText(Integer agvStat) {
  602. if (agvStat == null) {
  603. return "空闲";
  604. }
  605. if (agvStat == 0) {
  606. return "空闲";
  607. } else if (agvStat == 1) {
  608. return "运行中";
  609. } else if (agvStat == 2) {
  610. return "直线运动中";
  611. } else if (agvStat == 3) {
  612. return "旋转中";
  613. } else if (agvStat == 13) {
  614. return "充电中";
  615. } else if (agvStat == 23) {
  616. return "暂停";
  617. } else if (agvStat == 128) {
  618. return "异常状态";
  619. } else if (agvStat == 129) {
  620. return "急停";
  621. } else if (agvStat == 130) {
  622. return "碰撞告警";
  623. } else if (agvStat == 131) {
  624. return "告警";
  625. } else if (agvStat >= 1 && agvStat <= 12) {
  626. return "运行中";
  627. } else if (agvStat >= 128) {
  628. return "异常";
  629. }
  630. return "未知状态";
  631. }
  632. /**
  633. * 每5秒检查成品入库出库区看板数据并推送
  634. *
  635. * <p><b>数据来源</b></p>
  636. * <ul>
  637. * <li>view_board_finish_package - 成品包装区数据</li>
  638. * <li>view_board_finish_inbound - 成品入库区数据</li>
  639. * </ul>
  640. */
  641. @Scheduled(fixedRate = 5000)
  642. public void pushFinishedProductBoardData() {
  643. // 检查总开关
  644. if (!dashboardPushEnabled) {
  645. log.trace("看板推送已禁用");
  646. return;
  647. }
  648. try {
  649. // 从数据库视图获取成品区数据
  650. Map<String, Object> data = getFinishedProductBoardDataFromDb();
  651. // 如果返回null,转换为空数据
  652. if (data == null) {
  653. data = createEmptyFinishedProductData();
  654. }
  655. // 计算数据哈希值
  656. int currentHash = data.hashCode();
  657. webSocketService.pushFinishedProductBoardData(data);
  658. lastDataHash.put("finished-product", currentHash);
  659. } catch (Exception e) {
  660. log.error("推送成品入库出库区看板数据失败,推送空数据清空前端列表: {}", e.getMessage(), e);
  661. // 异常时推送空数据,避免前端显示过期数据
  662. try {
  663. Map<String, Object> emptyData = createEmptyFinishedProductData();
  664. webSocketService.pushFinishedProductBoardData(emptyData);
  665. lastDataHash.put("finished-product", emptyData.hashCode());
  666. } catch (Exception ex) {
  667. log.error("推送空数据失败: {}", ex.getMessage());
  668. }
  669. }
  670. }
  671. /**
  672. * 从数据库视图获取成品入库出库区看板数据
  673. *
  674. * @return 成品入库出库区看板数据
  675. */
  676. private Map<String, Object> getFinishedProductBoardDataFromDb() {
  677. try {
  678. // 查询成品包装区数据
  679. List<Map<String, Object>> packageList = dashboardDao.queryFinishPackageData();
  680. log.debug("查询到成品包装区数据: {}条", packageList != null ? packageList.size() : 0);
  681. // 查询成品入库区数据
  682. List<Map<String, Object>> inboundList = dashboardDao.queryFinishInboundData();
  683. log.debug("查询到成品入库区数据: {}条", inboundList != null ? inboundList.size() : 0);
  684. // 构造返回数据
  685. Map<String, Object> resultData = new HashMap<>();
  686. resultData.put("packagingList", packageList != null ? packageList : new ArrayList<>());
  687. resultData.put("inboundList", inboundList != null ? inboundList : new ArrayList<>());
  688. return resultData;
  689. } catch (Exception e) {
  690. log.error("从数据库获取成品入库出库区看板数据失败: {}", e.getMessage(), e);
  691. return null;
  692. }
  693. }
  694. /**
  695. * 创建空的成品入库出库区数据
  696. *
  697. * @return 空的成品入库出库区数据结构
  698. */
  699. private Map<String, Object> createEmptyFinishedProductData() {
  700. Map<String, Object> emptyData = new HashMap<>();
  701. emptyData.put("packagingList", new ArrayList<>());
  702. emptyData.put("inboundList", new ArrayList<>());
  703. return emptyData;
  704. }
  705. /**
  706. * 判断成品入库出库区数据是否为空
  707. *
  708. * @param data 待检查的数据
  709. * @return true=数据为空false=数据不为空
  710. */
  711. private boolean isFinishedProductDataEmpty(Map<String, Object> data) {
  712. if (data == null || data.isEmpty()) {
  713. return true;
  714. }
  715. List<?> packageList = (List<?>) data.get("packagingList");
  716. List<?> inboundList = (List<?>) data.get("inboundList");
  717. return (packageList == null || packageList.isEmpty())
  718. && (inboundList == null || inboundList.isEmpty());
  719. }
  720. /**
  721. * 每5秒检查原材收货区看板数据并推送
  722. *
  723. * <p><b>数据来源</b></p>
  724. * <ul>
  725. * <li>view_board_receiving_receive - 原材收货区数据</li>
  726. * <li>view_board_receiving_inbound - 原材入库区数据</li>
  727. * </ul>
  728. */
  729. @Scheduled(fixedRate = 5000)
  730. public void pushMaterialReceivingBoardData() {
  731. // 检查总开关
  732. if (!dashboardPushEnabled) {
  733. log.trace("看板推送已禁用");
  734. return;
  735. }
  736. try {
  737. // 从数据库视图获取原材收货区数据
  738. Map<String, Object> data = getMaterialReceivingBoardDataFromDb();
  739. // 如果返回null,转换为空数据
  740. if (data == null) {
  741. data = createEmptyMaterialReceivingData();
  742. }
  743. // 计算数据哈希值
  744. int currentHash = data.hashCode();
  745. webSocketService.pushMaterialReceivingBoardData(data);
  746. lastDataHash.put("material-receiving", currentHash);
  747. } catch (Exception e) {
  748. log.error("推送原材收货区看板数据失败,推送空数据清空前端列表: {}", e.getMessage(), e);
  749. // 异常时推送空数据,避免前端显示过期数据
  750. try {
  751. Map<String, Object> emptyData = createEmptyMaterialReceivingData();
  752. webSocketService.pushMaterialReceivingBoardData(emptyData);
  753. lastDataHash.put("material-receiving", emptyData.hashCode());
  754. } catch (Exception ex) {
  755. log.error("推送空数据失败: {}", ex.getMessage());
  756. }
  757. }
  758. }
  759. /**
  760. * 从数据库视图获取原材收货区看板数据
  761. *
  762. * @return 原材收货区看板数据
  763. */
  764. private Map<String, Object> getMaterialReceivingBoardDataFromDb() {
  765. try {
  766. // 查询原材收货区数据
  767. List<Map<String, Object>> receiveList = dashboardDao.queryReceivingReceiveData();
  768. log.debug("查询到原材收货区数据: {}条", receiveList != null ? receiveList.size() : 0);
  769. // 查询原材入库区数据
  770. List<Map<String, Object>> inboundList = dashboardDao.queryReceivingInboundData();
  771. log.debug("查询到原材入库区数据: {}条", inboundList != null ? inboundList.size() : 0);
  772. // 构造返回数据
  773. Map<String, Object> resultData = new HashMap<>();
  774. resultData.put("receivingList", receiveList != null ? receiveList : new ArrayList<>());
  775. resultData.put("inboundList", inboundList != null ? inboundList : new ArrayList<>());
  776. return resultData;
  777. } catch (Exception e) {
  778. log.error("从数据库获取原材收货区看板数据失败: {}", e.getMessage(), e);
  779. return null;
  780. }
  781. }
  782. /**
  783. * 创建空的原材收货区数据
  784. *
  785. * @return 空的原材收货区数据结构
  786. */
  787. private Map<String, Object> createEmptyMaterialReceivingData() {
  788. Map<String, Object> emptyData = new HashMap<>();
  789. emptyData.put("receivingList", new ArrayList<>());
  790. emptyData.put("inboundList", new ArrayList<>());
  791. return emptyData;
  792. }
  793. /**
  794. * 判断原材收货区数据是否为空
  795. *
  796. * @param data 待检查的数据
  797. * @return true=数据为空false=数据不为空
  798. */
  799. private boolean isMaterialReceivingDataEmpty(Map<String, Object> data) {
  800. if (data == null || data.isEmpty()) {
  801. return true;
  802. }
  803. List<?> receiveList = (List<?>) data.get("receivingList");
  804. List<?> inboundList = (List<?>) data.get("inboundList");
  805. return (receiveList == null || receiveList.isEmpty())
  806. && (inboundList == null || inboundList.isEmpty());
  807. }
  808. }