From c38b35bc968b876b478f149f69291b6386cbe63b Mon Sep 17 00:00:00 2001
From: zhaowenyu <35589726@qq.com>
Date: Tue, 28 Oct 2025 15:04:51 +0800
Subject: [PATCH] 2025-10-28 zwy 更新WebSocket代码。

---
 ard-sample/src/main/java/com/ard/sample/component/mqtt/model/EventsOutputReceiver.java        |    3 
 ard-sample/src/main/java/com/ard/sample/component/mqtt/model/EventsReceiver.java              |    2 
 ard-sample/src/main/java/com/ard/sample/component/websocket/model/Payload.java                |   24 ++++
 ard-sample/src/main/java/com/ard/sample/component/websocket/config/WebSocketDockConfig.java   |   33 +++-
 ard-sample/src/main/java/com/ard/sample/component/websocket/handler/DockWebSocketHandler.java |  189 +++++++++++++++++++++++++++++++
 ard-sample/src/main/java/com/ard/sample/component/websocket/enums/MessageType.java            |   27 ++++
 ard-sample/src/main/java/com/ard/sample/component/websocket/model/WebSocketMessage.java       |   25 ++++
 7 files changed, 292 insertions(+), 11 deletions(-)

diff --git a/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/EventsOutputReceiver.java b/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/EventsOutputReceiver.java
index 46447fd..0d90eb0 100644
--- a/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/EventsOutputReceiver.java
+++ b/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/EventsOutputReceiver.java
@@ -1,5 +1,6 @@
 package com.ard.sample.component.mqtt.model;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.Data;
 
 /**
@@ -10,7 +11,9 @@
 @Data
 public class EventsOutputReceiver {
 
+    @JsonProperty("status")
     private String status;
 
+    @JsonProperty("progress")
     private OutputProgressReceiver progress;
 }
diff --git a/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/EventsReceiver.java b/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/EventsReceiver.java
index e1f6f86..c1ddad7 100644
--- a/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/EventsReceiver.java
+++ b/ard-sample/src/main/java/com/ard/sample/component/mqtt/model/EventsReceiver.java
@@ -26,4 +26,6 @@
 
     private String sn;
 
+    private String type;
+
 }
diff --git a/ard-sample/src/main/java/com/ard/sample/component/websocket/config/WebSocketDockConfig.java b/ard-sample/src/main/java/com/ard/sample/component/websocket/config/WebSocketDockConfig.java
index da048be..f6df97e 100644
--- a/ard-sample/src/main/java/com/ard/sample/component/websocket/config/WebSocketDockConfig.java
+++ b/ard-sample/src/main/java/com/ard/sample/component/websocket/config/WebSocketDockConfig.java
@@ -1,18 +1,29 @@
 package com.ard.sample.component.websocket.config;
 
-import org.springframework.context.annotation.Bean;
+import com.ard.sample.component.websocket.handler.*;
+import com.ard.sample.component.websocket.interceptor.DockHandshakeInterceptor;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Configuration;
-import org.springframework.web.socket.server.standard.ServerEndpointExporter;
+import org.springframework.web.socket.config.annotation.EnableWebSocket;
+import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
+import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
 
-/**
- * 启用WebSocket支持,注册端点处理器
- */
 @Configuration
