This commit is contained in:
tangzh 2025-05-27 23:17:27 +08:00
parent c429203106
commit 51a74d15be
14 changed files with 291 additions and 368 deletions

View File

@ -5,7 +5,7 @@
"author": "Zheng Jie",
"license": "Apache-2.0",
"scripts": {
"dev": "vue-cli-service serve",
"dev": "SET NODE_OPTIONS=--openssl-legacy-provider && vue-cli-service serve",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",

View File

@ -1,33 +1,33 @@
///*
// * Copyright 2019-2025 Tz
// *
// * Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * http://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// */
//package me.zhengjie.config.webConfig;
//
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.web.socket.server.standard.ServerEndpointExporter;
//
///**
// * @author Tz
// * @date 2019-08-24 15:44
// */
//@Configuration
//public class WebSocketConfig {
//
// @Bean
// public ServerEndpointExporter serverEndpointExporter() {
// return new ServerEndpointExporter();
// }
//}
/*
* Copyright 2019-2025 Tz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.config.webConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @author Tz
* @date 2019-08-24 15:44
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

View File

@ -23,7 +23,10 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.ApplicationPidFileWriter;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.bind.annotation.RestController;

View File

@ -92,7 +92,8 @@ public class SpringSecurityConfig {
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/webSocket/**"
"/webSocket/**",
"/sdcp/**"
).permitAll()
// swagger 文档
.antMatchers("/swagger-ui.html").permitAll()

View File

@ -1,44 +0,0 @@
package me.zhengjie.modules.security.config;
import me.zhengjie.modules.security.config.handler.DeviceWebSocketHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
/**
* WebSocket配置类启用WebSocket功能并注册处理器
*/
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private DeviceWebSocketHandler deviceWebSocketHandler;
/**
* 注册WebSocket处理器配置端点和允许的源
* @param registry WebSocket处理器注册器
*/
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(deviceWebSocketHandler, "/ws/device")
.setAllowedOrigins("*");
}
/**
* 配置WebSocket容器参数设置消息缓冲区大小
* @return WebSocket容器工厂Bean
*/
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2019-2025 Tz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.modules.security.config.enums;
/**
* @author ZhangHouYing
* @date 2019-08-10 9:56
*/
public enum MsgType {
/** 连接 */
CONNECT,
/** 关闭 */
CLOSE,
/** 信息 */
INFO,
/** 错误 */
ERROR
}

View File

@ -1,77 +0,0 @@
package me.zhengjie.modules.security.config.handler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
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.concurrent.ConcurrentHashMap;
/**
* 处理客户端WebSocket连接的处理器
* 管理客户端会话并转发设备状态变化
*/
@Component
public class DeviceWebSocketHandler extends TextWebSocketHandler {
@Autowired @Lazy
private ThirdPartyWebSocketClient thirdPartyClient;
// 存储所有活跃的WebSocket会话键为会话ID
private final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
/**
* 处理来自客户端的文本消息
* @param session 客户端会话
* @param message 收到的消息
* @throws IOException 发送消息失败时抛出
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
String payload = message.getPayload();
// 将客户端指令转发给第三方WebSocket服务
thirdPartyClient.sendCommand(payload);
}
/**
* 客户端连接建立后的回调方法
* @param session 新建立的会话
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) {
sessions.put(session.getId(), session);
// 可在此处添加连接初始化逻辑
}
/**
* 客户端连接关闭后的回调方法
* @param session 关闭的会话
* @param status 关闭状态
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session.getId());
}
/**
* 通知所有客户端设备状态变化
* @param deviceId 设备ID
* @param status 设备状态
*/
public void notifyDeviceChange(String deviceId, String status) {
sessions.forEach((id, session) -> {
try {
// 向客户端发送JSON格式的设备状态消息
session.sendMessage(new TextMessage(
String.format("{\"deviceId\": \"%s\", \"status\": \"%s\"}", deviceId, status)
));
} catch (IOException e) {
e.printStackTrace();
}
});
}
}

View File

