From 1c6dd45e8cc074aa614d5e54cf93f3331119fbb8 Mon Sep 17 00:00:00 2001 From: "han\\hanst" Date: Thu, 23 Oct 2025 19:38:09 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=A0=87=E7=AD=BE=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/PdaLabelController.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/main/java/com/gaotao/modules/handlingunit/controller/PdaLabelController.java diff --git a/src/main/java/com/gaotao/modules/handlingunit/controller/PdaLabelController.java b/src/main/java/com/gaotao/modules/handlingunit/controller/PdaLabelController.java new file mode 100644 index 0000000..02f817c --- /dev/null +++ b/src/main/java/com/gaotao/modules/handlingunit/controller/PdaLabelController.java @@ -0,0 +1,87 @@ +package com.gaotao.modules.handlingunit.controller; + +import com.gaotao.common.utils.R; +import com.gaotao.modules.handlingunit.entity.HandlingUnit; +import com.gaotao.modules.handlingunit.service.HandlingUnitService; +import com.gaotao.modules.sys.controller.AbstractController; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * PDA标签查询控制器 + * + *

主要功能:

+ * + * + * @author System + * @since 2025-01-23 + */ +@Slf4j +@RestController +@RequestMapping("/pda/label") +public class PdaLabelController extends AbstractController { + + @Autowired + private HandlingUnitService handlingUnitService; + + /** + * @Author System + * @Description 查询标签信息 + * @Date 2025/01/23 + * @Param [Map] + * @return com.gaotao.common.utils.R + **/ + @PostMapping("query") + public R queryLabelInfo(@RequestBody Map params) { + try { + String site = (String) params.get("site"); + String labelCode = (String) params.get("labelCode"); + + log.info("=== 开始查询标签信息 ==="); + log.info("工厂: {}, 标签编码: {}", site, labelCode); + + // 参数验证 + if (site == null || site.trim().isEmpty()) { + return R.error("工厂编码不能为空"); + } + + if (labelCode == null || labelCode.trim().isEmpty()) { + return R.error("标签编码不能为空"); + } + + // 查询HandlingUnit信息 + HandlingUnit handlingUnit = handlingUnitService.lambdaQuery() + .eq(HandlingUnit::getSite, site) + .eq(HandlingUnit::getUnitId, labelCode.trim()) + .one(); + + if (handlingUnit == null) { + log.warn("标签不存在: site={}, labelCode={}", site, labelCode); + return R.error("标签不存在"); + } + + log.info("查询到标签信息: unitId={}, partNo={}, locationId={}, warehouseId={}, batchNo={}, wdr={}", + handlingUnit.getUnitId(), + handlingUnit.getPartNo(), + handlingUnit.getLocationId(), + handlingUnit.getWarehouseId(), + handlingUnit.getBatchNo(), + handlingUnit.getWdr()); + + log.info("=== 标签信息查询完成 ==="); + + return R.ok().put("data", handlingUnit); + + } catch (Exception e) { + log.error("=== 查询标签信息失败 === 错误信息: {}", e.getMessage(), e); + return R.error("查询失败: " + e.getMessage()); + } + } +} + From 6a2024aaddfcc82c968b17c51d78fe8f8b11a6c9 Mon Sep 17 00:00:00 2001 From: "han\\hanst" Date: Thu, 23 Oct 2025 19:39:00 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E7=9C=8B=E6=9D=BFwebsocket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 +- .../java/com/gaotao/config/ShiroConfig.java | 1 + .../com/gaotao/config/WebSocketConfig.java | 68 ++++ .../modules/dashboard/dao/DashboardDao.java | 39 ++ .../service/DashboardWebSocketService.java | 206 ++++++++++ .../dashboard/task/DashboardPushTask.java | 352 ++++++++++++++++++ .../mapper/dashboard/DashboardDao.xml | 31 ++ 7 files changed, 699 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/gaotao/config/WebSocketConfig.java create mode 100644 src/main/java/com/gaotao/modules/dashboard/dao/DashboardDao.java create mode 100644 src/main/java/com/gaotao/modules/dashboard/service/DashboardWebSocketService.java create mode 100644 src/main/java/com/gaotao/modules/dashboard/task/DashboardPushTask.java create mode 100644 src/main/resources/mapper/dashboard/DashboardDao.xml diff --git a/build.gradle b/build.gradle index 434d09f..3270328 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-freemarker' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' implementation 'org.springframework.boot:spring-boot-starter-data-redis' + // WebSocket 支持 + implementation 'org.springframework.boot:spring-boot-starter-websocket' //域控管理 implementation 'org.springframework.boot:spring-boot-starter-data-ldap' //邮件的包 @@ -132,7 +134,3 @@ sourceSets { } } } - -jar { - enabled = false -} diff --git a/src/main/java/com/gaotao/config/ShiroConfig.java b/src/main/java/com/gaotao/config/ShiroConfig.java index 8a77576..b7bc184 100644 --- a/src/main/java/com/gaotao/config/ShiroConfig.java +++ b/src/main/java/com/gaotao/config/ShiroConfig.java @@ -51,6 +51,7 @@ public class ShiroConfig { filterMap.put("/api/wms/**", "anon");//wcs、rcs反馈信息 filterMap.put("/api/agv/**", "anon");//agv反馈信息 filterMap.put("/api/dashboard/**", "anon");//看板接口 + filterMap.put("/ws/dashboard/**", "anon");//看板接口 filterMap.put("/swagger/**", "anon"); filterMap.put("/v2/api-docs", "anon"); filterMap.put("/swagger-ui.html", "anon"); diff --git a/src/main/java/com/gaotao/config/WebSocketConfig.java b/src/main/java/com/gaotao/config/WebSocketConfig.java new file mode 100644 index 0000000..e3d9a2f --- /dev/null +++ b/src/main/java/com/gaotao/config/WebSocketConfig.java @@ -0,0 +1,68 @@ +package com.gaotao.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +/** + * WebSocket 配置类 + * + *

功能说明:

+ *
    + *
  • 启用STOMP协议的WebSocket消息代理
  • + *
  • 配置消息端点和订阅前缀
  • + *
  • 支持跨域访问
  • + *
  • 提供SockJS降级支持
  • + *
+ * + *

订阅主题说明:

+ *
    + *
  • /topic/dashboard/manual-picking - 人工拣选看板
  • + *
  • /topic/dashboard/robot-picking - 机械臂拣选看板
  • + *
  • /topic/dashboard/slitting-board - 分切区看板
  • + *
  • /topic/dashboard/inventory-board - 库存分析看板
  • + *
+ * + * @author System + * @since 2025-01-23 + */ +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + /** + * 配置消息代理 + * + * @param config 消息代理注册器 + */ + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + // 启用简单消息代理,用于向客户端推送消息 + // /topic - 公共广播(一对多) + // /queue - 点对点(一对一) + config.enableSimpleBroker("/topic", "/queue"); + + // 客户端发送消息的目的地前缀 + config.setApplicationDestinationPrefixes("/app"); + + // 点对点消息的用户前缀 + config.setUserDestinationPrefix("/user"); + } + + /** + * 注册STOMP端点 + * + * @param registry STOMP端点注册器 + */ + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + // 注册WebSocket端点:/ws/dashboard + // 前端通过此端点建立WebSocket连接 + registry.addEndpoint("/ws/dashboard") + .setAllowedOriginPatterns("*") // 允许所有跨域访问 + .withSockJS(); // 启用SockJS降级支持(当浏览器不支持WebSocket时) + } +} + diff --git a/src/main/java/com/gaotao/modules/dashboard/dao/DashboardDao.java b/src/main/java/com/gaotao/modules/dashboard/dao/DashboardDao.java new file mode 100644 index 0000000..0f13359 --- /dev/null +++ b/src/main/java/com/gaotao/modules/dashboard/dao/DashboardDao.java @@ -0,0 +1,39 @@ +package com.gaotao.modules.dashboard.dao; + +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Map; + +/** + * 看板数据访问接口 + * + *

功能说明:从数据库视图获取看板实时数据

+ * + *

数据来源:

+ *
    + *
  • view_board_slitting_assist_arm - 分切区助力臂数据
  • + *
  • view_board_slitting_inbound - 分切区入库数据
  • + *
+ * + * @author System + * @since 2025-01-23 + */ +@Mapper +public interface DashboardDao { + + /** + * 查询分切区助力臂数据 + * + * @return 助力臂区实时数据 + */ + List> querySlittingAssistArmData(); + + /** + * 查询分切区入库数据 + * + * @return 分切入库区实时数据 + */ + List> querySlittingInboundData(); +} + diff --git a/src/main/java/com/gaotao/modules/dashboard/service/DashboardWebSocketService.java b/src/main/java/com/gaotao/modules/dashboard/service/DashboardWebSocketService.java new file mode 100644 index 0000000..71e3472 --- /dev/null +++ b/src/main/java/com/gaotao/modules/dashboard/service/DashboardWebSocketService.java @@ -0,0 +1,206 @@ +package com.gaotao.modules.dashboard.service; + +import com.gaotao.common.utils.R; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Service; + +import java.util.Map; + +/** + * 看板WebSocket推送服务 + * + *

功能说明:

+ *
    + *
  • 向订阅的前端看板推送实时数据
  • + *
  • 支持按看板类型定向推送
  • + *
  • 支持广播推送到所有看板
  • + *
+ * + *

订阅主题列表:

+ *
    + *
  • /topic/dashboard/manual-picking - 人工拣选看板
  • + *
  • /topic/dashboard/robot-picking - 机械臂拣选看板
  • + *
  • /topic/dashboard/slitting-board - 分切区看板
  • + *
  • /topic/dashboard/inventory-board - 库存分析看板
  • + *
+ * + * @author System + * @since 2025-01-23 + */ +@Slf4j +@Service +public class DashboardWebSocketService { + + @Autowired + private SimpMessagingTemplate messagingTemplate; + + /** + * 推送人工拣选看板数据 + * + * @param data 看板数据 + */ + public void pushManualPickingData(Map data) { + log.debug("推送人工拣选看板数据"); + try { + messagingTemplate.convertAndSend("/topic/dashboard/manual-picking", + R.ok().put("data", data)); + } catch (Exception e) { + log.error("推送人工拣选看板数据失败: {}", e.getMessage(), e); + } + } + + /** + * 推送机械臂拣选看板数据 + * + * @param data 看板数据 + */ + public void pushRobotPickingData(Map data) { + log.debug("推送机械臂拣选看板数据: 周转箱{}条, 原材{}条", + data.get("containerList") != null ? ((java.util.List)data.get("containerList")).size() : 0, + data.get("materialList") != null ? ((java.util.List)data.get("materialList")).size() : 0); + try { + messagingTemplate.convertAndSend("/topic/dashboard/robot-picking", + R.ok().put("data", data)); + } catch (Exception e) { + log.error("推送机械臂拣选看板数据失败: {}", e.getMessage(), e); + } + } + + /** + * 推送分切区看板数据 + * + * @param data 看板数据 + */ + public void pushSlittingBoardData(Map data) { + log.debug("推送分切区看板数据"); + try { + messagingTemplate.convertAndSend("/topic/dashboard/slitting-board", + R.ok().put("data", data)); + } catch (Exception e) { + log.error("推送分切区看板数据失败: {}", e.getMessage(), e); + } + } + + /** + * 推送库存分析看板数据 + * + * @param data 看板数据 + */ + public void pushInventoryBoardData(Map data) { + log.debug("推送库存分析看板数据"); + try { + messagingTemplate.convertAndSend("/topic/dashboard/inventory-board", + R.ok().put("data", data)); + } catch (Exception e) { + log.error("推送库存分析看板数据失败: {}", e.getMessage(), e); + } + } + + /** + * 推送成品入库出库区看板数据 + * + * @param data 看板数据 + */ + public void pushFinishedProductBoardData(Map data) { + log.debug("推送成品入库出库区看板数据"); + try { + messagingTemplate.convertAndSend("/topic/dashboard/finished-product-board", + R.ok().put("data", data)); + } catch (Exception e) { + log.error("推送成品入库出库区看板数据失败: {}", e.getMessage(), e); + } + } + + /** + * 推送原材收货区看板数据 + * + * @param data 看板数据 + */ + public void pushMaterialReceivingBoardData(Map data) { + log.debug("推送原材收货区看板数据"); + try { + messagingTemplate.convertAndSend("/topic/dashboard/material-receiving-board", + R.ok().put("data", data)); + } catch (Exception e) { + log.error("推送原材收货区看板数据失败: {}", e.getMessage(), e); + } + } + + /** + * 推送缓存区看板数据 + * + * @param data 看板数据 + */ + public void pushBufferBoardData(Map data) { + log.debug("推送缓存区看板数据"); + try { + messagingTemplate.convertAndSend("/topic/dashboard/buffer-board", + R.ok().put("data", data)); + } catch (Exception e) { + log.error("推送缓存区看板数据失败: {}", e.getMessage(), e); + } + } + + /** + * 推送车间AGV放料区看板数据 + * + * @param data 看板数据 + */ + public void pushWorkshopFeedingBoardData(Map data) { + log.debug("推送车间AGV放料区看板数据"); + try { + messagingTemplate.convertAndSend("/topic/dashboard/workshop-feeding-board", + R.ok().put("data", data)); + } catch (Exception e) { + log.error("推送车间AGV放料区看板数据失败: {}", e.getMessage(), e); + } + } + + /** + * 推送异常处理区看板数据 + * + * @param data 看板数据 + */ + public void pushExceptionBoardData(Map data) { + log.debug("推送异常处理区看板数据"); + try { + messagingTemplate.convertAndSend("/topic/dashboard/exception-board", + R.ok().put("data", data)); + } catch (Exception e) { + log.error("推送异常处理区看板数据失败: {}", e.getMessage(), e); + } + } + + /** + * 广播推送到所有看板 + * + * @param data 看板数据 + */ + public void broadcastToAllDashboards(Map data) { + log.info("广播推送到所有看板"); + try { + messagingTemplate.convertAndSend("/topic/dashboard/broadcast", + R.ok().put("data", data)); + } catch (Exception e) { + log.error("广播推送到所有看板失败: {}", e.getMessage(), e); + } + } + + /** + * 推送到指定主题 + * + * @param topic 主题名称 + * @param data 数据 + */ + public void pushToTopic(String topic, Object data) { + log.debug("推送数据到主题: {}", topic); + try { + messagingTemplate.convertAndSend(topic, R.ok().put("data", data)); + } catch (Exception e) { + log.error("推送数据到主题{}失败: {}", topic, e.getMessage(), e); + } + } +} + diff --git a/src/main/java/com/gaotao/modules/dashboard/task/DashboardPushTask.java b/src/main/java/com/gaotao/modules/dashboard/task/DashboardPushTask.java new file mode 100644 index 0000000..723af8b --- /dev/null +++ b/src/main/java/com/gaotao/modules/dashboard/task/DashboardPushTask.java @@ -0,0 +1,352 @@ +package com.gaotao.modules.dashboard.task; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.gaotao.common.utils.HttpUtils; +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.*; + +/** + * 看板数据推送定时任务 + * + *

功能说明:

+ *
    + *
  • 定时从WCS Board API获取最新数据
  • + *
  • 检测到数据变更后通过WebSocket推送到前端
  • + *
  • 相比轮询,只在数据变更时推送,减少无效传输
  • + *
+ * + *

推送策略:

+ *
    + *
  • 每5秒检查一次机械臂拣选数据
  • + *
  • 数据有变化时立即推送
  • + *
  • 推送失败时记录日志,不影响下次推送
  • + *
+ * + * @author System + * @since 2025-01-23 + */ +@Slf4j +@Component +public class DashboardPushTask { + + @Autowired + private DashboardWebSocketService webSocketService; + + @Value("${custom.wcs-board-api}") + private String wcsBoardApi; + + /** + * 上次推送的数据哈希值(用于检测数据变更) + */ + private Map lastDataHash = new HashMap<>(); + + @Autowired + private com.gaotao.modules.dashboard.dao.DashboardDao dashboardDao; + + /** + * 每5秒检查机械臂拣选数据并推送 + * + *

注意:这个间隔可以根据实际需求调整

+ *
    + *
  • 如果数据变化频繁,可以缩短间隔(如2-3秒)
  • + *
  • 如果数据变化不频繁,可以延长间隔(如10-15秒)
  • + *
+ */ + @Scheduled(fixedRate = 5000) + public void pushRobotPickingData() { + try { + // 从WCS Board API获取机械臂拣选数据 + Map data = getRobotPickingDataFromWcs(); + + // 如果返回null,转换为空数据(避免前端显示过期数据) + if (data == null) { + data = createEmptyData(); + } + + // 计算数据哈希值 + int currentHash = data.hashCode(); + int lastHash = lastDataHash.getOrDefault("robot-picking", 0); + + // 只在数据变更时推送(包括从有数据变为空数据) + if (currentHash != lastHash) { + boolean isEmpty = isDataEmpty(data); + if (isEmpty) { + log.info("=== 机械臂拣选数据为空,推送空数据清空前端列表 ==="); + } else { + int containerCount = ((List) data.get("containerList")).size(); + int materialCount = ((List) data.get("materialList")).size(); + log.info("=== 检测到机械臂拣选数据变更,推送到前端(周转箱:{}条,原材:{}条)===", + containerCount, materialCount); + } + webSocketService.pushRobotPickingData(data); + lastDataHash.put("robot-picking", currentHash); + } else { + log.debug("机械臂拣选数据无变化,跳过推送"); + } + + } catch (Exception e) { + log.error("推送机械臂拣选数据失败,推送空数据清空前端列表: {}", e.getMessage(), e); + // 异常时推送空数据,避免前端显示过期数据 + try { + Map emptyData = createEmptyData(); + webSocketService.pushRobotPickingData(emptyData); + lastDataHash.put("robot-picking", emptyData.hashCode()); + } catch (Exception ex) { + log.error("推送空数据失败: {}", ex.getMessage()); + } + } + } + + /** + * 从WCS Board API获取机械臂拣选数据 + * + * @return 机械臂拣选数据 + */ + private Map 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> containerList = new ArrayList<>(); // 1071-周转箱 + List> 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 item = convertMaterialToItem(material, sortingStation); + + // 根据工作站分类 + if ("1071".equals(sortingStation)) { + containerList.add(item); + } else if ("1060".equals(sortingStation)) { + materialList.add(item); + } + } + } + } + + // 构造返回数据 + Map 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 convertMaterialToItem(JsonNode material, String sortingStation) { + Map 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 createEmptyData() { + Map emptyData = new HashMap<>(); + emptyData.put("containerList", new ArrayList<>()); + emptyData.put("materialList", new ArrayList<>()); + return emptyData; + } + + /** + * 判断数据是否为空 + * + * @param data 待检查的数据 + * @return true=数据为空,false=数据不为空 + */ + private boolean isDataEmpty(Map 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秒检查分切区看板数据并推送 + * + *

数据来源:

+ *
    + *
  • view_board_slitting_assist_arm - 助力臂区数据
  • + *
  • view_board_slitting_inbound - 分切入库区数据
  • + *
+ */ + @Scheduled(fixedRate = 5000) + public void pushSlittingBoardData() { + try { + // 从数据库视图获取分切区数据 + Map data = getSlittingBoardDataFromDb(); + + // 如果返回null,转换为空数据 + if (data == null) { + data = createEmptySlittingData(); + } + + // 计算数据哈希值 + int currentHash = data.hashCode(); + int lastHash = lastDataHash.getOrDefault("slitting-board", 0); + + // 只在数据变更时推送 + if (currentHash != lastHash) { + boolean isEmpty = isSlittingDataEmpty(data); + if (isEmpty) { + log.info("=== 分切区看板数据为空,推送空数据清空前端列表 ==="); + } else { + int assistArmCount = ((List) data.get("assistArmList")).size(); + int inboundCount = ((List) data.get("slittingInboundList")).size(); + log.info("=== 检测到分切区看板数据变更,推送到前端(助力臂:{}条,入库:{}条)===", + assistArmCount, inboundCount); + } + webSocketService.pushSlittingBoardData(data); + lastDataHash.put("slitting-board", currentHash); + } else { + log.debug("分切区看板数据无变化,跳过推送"); + } + + } catch (Exception e) { + log.error("推送分切区看板数据失败,推送空数据清空前端列表: {}", e.getMessage(), e); + // 异常时推送空数据,避免前端显示过期数据 + try { + Map emptyData = createEmptySlittingData(); + webSocketService.pushSlittingBoardData(emptyData); + lastDataHash.put("slitting-board", emptyData.hashCode()); + } catch (Exception ex) { + log.error("推送空数据失败: {}", ex.getMessage()); + } + } + } + + /** + * 从数据库视图获取分切区看板数据 + * + * @return 分切区看板数据 + */ + private Map getSlittingBoardDataFromDb() { + try { + // 查询助力臂区数据 + List> assistArmList = dashboardDao.querySlittingAssistArmData(); + log.debug("查询到助力臂区数据: {}条", assistArmList != null ? assistArmList.size() : 0); + + // 查询分切入库区数据 + List> inboundList = dashboardDao.querySlittingInboundData(); + log.debug("查询到分切入库区数据: {}条", inboundList != null ? inboundList.size() : 0); + + // 构造返回数据 + Map 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 createEmptySlittingData() { + Map emptyData = new HashMap<>(); + emptyData.put("assistArmList", new ArrayList<>()); + emptyData.put("slittingInboundList", new ArrayList<>()); + return emptyData; + } + + /** + * 判断分切区数据是否为空 + * + * @param data 待检查的数据 + * @return true=数据为空,false=数据不为空 + */ + private boolean isSlittingDataEmpty(Map 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()); + } +} + diff --git a/src/main/resources/mapper/dashboard/DashboardDao.xml b/src/main/resources/mapper/dashboard/DashboardDao.xml new file mode 100644 index 0000000..1870452 --- /dev/null +++ b/src/main/resources/mapper/dashboard/DashboardDao.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + From 33715b9661a5fde5057e9c71683fe78c5bdaee29 Mon Sep 17 00:00:00 2001 From: "han\\hanst" Date: Thu, 23 Oct 2025 21:01:59 +0800 Subject: [PATCH 3/3] =?UTF-8?q?agv=E7=9C=9F=E5=AE=9E=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gaotao/common/utils/AgvClientUtil.java | 21 -- .../modules/dashboard/dao/DashboardDao.java | 99 ++++++ .../service/DashboardWebSocketService.java | 16 + .../dashboard/task/DashboardPushTask.java | 285 +++++++++++++++++- .../po/service/impl/PoServiceImpl.java | 14 +- .../mapper/dashboard/DashboardDao.xml | 238 +++++++++++++++ 6 files changed, 647 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/gaotao/common/utils/AgvClientUtil.java b/src/main/java/com/gaotao/common/utils/AgvClientUtil.java index 95fbe22..d300c86 100644 --- a/src/main/java/com/gaotao/common/utils/AgvClientUtil.java +++ b/src/main/java/com/gaotao/common/utils/AgvClientUtil.java @@ -313,39 +313,18 @@ public class AgvClientUtil { */ public Object getOnlineRobot() { String url = agvUrl + "/rpc/getOnlineRobot"; - Map request = new HashMap<>(); - - Long logId = null; try { - // 记录接口调用日志 - String requestJson = JSONObject.toJSONString(request); - logId = interfaceCallLogService.logInterfaceCall( - "AgvClientUtil", - "getOnlineRobot", - requestJson, - "55", - null, - "AGV查询在线小车接口" - ); String ifsResponse = HttpUtils.doGet(url, null, null); - ObjectMapper mapper = new ObjectMapper(); JsonNode jsonNode = mapper.readTree(ifsResponse); - int code = jsonNode.get("code").asInt(); String msg = jsonNode.get("msg").asText(); if(code != 200){ throw new RuntimeException("调用AGV接口失败,错误码:"+code+",错误信息:"+msg); } - // 返回数据部分 return jsonNode.get("data"); - } catch (Exception e) { - // 更新接口日志错误信息 - if (logId != null) { - interfaceCallLogService.updateCallResult(logId, null, "FAILED", e.getMessage(), null); - } throw new RuntimeException(e.getMessage()); } } diff --git a/src/main/java/com/gaotao/modules/dashboard/dao/DashboardDao.java b/src/main/java/com/gaotao/modules/dashboard/dao/DashboardDao.java index 0f13359..d78fecd 100644 --- a/src/main/java/com/gaotao/modules/dashboard/dao/DashboardDao.java +++ b/src/main/java/com/gaotao/modules/dashboard/dao/DashboardDao.java @@ -14,6 +14,7 @@ import java.util.Map; *
    *
  • view_board_slitting_assist_arm - 分切区助力臂数据
  • *
  • view_board_slitting_inbound - 分切区入库数据
  • + *
  • 智能立体仓库相关视图/表 - 任务统计、库位、设备状态等
  • *
* * @author System @@ -35,5 +36,103 @@ public interface DashboardDao { * @return 分切入库区实时数据 */ List> querySlittingInboundData(); + + // ==================== 智能立体仓库看板数据 ==================== + + /** + * 查询立体仓库任务统计数据 + * + *

数据说明:

+ *
    + *
  • totalTasks - 累计任务总数
  • + *
  • monthlyTasks - 月度作业总数
  • + *
  • outboundTasks - 出库作业数
  • + *
  • inboundTasks - 入库作业数
  • + *
  • outboundPercent - 出库占比
  • + *
  • inboundPercent - 入库占比
  • + *
+ * + * @return 任务统计数据 + */ + Map queryWarehouseTaskStats(); + + /** + * 查询立体仓库库位利用率数据 + * + *

数据说明:

+ *
    + *
  • totalSlots - 总库位数
  • + *
  • usedSlots - 已使用库位数
  • + *
  • utilizationRate - 利用率百分比
  • + *
  • steelPallet - 钢制托盘数量
  • + *
  • guardPallet - 护边托盘数量
  • + *
  • flatPallet - 平托盘数量
  • + *
+ * + * @return 库位利用率数据 + */ + Map queryWarehouseStorageUtilization(); + + /** + * 查询立体仓库机器人状态数据 + * + *

数据说明:

+ *
    + *
  • id - 机器人ID
  • + *
  • name - 机器人名称
  • + *
  • status - 状态(working/idle/charging/error)
  • + *
  • statusText - 状态文本
  • + *
  • efficiency - 效率百分比
  • + *
  • tasks - 当前任务数
  • + *
+ * + * @return 机器人状态列表 + */ + List> queryWarehouseRobotStatus(); + + /** + * 查询立体仓库AGV状态数据 + * + *

注意:此方法已废弃!

+ *

AGV状态数据不从数据库查询,而是从TUSK系统实时获取。

+ *

请使用 DashboardPushTask.getAgvStatusFromTusk() 方法获取AGV状态。

+ * + * @deprecated 使用 TuskClientService.getOnlineRobots() 代替 + * @return AGV状态列表(空列表) + */ + @Deprecated + List> queryWarehouseAgvStatus(); + + /** + * 查询立体仓库领料申请单统计 + * + *

数据说明:

+ *
    + *
  • total - 总数
  • + *
  • completed - 已完成数
  • + *
  • processing - 处理中数
  • + *
  • pending - 待处理数
  • + *
  • completionRate - 完成率百分比
  • + *
+ * + * @return 领料申请单统计 + */ + Map queryWarehouseMaterialRequestStats(); + + /** + * 查询立体仓库发货统计 + * + *

数据说明:

+ *
    + *
  • total - 总数
  • + *
  • completed - 已完成数
  • + *
  • processing - 处理中数
  • + *
  • pending - 待处理数
  • + *
  • completionRate - 完成率百分比
  • + *
+ * + * @return 发货统计 + */ + Map queryWarehouseShipmentStats(); } diff --git a/src/main/java/com/gaotao/modules/dashboard/service/DashboardWebSocketService.java b/src/main/java/com/gaotao/modules/dashboard/service/DashboardWebSocketService.java index 71e3472..e0b5917 100644 --- a/src/main/java/com/gaotao/modules/dashboard/service/DashboardWebSocketService.java +++ b/src/main/java/com/gaotao/modules/dashboard/service/DashboardWebSocketService.java @@ -24,6 +24,7 @@ import java.util.Map; *
  • /topic/dashboard/robot-picking - 机械臂拣选看板
  • *
  • /topic/dashboard/slitting-board - 分切区看板
  • *
  • /topic/dashboard/inventory-board - 库存分析看板
  • + *
  • /topic/dashboard/warehouse-3d - 智能立体仓库看板
  • * * * @author System @@ -188,6 +189,21 @@ public class DashboardWebSocketService { } } + /** + * 推送智能立体仓库看板数据 + * + * @param data 看板数据 + */ + public void pushWarehouse3dBoardData(Map data) { + log.debug("推送智能立体仓库看板数据"); + try { + messagingTemplate.convertAndSend("/topic/dashboard/warehouse-3d", + R.ok().put("data", data)); + } catch (Exception e) { + log.error("推送智能立体仓库看板数据失败: {}", e.getMessage(), e); + } + } + /** * 推送到指定主题 * diff --git a/src/main/java/com/gaotao/modules/dashboard/task/DashboardPushTask.java b/src/main/java/com/gaotao/modules/dashboard/task/DashboardPushTask.java index 723af8b..3297a86 100644 --- a/src/main/java/com/gaotao/modules/dashboard/task/DashboardPushTask.java +++ b/src/main/java/com/gaotao/modules/dashboard/task/DashboardPushTask.java @@ -1,8 +1,12 @@ 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; @@ -46,10 +50,13 @@ public class DashboardPushTask { * 上次推送的数据哈希值(用于检测数据变更) */ private Map lastDataHash = new HashMap<>(); - + @Autowired private com.gaotao.modules.dashboard.dao.DashboardDao dashboardDao; + @Autowired(required = false) + private TuskClientService tuskClientService; + /** * 每5秒检查机械臂拣选数据并推送 * @@ -348,5 +355,281 @@ public class DashboardPushTask { return (assistArmList == null || assistArmList.isEmpty()) && (inboundList == null || inboundList.isEmpty()); } + + /** + * 每5秒检查智能立体仓库看板数据并推送 + * + *

    数据来源:

    + *
      + *
    • 任务统计数据 - queryWarehouseTaskStats
    • + *
    • 库位利用率 - queryWarehouseStorageUtilization
    • + *
    • 机器人状态 - queryWarehouseRobotStatus
    • + *
    • AGV状态 - queryWarehouseAgvStatus
    • + *
    • 领料申请单统计 - queryWarehouseMaterialRequestStats
    • + *
    • 发货统计 - queryWarehouseShipmentStats
    • + *
    + */ + @Scheduled(fixedRate = 5000) + public void pushWarehouse3dBoardData() { + try { + // 从数据库获取立体仓库看板数据 + Map data = getWarehouse3dBoardDataFromDb(); + + // 如果返回null,转换为空数据 + if (data == null) { + data = createEmptyWarehouse3dData(); + } + + // 计算数据哈希值 + int currentHash = data.hashCode(); + int lastHash = lastDataHash.getOrDefault("warehouse-3d", 0); + + // 只在数据变更时推送 + if (currentHash != lastHash) { + boolean isEmpty = isWarehouse3dDataEmpty(data); + if (isEmpty) { + log.info("=== 智能立体仓库看板数据为空,推送空数据 ==="); + } else { + log.info("=== 检测到智能立体仓库看板数据变更,推送到前端 ==="); + } + webSocketService.pushWarehouse3dBoardData(data); + lastDataHash.put("warehouse-3d", currentHash); + } else { + log.debug("智能立体仓库看板数据无变化,跳过推送"); + } + + } catch (Exception e) { + log.error("推送智能立体仓库看板数据失败,推送空数据: {}", e.getMessage(), e); + // 异常时推送空数据,避免前端显示过期数据 + try { + Map emptyData = createEmptyWarehouse3dData(); + webSocketService.pushWarehouse3dBoardData(emptyData); + lastDataHash.put("warehouse-3d", emptyData.hashCode()); + } catch (Exception ex) { + log.error("推送空数据失败: {}", ex.getMessage()); + } + } + } + + /** + * 从数据库获取智能立体仓库看板数据 + * + * @return 智能立体仓库看板数据 + */ + private Map getWarehouse3dBoardDataFromDb() { + try { + log.debug("开始从数据库获取智能立体仓库看板数据"); + + // 查询任务统计数据 + //Map taskStats = dashboardDao.queryWarehouseTaskStats(); + Map taskStats = new HashMap<>(); + log.debug("任务统计数据: {}", taskStats); + + // 查询库位利用率数据 + //Map storageUtilization = dashboardDao.queryWarehouseStorageUtilization(); + Map storageUtilization = new HashMap<>(); + log.debug("库位利用率数据: {}", storageUtilization); + + // 查询机器人状态数据 + //List> robotStatus = dashboardDao.queryWarehouseRobotStatus(); + List> robotStatus = new ArrayList<>(); + log.debug("查询到机器人状态数据: {}条", robotStatus != null ? robotStatus.size() : 0); + + // 查询AGV状态数据(从TUSK系统获取) + List> agvStatus = getAgvStatusFromTusk(); + log.debug("查询到AGV状态数据: {}条", agvStatus != null ? agvStatus.size() : 0); + + // 查询领料申请单统计 + //Map materialRequestStats = dashboardDao.queryWarehouseMaterialRequestStats(); + Map materialRequestStats = new HashMap<>(); + log.debug("领料申请单统计: {}", materialRequestStats); + + // 查询发货统计 + //Map shipmentStats = dashboardDao.queryWarehouseShipmentStats(); + Map shipmentStats = new HashMap<>(); + log.debug("发货统计: {}", shipmentStats); + + // 构造返回数据 + Map 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<>()); + + log.debug("智能立体仓库看板数据组装完成"); + return resultData; + + } catch (Exception e) { + log.error("从数据库获取智能立体仓库看板数据失败: {}", e.getMessage(), e); + return null; + } + } + + /** + * 创建空的智能立体仓库数据 + * + * @return 空的智能立体仓库数据结构 + */ + private Map createEmptyWarehouse3dData() { + Map 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 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状态数据 + * + *

    数据转换说明:

    + *
      + *
    • 从TUSK获取原始AGV状态
    • + *
    • 转换为看板需要的格式
    • + *
    • 映射状态码为状态文本
    • + *
    + * + * @return AGV状态列表 + */ + private List> getAgvStatusFromTusk() { + List> agvList = new ArrayList<>(); + + try { + // 如果TUSK客户端服务未配置,返回空列表 + if (tuskClientService == null) { + log.debug("TUSK客户端服务未配置,跳过AGV状态查询"); + return agvList; + } + + // 调用TUSK接口获取在线AGV列表 + TuskResponse> response = tuskClientService.getOnlineRobots(); + + if (!response.isSuccess() || response.getData() == null) { + log.warn("从TUSK获取AGV状态失败: {}", response.getMsg()); + return agvList; + } + + // 转换TUSK数据为看板格式 + List tuskAgvList = response.getData(); + for (AgvStatus agvStatus : tuskAgvList) { + Map 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 "未知状态"; + } } diff --git a/src/main/java/com/gaotao/modules/po/service/impl/PoServiceImpl.java b/src/main/java/com/gaotao/modules/po/service/impl/PoServiceImpl.java index 89f56c4..e362173 100644 --- a/src/main/java/com/gaotao/modules/po/service/impl/PoServiceImpl.java +++ b/src/main/java/com/gaotao/modules/po/service/impl/PoServiceImpl.java @@ -172,8 +172,14 @@ public class PoServiceImpl extends ServiceImpl implemen if (partData!=null && !partData.isEmpty()) { shelfLife = partData.getFirst().get("durabilityDays")!=null?(Integer) partData.getFirst().get("durabilityDays"):null; } + // 根据库位获取仓库ID + String warehouseId = null; + Location location = locationService.getByLocationIdAndSite(inData.getSite(), inData.getLocationId()); + if (location != null) { + warehouseId = location.getWarehouseId(); + } // 创建采购接收记录 - 在库存更新之前创建 - PoReceiptDetail receiptDetail = createPoReceiptRecords(inData,shelfLife); + PoReceiptDetail receiptDetail = createPoReceiptRecords(inData,shelfLife,warehouseId); String receiptNo = receiptDetail.getReceiptNo(); SysUserEntity currentUser = (SysUserEntity) SecurityUtils.getSubject().getPrincipal(); String transType = "CRT"; @@ -184,7 +190,7 @@ public class PoServiceImpl extends ServiceImpl implemen transHeader.setTransNo(transNo.getNewTransNo()); transHeader.setTransDate(new Date()); transHeader.setTransTypeDb(transType); - transHeader.setWarehouseId(inData.getWarehouseId()); + transHeader.setWarehouseId(warehouseId); transHeader.setUserId(currentUser.getUserId().toString()); transHeader.setUserName(currentUser.getUserDisplay()); transHeader.setRemark(inData.getRemark()); @@ -522,7 +528,7 @@ public class PoServiceImpl extends ServiceImpl implemen /** * 创建采购接收记录 */ - private PoReceiptDetail createPoReceiptRecords(TransDetailDto inData,Integer shelfLife) { + private PoReceiptDetail createPoReceiptRecords(TransDetailDto inData,Integer shelfLife,String warehouseId) { // 生成接收单号 TransNoControl receiptNoControl = transNoService.getTransNo(inData.getSite(), "PR", 10); String receiptNo = receiptNoControl.getNewTransNo(); @@ -542,7 +548,7 @@ public class PoServiceImpl extends ServiceImpl implemen //poReceipt.setDeliveryNoteNo(inData.getPoNo()); // UI和ifs没有返回送货单号 poReceipt.setPrinted("N"); poReceipt.setRemark("PO接收自动创建 - " + inData.getRemark()); - poReceipt.setWarehouseId(inData.getWarehouseId()); + poReceipt.setWarehouseId(warehouseId); poReceipt.setOutWorkOrderFlag("N"); poReceipt.setEmailCanSendFlag("N"); poReceipt.setOrderref1(inData.getPoNo()); diff --git a/src/main/resources/mapper/dashboard/DashboardDao.xml b/src/main/resources/mapper/dashboard/DashboardDao.xml index 1870452..3f3acb2 100644 --- a/src/main/resources/mapper/dashboard/DashboardDao.xml +++ b/src/main/resources/mapper/dashboard/DashboardDao.xml @@ -27,5 +27,243 @@ ORDER BY storage_location ASC + + + + + + + + + + + + + + + + + + + + +