-public class WebSocketDockConfig {
+@EnableWebSocket // 启用原生WebSocket支持
+public class WebSocketDockConfig implements WebSocketConfigurer {
 
-    // 自动注册@ServerEndpoint注解的类
-    @Bean
-    public ServerEndpointExporter serverEndpointExporter() {
-        return new ServerEndpointExporter();
+    @Autowired
+    private DockWebSocketHandler dockWebSocketHandler;
+
+    // 注册WebSocket处理器和路径
+    @Override
+    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
+        // 1. 注册处理器:路径 "/ws/dock/**" 由 DockWebSocketHandler 处理
+        // 2. 添加握手拦截器:验证用户身份DockHandshakeInterceptor
+        // 3. 配置跨域:允许所有域名(生产环境需限制)
+        registry.addHandler(dockWebSocketHandler, "/ws/dock/**")
+                .addInterceptors(new DockHandshakeInterceptor())
+                .setAllowedOrigins("*");
+
     }
-}
\ No newline at end of file
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/component/websocket/enums/MessageType.java b/ard-sample/src/main/java/com/ard/sample/component/websocket/enums/MessageType.java
new file mode 100644
index 0000000..8d488f3
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/component/websocket/enums/MessageType.java
@@ -0,0 +1,27 @@
+package com.ard.sample.component.websocket.enums;
+
+public enum MessageType {
+    STATUS("status"),
+    CONTROL("control"),
+    PROGRESS("progress"),
+    ALL("all");
+
+    private final String type;
+
+    MessageType(String type) {
+        this.type = type;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public static MessageType fromString(String type) {
+        for (MessageType messageType : MessageType.values()) {
+            if (messageType.type.equalsIgnoreCase(type)) {
+                return messageType;
+            }
+        }
+        throw new IllegalArgumentException("Unknown message type: " + type);
+    }
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/component/websocket/handler/DockWebSocketHandler.java b/ard-sample/src/main/java/com/ard/sample/component/websocket/handler/DockWebSocketHandler.java
new file mode 100644
index 0000000..98c1618
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/component/websocket/handler/DockWebSocketHandler.java
@@ -0,0 +1,189 @@
+package com.ard.sample.component.websocket.handler;
+
+import com.alibaba.fastjson2.JSON;
+import com.ard.sample.component.websocket.model.Payload;
+import com.ard.sample.component.websocket.model.WebSocketMessage;
+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.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class DockWebSocketHandler extends TextWebSocketHandler {
+
+    // 存储用户会话:userId -> WebSocketSession
+    private final Map<String, WebSocketSession> userSessions = new ConcurrentHashMap<>();
+
+    // 订阅关系:key=订阅标识(sn+type),value=订阅用户ID集合
+    private final Map<String, Set<String>> subscriptions = new ConcurrentHashMap<>();
+
+    // 关键:为每个WebSocketSession分配独立的锁对象,确保写操作同步
+    private final Map<WebSocketSession, Object> sessionLocks = new ConcurrentHashMap<>();
+
+    /**
+     * 连接建立后回调
+     */
+    @Override
+    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
+        // 从会话属性中获取认证后的用户ID
+        String userId = (String) session.getAttributes().get("userId");
+        if (userId == null) {
+            session.close(CloseStatus.POLICY_VIOLATION.withReason("未认证"));
+            return;
+        }
+
+        userSessions.put(userId, session);
+        // 为新会话初始化锁对象
+        sessionLocks.put(session, new Object());
+        System.out.println("用户[" + userId + "]已连接,当前在线人数:" + userSessions.size());
+    }
+
+    /**
+     * 处理客户端发送的消息
+     */
+    @Override
+    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
+        String userId = (String) session.getAttributes().get("userId");
+        String jsonMessage = message.getPayload();
+
+        try {
+            // 解析客户端消息
+            WebSocketMessage webSocketMessage = JSON.parseObject(jsonMessage, WebSocketMessage.class);
+            String type = webSocketMessage.getType();
+            Payload payload = webSocketMessage.getPayload();
+
+            // 验证消息类型是否合法
+            if (!Arrays.asList("status", "control", "progress").contains(type)) {
+                session.sendMessage(new TextMessage("不支持的消息类型:" + type));
+                return;
+            }
+
+            // 处理订阅/取消订阅
+            handleSubscription(userId, type, payload);
+
+        } catch (Exception e) {
+            System.err.println("消息处理错误:" + e.getMessage());
+            session.sendMessage(new TextMessage("消息格式错误"));
+        }
+    }
+
+    /**
+     * 处理订阅/取消订阅逻辑
+     */
+    private void handleSubscription(String userId, String type, Payload payload) {
+        String sn = payload.getSn();
+        boolean subscribe = payload.isSubscribe();
+        String subscriptionKey = getSubscriptionKey(sn, type); // 生成唯一订阅标识
+
+        if (subscribe) {
+            // 订阅操作
+            subscriptions.computeIfAbsent(subscriptionKey, k -> ConcurrentHashMap.newKeySet()).add(userId);
+            System.out.println("用户[" + userId + "]订阅了 设备[" + sn + "]的[" + type + "]类型消息");
+        } else {
+            // 取消订阅操作
+            if (subscriptions.containsKey(subscriptionKey)) {
+                subscriptions.get(subscriptionKey).remove(userId);
+                if (subscriptions.get(subscriptionKey).isEmpty()) {
+                    subscriptions.remove(subscriptionKey);
+                }
+                System.out.println("用户[" + userId + "]取消订阅了 设备[" + sn + "]的[" + type + "]类型消息");
+            }
+        }
+    }
+
+    /**
+     * 连接关闭后回调
+     */
+    @Override
+    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
+        String userId = (String) session.getAttributes().get("userId");
+        if (userId != null) {
+            userSessions.remove(userId);
+            sessionLocks.remove(session); // 移除会话锁
+            // 清除该用户的所有订阅
+            removeUserAllSubscriptions(userId);
+            System.out.println("用户[" + userId + "]已断开连接,当前在线人数:" + userSessions.size());
+        }
+    }
+
+    /**
+     * 清除用户的所有订阅
+     */
+    private void removeUserAllSubscriptions(String userId) {
+        for (Map.Entry<String, Set<String>> entry : subscriptions.entrySet()) {
+            entry.getValue().remove(userId);
+            if (entry.getValue().isEmpty()) {
+                subscriptions.remove(entry.getKey());
+            }
+        }
+    }
+
+    /**
+     * 向订阅了特定设备特定类型的用户发送消息
+     */
+    public void sendMessageToSubscribers(String sn, String type, String content) {
+        String subscriptionKey = getSubscriptionKey(sn, type);
+        Set<String> subscriberIds = subscriptions.getOrDefault(subscriptionKey, new HashSet<>());
+
+        if (subscriberIds.isEmpty()) {
+            //System.out.println("没有用户订阅 设备[" + sn + "]的[" + type + "]类型消息");
+            return;
+        }
+
+        try {
+            // 构建发送的消息
+//            WebSocketMessage message = new WebSocketMessage();
+//            message.setType(type);
+//
+//            Payload payload = new Payload();
+//            payload.setSn(sn);
+//            payload.setSubscribe(true); // 仅作为标识,无实际意义
+//            message.setPayload(payload);
+//
+//            String jsonMessage = JSON.toJSONString(message);
+
+            // 向所有订阅者发送消息
+            for (String userId : subscriberIds) {
+                WebSocketSession session = userSessions.get(userId);
+                // 1. 检查会话是否有效
+                if (session == null || !session.isOpen()) {
+                    // 移除无效会话
+                    userSessions.remove(userId);
+                    sessionLocks.remove(session);
+                    continue;
+                }
+
+                // 2. 获取当前会话的锁对象,确保同步写操作
+                Object lock = sessionLocks.get(session);
+                if (lock == null) {
+                    continue; // 理论上不会出现,防止NPE
+                }
+
+                // 3. 同步发送消息,避免并发冲突
+                synchronized (lock) {
+                    session.sendMessage(new TextMessage(content));
+                    System.out.println("向用户[" + userId + "]发送 设备[" + sn + "]的[" + type + "]类型消息:" + content);
+                }
+//                if (session != null && session.isOpen()) {
+//                    session.sendMessage(new TextMessage(content));
+//                    System.out.println("向用户[" + userId + "]发送 设备[" + sn + "]的[" + type + "]类型消息");
+//                    System.out.println(content);
+//                }
+            }
+        } catch (IOException e) {
+            System.err.println("发送消息错误:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 生成订阅标识(sn+type的组合)
+     */
+    private String getSubscriptionKey(String sn, String type) {
+        return sn + ":" + type;
+    }
+}
\ No newline at end of file
diff --git a/ard-sample/src/main/java/com/ard/sample/component/websocket/model/Payload.java b/ard-sample/src/main/java/com/ard/sample/component/websocket/model/Payload.java
new file mode 100644
index 0000000..cbe293f
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/component/websocket/model/Payload.java
@@ -0,0 +1,24 @@
+package com.ard.sample.component.websocket.model;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 消息内容实体类
+ * */
+@Data
+//@Builder
+public class Payload {
+
+//    public Payload() {}
+
+    /**
+     * 设备序列号
+     * */
+    private String sn;
+
+    /**
+     * 是否订阅
+     * */
+    private boolean subscribe;
+}
diff --git a/ard-sample/src/main/java/com/ard/sample/component/websocket/model/WebSocketMessage.java b/ard-sample/src/main/java/com/ard/sample/component/websocket/model/WebSocketMessage.java
new file mode 100644
index 0000000..2b90c9a
--- /dev/null
+++ b/ard-sample/src/main/java/com/ard/sample/component/websocket/model/WebSocketMessage.java
@@ -0,0 +1,25 @@
+package com.ard.sample.component.websocket.model;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 消息实体类
+ * */
+@Data
+//@Builder
+public class WebSocketMessage {
+
+//    public WebSocketMessage() {}
+
+    /**
+     * 访问类型,例如:status、control、progress等
+     * */
+    private String type;
+
+    /**
+     * 访问的消息实体类
+     * */
+    private Payload payload;
+    //private String userId; // 服务器端添加的用户ID
+}

--
Gitblit v1.9.3