@ -1,148 +0,0 @@
package me.zhengjie.modules.security.config.handler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.client.WebSocketClient;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* 与第三方WebSocket服务通信的客户端
* 负责建立连接订阅设备和处理消息
*/
@Component
public class ThirdPartyWebSocketClient extends TextWebSocketHandler {
@Value("${thirdparty.ws.url}")
private String thirdPartyWsUrl;
@Autowired @Lazy
private DeviceWebSocketHandler deviceHandler;
private WebSocketSession session;
// 存储已订阅的设备键为设备ID值为订阅状态
private final ConcurrentHashMap<String, String> subscribedDevices = new ConcurrentHashMap<>();
private final ScheduledExecutorService reconnectExecutor = Executors.newSingleThreadScheduledExecutor();
/**
* Bean初始化后连接到第三方WebSocket服务
*/
@PostConstruct
public void init() {
// connect();
}
/**
* 建立与第三方WebSocket服务的连接
*/
private void connect() {
WebSocketClient client = new StandardWebSocketClient();
try {
session = client.doHandshake(this, thirdPartyWsUrl).get();
System.out.println("Connected to third-party WebSocket server");
} catch (Exception e) {
e.printStackTrace();
// 连接失败安排重连
scheduleReconnect();
}
}
/**
* 安排重连任务
*/
private void scheduleReconnect() {
reconnectExecutor.schedule(this::connect, 5, TimeUnit.SECONDS);
}
/**
* 订阅指定设备的状态变化
* @param deviceId 设备ID
*/
public void subscribeDevice(String deviceId) {
if (session != null && session.isOpen()) {
try {
// 发送订阅消息到第三方服务
session.sendMessage(new TextMessage(
String.format("{\"action\": \"subscribe\", \"deviceId\": \"%s\"}", deviceId)
));
subscribedDevices.put(deviceId, "subscribed");
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 向第三方服务发送指令
* @param command 指令内容
*/
public void sendCommand(String command) {
if (session != null && session.isOpen()) {
try {
session.sendMessage(new TextMessage(command));
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 处理从第三方服务接收到的文本消息
* @param session 会话
* @param message 收到的消息
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
String payload = message.getPayload();
// 处理设备状态变化消息
processDeviceStatusChange(payload);
}
/**
* 解析设备状态变化消息并通知客户端
* @param payload 消息内容
*/
private void processDeviceStatusChange(String payload) {
// 解析消息并提取设备ID和状态
String deviceId = extractDeviceId(payload);
String status = extractStatus(payload);
if (deviceId != null && status != null) {
// 通知所有连接的客户端设备状态变化
deviceHandler.notifyDeviceChange(deviceId, status);
}
}
/**
* 从消息中提取设备ID
* 需要根据第三方服务的消息格式实现具体解析逻辑
* @param payload 消息内容
* @return 设备ID
*/
private String extractDeviceId(String payload) {
// TODO: 实现JSON解析逻辑
return null;
}
/**
* 从消息中提取设备状态
* 需要根据第三方服务的消息格式实现具体解析逻辑
* @param payload 消息内容
* @return 设备状态
*/
private String extractStatus(String payload) {
// TODO: 实现JSON解析逻辑
return null;
}
}

View File

@ -1,31 +1,36 @@
package me.zhengjie.modules.system.rest;
import me.zhengjie.modules.system.service.socket.DeviceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
@RestController
@RequestMapping("/api/printer")
@Controller
public class BusDeviceSocketController {
@Autowired DeviceService deviceService;
/**
* 订阅设备状态变化的HTTP接口
* @param deviceId 设备ID
*/
@PostMapping("/{deviceId}/subscribe")
public void subscribe(@PathVariable String deviceId) {
deviceService.subscribeToDevice(deviceId);
// 处理客户端发送到/app/send的消息
@MessageMapping("/hello") // 接收路径/app/send
@SendTo("/topic/greetings") // 响应路径/topic/messages
public String handleMessage(String message) {
System.out.println("Received message: " + message);
return "Message received: " + message;
}
/**
* 向设备发送指令的HTTP接口
* @param deviceId 设备ID
* @param command 指令内容
*/
@PostMapping("/{deviceId}/command")
public void sendCommand(@PathVariable String deviceId, @RequestBody String command) {
deviceService.sendDeviceCommand(deviceId, command);
}
// /**
// * 订阅设备状态变化的HTTP接口
// * @param deviceId 设备ID
// */
// @PostMapping("/{deviceId}/subscribe")
// public void subscribe(@PathVariable String deviceId) {
// deviceService.subscribeToDevice(deviceId);
// }
//
// /**
// * 向设备发送指令的HTTP接口
// * @param deviceId 设备ID
// * @param command 指令内容
// */
// @PostMapping("/{deviceId}/command")
// public void sendCommand(@PathVariable String deviceId, @RequestBody String command) {
// deviceService.sendDeviceCommand(deviceId, command);
// }
}

View File

@ -1,37 +0,0 @@
package me.zhengjie.modules.system.service.socket;
import me.zhengjie.modules.security.config.handler.ThirdPartyWebSocketClient;
import org.springframework.stereotype.Service;
/**
* 设备管理服务提供与设备相关的业务逻辑
*/
@Service
public class DeviceService {
private final ThirdPartyWebSocketClient webSocketClient;
public DeviceService(ThirdPartyWebSocketClient webSocketClient) {
this.webSocketClient = webSocketClient;
}
/**
* 订阅设备状态变化
* @param deviceId 设备ID
*/
public void subscribeToDevice(String deviceId) {
webSocketClient.subscribeDevice(deviceId);
}
/**
* 向设备发送指令
* @param deviceId 设备ID
* @param command 指令内容
*/
public void sendDeviceCommand(String deviceId, String command) {
// 构建符合第三方服务格式的指令
String formattedCommand = String.format("{\"deviceId\": \"%s\", \"command\": \"%s\"}",
deviceId, command);
webSocketClient.sendCommand(formattedCommand);
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2019-2025 Tz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.modules.system.service.webstocket;
import lombok.Data;
import me.zhengjie.modules.security.config.enums.MsgType;
/**
* @author ZhangHouYing
* @date 2019-08-10 9:55
*/
@Data
public class SocketMsg {
private String msg;
private MsgType msgType;
public SocketMsg(String msg, MsgType msgType) {
this.msg = msg;
this.msgType = msgType;
}
}

View File

@ -0,0 +1,143 @@
/*
* Copyright 2019-2025 Tz
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.zhengjie.modules.system.service.webstocket;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArraySet;
@ServerEndpoint("/sdcp/{method}/{mainboardId}")
@Slf4j
@Component
public class WebSocketServer {
/**
* concurrent包的线程安全Set用来存放每个客户端对应的MyWebSocket对象
*/
private static final CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();
/**
* 与某个客户端的连接会话需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收sid
*/
private String sid = "";
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("method") String method, @PathParam("mainboardId") String mainboardId) {
this.session = session;
//如果存在就先删除一个防止重复推送消息
webSocketSet.removeIf(webSocket -> webSocket.sid.equals(mainboardId));
webSocketSet.add(this);
this.sid = mainboardId;
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session, @PathParam("method") String method, @PathParam("mainboardId") String mainboardId) {
log.info("收到来" + sid + "的信息:" + message);
if ("ping".equals(message)) {
try {
sendMessage("pong");
} catch (IOException e) {
e.printStackTrace();
}
} else {
// 群发消息
for (WebSocketServer item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
}
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误", error);
}
/**
* 实现服务器主动推送
*/
private void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/**
* 群发自定义消息
*/
public static void sendInfo(SocketMsg socketMsg, @PathParam("mainboardId") String mainboardId) throws IOException {
String message = JSON.toJSONString(socketMsg);
log.info("推送消息到" + mainboardId + ",推送内容:" + message);
for (WebSocketServer item : webSocketSet) {
try {
//这里可以设定只推送给这个sid的为null则全部推送
if (mainboardId == null) {
item.sendMessage(message);
} else if (item.sid.equals(mainboardId)) {
item.sendMessage(message);
}
} catch (IOException ignored) {
}
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
WebSocketServer that = (WebSocketServer) o;
return Objects.equals(session, that.session) &&
Objects.equals(sid, that.sid);
}
@Override
public int hashCode() {
return Objects.hash(session, sid);
}
}

View File

@ -107,10 +107,16 @@ weChat:
appId: wx583d0c321ef43dfe
secret: 1b5eca11a1058361b0f14c5933ef6bd6
# socket地址
thirdparty:
ws:
url: ws://127.0.0.1:3030/websocket
# socket
ws:
url: ws://127.0.0.1:8000/webSocket
server:
# 客户端心跳开关
clientSwitch: false
# 间隔时间 单位:秒
interval: 10
# 最大错误次数
maxMissCount: 10
# 文件存储路径
file:

View File

@ -91,6 +91,12 @@
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.0.2</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>com.baomidou</groupId>