|
|
@@ -0,0 +1,269 @@
|
|
|
+package com.zksy.web.websocket;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSONArray;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.zksy.base.domain.EquipmentStatus;
|
|
|
+import com.zksy.base.service.EquipmentStatusService;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import org.springframework.web.socket.CloseStatus;
|
|
|
+import org.springframework.web.socket.TextMessage;
|
|
|
+import org.springframework.web.socket.WebSocketSession;
|
|
|
+import org.springframework.web.socket.handler.TextWebSocketHandler;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 设备状态 WebSocket 处理器(多路复用版)
|
|
|
+ * <p>
|
|
|
+ * 支持的消息类型(通过 JSON 中的 type 字段区分):
|
|
|
+ * <ul>
|
|
|
+ * <li><b>subscribe</b> — 批量订阅设备,后端状态变更时主动推送</li>
|
|
|
+ * <li><b>unsubscribe</b> — 取消订阅设备</li>
|
|
|
+ * <li><b>deviceStatus</b> — 单次查询设备状态(兼容旧接口)</li>
|
|
|
+ * <li><b>ping</b> — 心跳检测</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * <h3>协议示例</h3>
|
|
|
+ * <pre>
|
|
|
+ * // 订阅
|
|
|
+ * → {"type":"subscribe","deviceCodes":["DEV001","DEV002"]}
|
|
|
+ * ← {"type":"subscribed","deviceCodes":["DEV001","DEV002"]}
|
|
|
+ *
|
|
|
+ * // 取消订阅
|
|
|
+ * → {"type":"unsubscribe","deviceCodes":["DEV001"]}
|
|
|
+ * ← {"type":"unsubscribed","deviceCodes":["DEV001"]}
|
|
|
+ *
|
|
|
+ * // 心跳
|
|
|
+ * → {"type":"ping"}
|
|
|
+ * ← {"type":"pong","timestamp":1690000000000}
|
|
|
+ *
|
|
|
+ * // 单次查询(兼容旧接口)
|
|
|
+ * → {"type":"deviceStatus","deviceCode":"DEV001"}
|
|
|
+ * ← {"type":"deviceStatus","success":true,"data":{...}}
|
|
|
+ *
|
|
|
+ * // 设备状态变更推送(服务端主动推送)
|
|
|
+ * ← {"type":"deviceStatusPush","data":{...}}
|
|
|
+ * </pre>
|
|
|
+ *
|
|
|
+ * @author zksy
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+public class DeviceStatusWebSocketHandler extends TextWebSocketHandler {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private EquipmentStatusService equipmentStatusService;
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private SubscriptionManager subscriptionManager;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 维护所有在线连接,key 为 sessionId
|
|
|
+ * 供 RedisListener 跨线程查找会话
|
|
|
+ */
|
|
|
+ private static final Map<String, WebSocketSession> SESSION_MAP = new ConcurrentHashMap<>();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据 sessionId 获取 WebSocket 会话(供 RedisListener 调用)
|
|
|
+ */
|
|
|
+ public static WebSocketSession getSession(String sessionId) {
|
|
|
+ return SESSION_MAP.get(sessionId);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void afterConnectionEstablished(WebSocketSession session) {
|
|
|
+ String sessionId = session.getId();
|
|
|
+ SESSION_MAP.put(sessionId, session);
|
|
|
+ log.info("WebSocket 连接建立: sessionId={}, remoteAddress={}", sessionId, session.getRemoteAddress());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
|
|
|
+ String payload = message.getPayload();
|
|
|
+ log.debug("收到 WebSocket 消息: sessionId={}, payload={}", session.getId(), payload);
|
|
|
+
|
|
|
+ try {
|
|
|
+ JSONObject request = JSONObject.parseObject(payload);
|
|
|
+ String type = request.getString("type");
|
|
|
+
|
|
|
+ if (type == null || type.isEmpty()) {
|
|
|
+ sendError(session, "消息缺少 type 字段");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // === 根据 type 分发到不同的业务处理 ===
|
|
|
+ switch (type) {
|
|
|
+ //订阅
|
|
|
+ case "subscribe":
|
|
|
+ handleSubscribe(session, request);
|
|
|
+ break;
|
|
|
+ //取消订阅
|
|
|
+ case "unsubscribe":
|
|
|
+ handleUnsubscribe(session, request);
|
|
|
+ break;
|
|
|
+ //查询设备状态(单次查询)
|
|
|
+ case "deviceStatus":
|
|
|
+ handleDeviceStatus(session, request);
|
|
|
+ break;
|
|
|
+ //心跳
|
|
|
+ case "ping":
|
|
|
+ handlePing(session);
|
|
|
+ break;
|
|
|
+ // 后续新增接口在此添加 case 分支
|
|
|
+ default:
|
|
|
+ sendError(session, "不支持的消息类型: " + type);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("处理 WebSocket 消息异常: sessionId={}", session.getId(), e);
|
|
|
+ sendError(session, "消息处理失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理批量订阅
|
|
|
+ */
|
|
|
+ private void handleSubscribe(WebSocketSession session, JSONObject request) throws IOException {
|
|
|
+ JSONArray deviceCodesArr = request.getJSONArray("deviceCodes");
|
|
|
+ if (deviceCodesArr == null || deviceCodesArr.isEmpty()) {
|
|
|
+ sendError(session, "缺少 deviceCodes 参数");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Set<String> deviceCodes = new HashSet<>();
|
|
|
+ for (int i = 0; i < deviceCodesArr.size(); i++) {
|
|
|
+ String code = deviceCodesArr.getString(i);
|
|
|
+ if (code != null && !code.isEmpty()) {
|
|
|
+ deviceCodes.add(code);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (deviceCodes.isEmpty()) {
|
|
|
+ sendError(session, "deviceCodes 不能为空");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Set<String> subscribed = subscriptionManager.subscribe(session.getId(), deviceCodes);
|
|
|
+
|
|
|
+ JSONObject response = new JSONObject();
|
|
|
+ response.put("type", "subscribed");
|
|
|
+ response.put("deviceCodes", subscribed);
|
|
|
+ session.sendMessage(new TextMessage(response.toJSONString()));
|
|
|
+
|
|
|
+ log.info("订阅成功: sessionId={}, 订阅设备={}", session.getId(), subscribed);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理取消订阅
|
|
|
+ */
|
|
|
+ private void handleUnsubscribe(WebSocketSession session, JSONObject request) throws IOException {
|
|
|
+ JSONArray deviceCodesArr = request.getJSONArray("deviceCodes");
|
|
|
+ if (deviceCodesArr == null || deviceCodesArr.isEmpty()) {
|
|
|
+ sendError(session, "缺少 deviceCodes 参数");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Set<String> deviceCodes = new HashSet<>();
|
|
|
+ for (int i = 0; i < deviceCodesArr.size(); i++) {
|
|
|
+ String code = deviceCodesArr.getString(i);
|
|
|
+ if (code != null && !code.isEmpty()) {
|
|
|
+ deviceCodes.add(code);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (deviceCodes.isEmpty()) {
|
|
|
+ sendError(session, "deviceCodes 不能为空");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Set<String> remaining = subscriptionManager.unsubscribe(session.getId(), deviceCodes);
|
|
|
+
|
|
|
+ JSONObject response = new JSONObject();
|
|
|
+ response.put("type", "unsubscribed");
|
|
|
+ response.put("deviceCodes", deviceCodes);
|
|
|
+ response.put("remaining", remaining);
|
|
|
+ session.sendMessage(new TextMessage(response.toJSONString()));
|
|
|
+
|
|
|
+ log.info("取消订阅: sessionId={}, 取消设备={}, 剩余订阅={}", session.getId(), deviceCodes, remaining);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理设备状态查询(兼容旧接口)
|
|
|
+ */
|
|
|
+ private void handleDeviceStatus(WebSocketSession session, JSONObject request) throws IOException {
|
|
|
+ String deviceCode = request.getString("deviceCode");
|
|
|
+ if (deviceCode == null || deviceCode.isEmpty()) {
|
|
|
+ sendResponse(session, "deviceStatus", false, null, "缺少 deviceCode 参数");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("查询设备状态: deviceCode={}", deviceCode);
|
|
|
+ EquipmentStatus status = equipmentStatusService.getByDeviceCode(deviceCode);
|
|
|
+
|
|
|
+ if (status == null) {
|
|
|
+ sendResponse(session, "deviceStatus", false, null, "未找到设备状态: deviceCode=" + deviceCode);
|
|
|
+ } else {
|
|
|
+ sendResponse(session, "deviceStatus", true, status, null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理心跳
|
|
|
+ */
|
|
|
+ private void handlePing(WebSocketSession session) throws IOException {
|
|
|
+ JSONObject response = new JSONObject();
|
|
|
+ response.put("type", "pong");
|
|
|
+ response.put("timestamp", System.currentTimeMillis());
|
|
|
+ session.sendMessage(new TextMessage(response.toJSONString()));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
|
|
+ String sessionId = session.getId();
|
|
|
+ SESSION_MAP.remove(sessionId);
|
|
|
+ subscriptionManager.removeSession(sessionId);
|
|
|
+ log.info("WebSocket 连接关闭: sessionId={}, closeStatus={}", sessionId, status);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void handleTransportError(WebSocketSession session, Throwable exception) {
|
|
|
+ String sessionId = session.getId();
|
|
|
+ SESSION_MAP.remove(sessionId);
|
|
|
+ subscriptionManager.removeSession(sessionId);
|
|
|
+ log.error("WebSocket 传输异常: sessionId={}", sessionId, exception);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送成功/失败响应
|
|
|
+ */
|
|
|
+ private void sendResponse(WebSocketSession session, String type, boolean success, Object data, String msg)
|
|
|
+ throws IOException {
|
|
|
+ JSONObject response = new JSONObject();
|
|
|
+ response.put("type", type);
|
|
|
+ response.put("success", success);
|
|
|
+ if (data != null) {
|
|
|
+ response.put("data", data);
|
|
|
+ }
|
|
|
+ if (msg != null) {
|
|
|
+ response.put("msg", msg);
|
|
|
+ }
|
|
|
+ session.sendMessage(new TextMessage(response.toJSONString()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送错误响应
|
|
|
+ */
|
|
|
+ private void sendError(WebSocketSession session, String msg) throws IOException {
|
|
|
+ JSONObject response = new JSONObject();
|
|
|
+ response.put("type", "error");
|
|
|
+ response.put("success", false);
|
|
|
+ response.put("msg", msg);
|
|
|
+ session.sendMessage(new TextMessage(response.toJSONString()));
|
|
|
+ }
|
|
|
+}
|