Browse Source

看板websocket

master
han\hanst 4 months ago
parent
commit
6a2024aadd
  1. 6
      build.gradle
  2. 1
      src/main/java/com/gaotao/config/ShiroConfig.java
  3. 68
      src/main/java/com/gaotao/config/WebSocketConfig.java
  4. 39
      src/main/java/com/gaotao/modules/dashboard/dao/DashboardDao.java
  5. 206
      src/main/java/com/gaotao/modules/dashboard/service/DashboardWebSocketService.java
  6. 352
      src/main/java/com/gaotao/modules/dashboard/task/DashboardPushTask.java
  7. 31
      src/main/resources/mapper/dashboard/DashboardDao.xml

6
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
}

1
src/main/java/com/gaotao/config/ShiroConfig.java

@ -51,6 +51,7 @@ public class ShiroConfig {
filterMap.put("/api/wms/**", "anon");//wcsrcs反馈信息
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");

68
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 配置类
*
* <p><b>功能说明</b></p>
* <ul>
* <li>启用STOMP协议的WebSocket消息代理</li>
* <li>配置消息端点和订阅前缀</li>
* <li>支持跨域访问</li>
* <li>提供SockJS降级支持</li>
* </ul>
*
* <p><b>订阅主题说明</b></p>
* <ul>
* <li>/topic/dashboard/manual-picking - 人工拣选看板</li>
* <li>/topic/dashboard/robot-picking - 机械臂拣选看板</li>
* <li>/topic/dashboard/slitting-board - 分切区看板</li>
* <li>/topic/dashboard/inventory-board - 库存分析看板</li>
* </ul>
*
* @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时
}
}

39
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;
/**
* 看板数据访问接口
*
* <p><b>功能说明</b>从数据库视图获取看板实时数据</p>
*
* <p><b>数据来源</b></p>
* <ul>
* <li>view_board_slitting_assist_arm - 分切区助力臂数据</li>
* <li>view_board_slitting_inbound - 分切区入库数据</li>
* </ul>
*
* @author System
* @since 2025-01-23
*/
@Mapper
public interface DashboardDao {
/**
* 查询分切区助力臂数据
*
* @return 助力臂区实时数据
*/
List<Map<String, Object>> querySlittingAssistArmData();
/**
* 查询分切区入库数据
*
* @return 分切入库区实时数据
*/
List<Map<String, Object>> querySlittingInboundData();
}

206
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推送服务
*
* <p><b>功能说明</b></p>
* <ul>
* <li>向订阅的前端看板推送实时数据</li>
* <li>支持按看板类型定向推送</li>
* <li>支持广播推送到所有看板</li>
* </ul>
*
* <p><b>订阅主题列表</b></p>
* <ul>
* <li>/topic/dashboard/manual-picking - 人工拣选看板</li>
* <li>/topic/dashboard/robot-picking - 机械臂拣选看板</li>
* <li>/topic/dashboard/slitting-board - 分切区看板</li>
* <li>/topic/dashboard/inventory-board - 库存分析看板</li>
* </ul>
*
* @author System
* @since 2025-01-23
*/
@Slf4j
@Service
public class DashboardWebSocketService {
@Autowired
private SimpMessagingTemplate messagingTemplate;
/**
* 推送人工拣选看板数据
*
* @param data 看板数据
*/
public void pushManualPickingData(Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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);
}
}
}

352
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.*;
/**
* 看板数据推送定时任务
*
* <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;
/**
* 上次推送的数据哈希值用于检测数据变更
*/
private Map<String, Integer> lastDataHash = new HashMap<>();
@Autowired
private com.gaotao.modules.dashboard.dao.DashboardDao dashboardDao;
/**
* 每5秒检查机械臂拣选数据并推送
*
* <p>注意这个间隔可以根据实际需求调整</p>
* <ul>
* <li>如果数据变化频繁可以缩短间隔如2-3秒</li>
* <li>如果数据变化不频繁可以延长间隔如10-15秒</li>
* </ul>
*/
@Scheduled(fixedRate = 5000)
public void pushRobotPickingData() {
try {
// 从WCS Board API获取机械臂拣选数据
Map<String, Object> 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<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() {
try {
// 从数据库视图获取分切区数据
Map<String, Object> 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<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());
}
}

31
src/main/resources/mapper/dashboard/DashboardDao.xml

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gaotao.modules.dashboard.dao.DashboardDao">
<!-- 查询分切区助力臂数据 -->
<select id="querySlittingAssistArmData" resultType="map">
SELECT
storage_location AS storageLocation,
pallet_code AS palletCode,
picking_location AS pickingLocation,
material_name AS materialName,
quantity,
status
FROM view_board_slitting_assist_arm
ORDER BY picking_location ASC
</select>
<!-- 查询分切区入库数据 -->
<select id="querySlittingInboundData" resultType="map">
SELECT
storage_location AS storageLocation,
pallet_code AS palletCode,
task_type AS taskType,
status
FROM view_board_slitting_inbound
ORDER BY storage_location ASC
</select>
</mapper>
Loading…
Cancel
Save