From 9cbe312933f125ee386ee8a809584773350f57df Mon Sep 17 00:00:00 2001 From: tangw Date: Sun, 25 Jan 2026 14:07:39 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B4=A8=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SpringContextWebSocketConfigurator.java | 50 +++++++++++ .../websocket/ChatWebSocketServer.java | 77 +++++++++++++++-- .../websocket/WebSocketMessage.java | 1 + .../websocket/WebSocketMsgServiceImpl.java | 83 +++++++++++++++---- .../websocket/WebSocketUser.java | 27 ++++-- .../controller/dict/DictController.java | 18 ++-- .../zycx/service/impl/ZycxServiceImpl.java | 26 +++++- 7 files changed, 243 insertions(+), 39 deletions(-) create mode 100644 lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/config/SpringContextWebSocketConfigurator.java diff --git a/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/config/SpringContextWebSocketConfigurator.java b/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/config/SpringContextWebSocketConfigurator.java new file mode 100644 index 0000000..0c66d40 --- /dev/null +++ b/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/config/SpringContextWebSocketConfigurator.java @@ -0,0 +1,50 @@ +package com.czlis.interfaceCommon.config; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +import javax.websocket.server.ServerEndpointConfig; + +/** + * 独立的WebSocket Spring上下文配置器 + * 作用:解决WebSocket端点中获取Spring Bean的问题,替代过时的SpringConfigurator + * 适配:SpringBoot 2.x + 若依框架 + Tomcat 8/9/10 + */ +@Component // 让Spring扫描并初始化该类,获取上下文 +public class SpringContextWebSocketConfigurator extends ServerEndpointConfig.Configurator implements ApplicationContextAware { + + // 静态持有Spring上下文(全局唯一) + private static ApplicationContext applicationContext; + + /** + * 实现ApplicationContextAware接口,Spring启动时自动注入上下文 + */ + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + // 仅初始化一次,避免多次赋值 + if (applicationContext == null) { + applicationContext = context; + } + } + + /** + * 重写创建端点实例的核心方法 + * 从Spring容器中获取WebSocket端点实例,保证Bean注入生效 + */ + @Override + public T getEndpointInstance(Class clazz) throws InstantiationException { + // 双重校验:上下文存在 + 容器中有该Bean + if (applicationContext != null) { + try { + return applicationContext.getBean(clazz); + } catch (BeansException e) { + // 容器中无该Bean时,降级使用默认实例化逻辑(兼容测试场景) + return super.getEndpointInstance(clazz); + } + } + // 上下文未初始化时,走父类默认逻辑 + return super.getEndpointInstance(clazz); + } +} \ No newline at end of file diff --git a/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/ChatWebSocketServer.java b/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/ChatWebSocketServer.java index 832cf6c..a7cad74 100644 --- a/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/ChatWebSocketServer.java +++ b/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/ChatWebSocketServer.java @@ -4,7 +4,9 @@ import com.alibaba.fastjson2.JSON; import com.czlis.common.core.domain.model.LoginUser; import com.czlis.common.utils.SecurityUtils; import com.czlis.common.utils.spring.SpringUtils; +import com.czlis.interfaceCommon.config.SpringContextWebSocketConfigurator; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; import org.springframework.web.socket.server.standard.SpringConfigurator; import javax.websocket.*; @@ -24,7 +26,8 @@ import java.util.concurrent.ConcurrentHashMap; * 工具方法暴露:在 WebSocket 端点类中提供静态工具方法(如 sendToUser、broadcast),其他业务直接调用该方法,无需关心实例创建; * 避免重复连接:连接的创建 / 销毁由 WebSocket 协议自动管理(@OnOpen 新增、@OnClose 移除),业务层不干预实例创建。 */ -@ServerEndpoint(value = "/ws/msgserver/{userId}", configurator = SpringConfigurator.class) +@Component +@ServerEndpoint(value = "/ws/msgserver/{userId}", configurator = SpringContextWebSocketConfigurator.class) public class ChatWebSocketServer { // 🌟 核心固定参数白名单:这些参数不存入动态属性(单独解析为固定身份属性) private static final Set CORE_PARAMS = new HashSet<>(Arrays.asList("token", "username", "role")); @@ -94,22 +97,47 @@ public class ChatWebSocketServer { try { // 1. 基础校验:消息为空/连接关闭 → 直接返回 if (message == null || message.trim().isEmpty() || !session.isOpen()) { + System.out.println("消息为空或连接已关闭,忽略处理:userId=" + userId); return; } - // 2. 解析消息为统一协议对象(JSON解析) - WebSocketMessage wsMessage = JSON.parseObject(message, WebSocketMessage.class); - if (wsMessage == null) { - sendToUser(userId, "错误:消息格式不合法,请按JSON格式发送"); + // 2. 前置校验:判断是否为合法JSON(避免非JSON字符串直接解析) + String trimMsg = message.trim(); + if (!isValidJson(trimMsg)) { + String errorMsg = "错误:消息格式必须是JSON字符串,当前消息:" + trimMsg; + sendToUser(userId, errorMsg); + System.out.println("非JSON消息被拒绝:userId=" + userId + ",消息=" + trimMsg); return; } - //暂时使用默认时间戳,生产环境应该使用客户端必传字段 + + // 3. 解析JSON为消息对象(捕获解析异常) + WebSocketMessage wsMessage; + try { + wsMessage = JSON.parseObject(trimMsg, WebSocketMessage.class); + } catch (Exception e) { + String errorMsg = "错误:JSON格式不合法,无法解析为WebSocketMessage,原因:" + e.getMessage(); + sendToUser(userId, errorMsg); + System.out.println("JSON解析失败:userId=" + userId + ",消息=" + trimMsg + ",原因=" + e.getMessage()); + return; + } + + // 4. 校验解析后的对象是否为空 + if (wsMessage == null) { + String errorMsg = "错误:消息解析后为空,请检查JSON格式"; + sendToUser(userId, errorMsg); + return; + } + + // 5. 补充默认值(时间戳/发送者ID) if (wsMessage.getTimestamp() == null || wsMessage.getTimestamp() == 0) { wsMessage.setTimestamp(System.currentTimeMillis()); } - // 3. 补充发送者ID(从路径参数获取,避免客户端伪造) wsMessage.setSenderId(userId); + // 6. 生成唯一消息ID(避免重试队列重复) + if (wsMessage.getMsgId() == null || wsMessage.getMsgId().isEmpty()) { + wsMessage.setMsgId(userId + "_" + System.currentTimeMillis()); + } // 4. 获取Spring管理的业务层Bean,转发消息处理 WebSocketMsgService msgService = SpringUtils.getBean(WebSocketMsgService.class); if (msgService == null) { @@ -190,7 +218,26 @@ public class ChatWebSocketServer { } return roleUsers; } - + /** + * 根据登录IP筛选在线用户(精确匹配) + * @param loginIp 要查询的登录IP(如 "192.168.1.100") + * @return 匹配该IP的在线且连接有效的用户列表 + */ + public static List getOnlineUsersByLoginIp(String loginIp) { + List ipUsers = new ArrayList<>(); + // 前置空值校验:避免传入null/空字符串导致无效查询 + if (loginIp == null || loginIp.trim().isEmpty()) { + return ipUsers; + } + // 遍历在线用户,筛选匹配IP且连接有效的用户 + for (WebSocketUser user : ONLINE_USER_MAP.values()) { + // 双重校验:IP精确匹配 + 连接有效 + if (loginIp.equals(user.getLoginIp()) && user.isConnected()) { + ipUsers.add(user); + } + } + return ipUsers; + } /** * 获取在线用户的userId列表(兼容原有逻辑) */ @@ -291,5 +338,19 @@ public class ChatWebSocketServer { user.removeDynamicAttr(attrKey); return true; } + private boolean isValidJson(String jsonStr) { + // 1. 先过滤空值,避免无意义解析 + if (jsonStr == null || jsonStr.trim().isEmpty()) { + return false; + } + try { + // 2. 核心:用 FastJSON2 解析为 JSON 对象/数组,解析成功则合法 + JSON.parse(jsonStr); + return true; + } catch (Exception e) { + // 3. 解析失败(抛出 JSONException),说明非合法 JSON + return false; + } + } } diff --git a/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/WebSocketMessage.java b/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/WebSocketMessage.java index f9f3f75..1f02285 100644 --- a/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/WebSocketMessage.java +++ b/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/WebSocketMessage.java @@ -11,6 +11,7 @@ import lombok.NoArgsConstructor; @AllArgsConstructor @NoArgsConstructor public class WebSocketMessage { + private String msgId; // 消息类型(如CHAT/NOTICE/ORDER/HEARTBEAT) private String msgType; // 业务数据(JSON字符串,可解析为对应业务DTO) diff --git a/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/WebSocketMsgServiceImpl.java b/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/WebSocketMsgServiceImpl.java index b5f4ce0..2b7b11f 100644 --- a/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/WebSocketMsgServiceImpl.java +++ b/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/WebSocketMsgServiceImpl.java @@ -1,17 +1,11 @@ package com.czlis.interfaceCommon.websocket; import com.alibaba.fastjson2.JSON; -import com.czlis.common.annotation.Async; import com.czlis.common.core.redis.RedisCache; import com.czlis.common.utils.spring.SpringUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; import javax.websocket.Session; import java.util.Map; -import java.util.concurrent.Executor; -import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /**业务层(Spring Bean)专注于「消息解析、权限校验、业务逻辑处理、结果推送」,支持不同类型消息的差异化处理。 @@ -32,6 +26,7 @@ public class WebSocketMsgServiceImpl implements WebSocketMsgService { ChatWebSocketServer.sendToUser(wsMessage.getSenderId(), "提示:消息已处理,无需重复发送"); return; } + System.err.println("wsMessage=" + wsMessage); try { // 1. 基础校验:消息类型、发送者ID String msgType = wsMessage.getMsgType(); @@ -170,22 +165,82 @@ public class WebSocketMsgServiceImpl implements WebSocketMsgService { private void handleHeartbeatMsg(WebSocketMessage wsMessage) { String senderId = wsMessage.getSenderId(); // 回复心跳确认 - ChatWebSocketServer.sendToUser(senderId, JSON.toJSONString(new WebSocketMessage("HEARTBEAT_ACK", "pong", senderId, null, System.currentTimeMillis()))); + ChatWebSocketServer.sendToUser(senderId, JSON.toJSONString(new WebSocketMessage(senderId + "_" + System.currentTimeMillis(),"HEARTBEAT_ACK", "pong", senderId, null, System.currentTimeMillis()))); } /** - * 示例3:给在线用户设置动态属性(如设备类型) + * 示例方法:给在线用户设置动态属性(如设备类型),优化后版本 */ public void setClientInfo(WebSocketMessage wsMessage) { + // 1. 前置空值校验:避免空指针异常 + if (wsMessage == null) { + System.err.println("setClientInfo失败:wsMessage为null"); + return; + } + String senderId = wsMessage.getSenderId(); String data = wsMessage.getData(); - Map dataStrMap = JSON.parseObject(data, Map.class); + + // 校验核心参数 + if (senderId == null || senderId.trim().isEmpty()) { + System.err.println("setClientInfo失败:senderId为空"); + return; + } + if (data == null || data.trim().isEmpty()) { + System.err.println("setClientInfo失败:data为空,senderId=" + senderId); + return; + } + + // 2. 安全解析JSON:捕获解析异常 + Map dataStrMap; + try { + dataStrMap = JSON.parseObject(data, Map.class); + } catch (Exception e) { + System.err.println("setClientInfo失败:JSON解析异常,senderId=" + senderId + ",data=" + data + ",异常:" + e.getMessage()); + return; + } + + // 3. 获取在线用户并校验有效性 WebSocketUser user = ChatWebSocketServer.getOnlineUser(senderId); - if (user != null) { - for (Map.Entry entry : dataStrMap.entrySet()) { - String key = entry.getKey(); - String value =(String)entry.getValue(); - user.setDynamicAttr(key, value); + if (user == null) { + System.err.println("setClientInfo失败:未找到在线用户,senderId=" + senderId); + return; + } +// // 额外校验连接是否有效 +// if (!user.isConnected()) { +// System.err.println("setClientInfo失败:用户连接已关闭,senderId=" + senderId); +// // 可选:移除离线用户 +// ChatWebSocketServer.removeOnlineUser(senderId); +// return; +// } + + // 4. 遍历更新属性:优化判断逻辑,避免重复赋值 + for (Map.Entry entry : dataStrMap.entrySet()) { + String key = entry.getKey(); + // 空值校验:避免key/value为null导致的异常 + if (key == null || entry.getValue() == null) { + continue; + } + // 统一转为字符串(兼容非字符串类型的value) + String value = String.valueOf(entry.getValue()).trim(); + + // 优先更新固定属性,再更新动态属性 + switch (key) { + case "username": + user.setUsername(value); + break; + case "role": + user.setRole(value); + break; + case "loginIp": + user.setLoginIp(value); + break; + default: + // 非固定属性,存入动态属性 + user.setDynamicAttr(key, value); + break; } } + + System.out.println("setClientInfo成功:用户属性更新完成,senderId=" + senderId); } } \ No newline at end of file diff --git a/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/WebSocketUser.java b/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/WebSocketUser.java index a8284e4..1b910ea 100644 --- a/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/WebSocketUser.java +++ b/lis-interfaceCommon/src/main/java/com/czlis/interfaceCommon/websocket/WebSocketUser.java @@ -1,7 +1,6 @@ package com.czlis.interfaceCommon.websocket; import javax.websocket.Session; -import java.net.InetSocketAddress; import java.time.LocalDateTime; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -10,12 +9,12 @@ import java.util.concurrent.ConcurrentHashMap; * 封装WebSocket用户连接信息:聚合Session + 身份属性 + 连接元数据 */ public class WebSocketUser { - // 核心字段:用户唯一标识 + // 核心字段:用户唯一标识(仍保持final,不建议修改) private final String userId; - // 身份属性(可根据业务扩展) - private final String username; // 用户名 - private final String role; // 角色(如ADMIN/USER) - private final String loginIp; // 登录IP + // 身份属性(移除final修饰符,支持修改) + private String username; // 用户名 + private String role; // 角色(如ADMIN/USER) + private String loginIp; // 登录IP // 连接元数据 private final Session session; // WebSocket连接会话 private final LocalDateTime connectTime; // 连接建立时间 @@ -38,18 +37,34 @@ public class WebSocketUser { return userId; } + // ========== 可修改属性(getter + setter) ========== public String getUsername() { return username; } + // 添加username的setter方法 + public void setUsername(String username) { + this.username = username; + } + public String getRole() { return role; } + // 添加role的setter方法 + public void setRole(String role) { + this.role = role; + } + public String getLoginIp() { return loginIp; } + // 添加loginIp的setter方法 + public void setLoginIp(String loginIp) { + this.loginIp = loginIp; + } + public Session getSession() { return session; } diff --git a/liswork/src/main/java/com/czlis/liswork/controller/dict/DictController.java b/liswork/src/main/java/com/czlis/liswork/controller/dict/DictController.java index c21ea31..ecf2035 100644 --- a/liswork/src/main/java/com/czlis/liswork/controller/dict/DictController.java +++ b/liswork/src/main/java/com/czlis/liswork/controller/dict/DictController.java @@ -33,8 +33,8 @@ public class DictController extends BaseController { @Autowired private ComDictService comDictService; - @Autowired - WebSocketInstrComm webSocketInstrComm; +// @Autowired +// WebSocketInstrComm webSocketInstrComm; /** * 获取字典列表 @@ -107,11 +107,11 @@ public class DictController extends BaseController { return comDictService.insertComDictList(comDictList, updateSupport); } - @ApiOperation("测试服务") - @GetMapping("/test1") - public Result test1(){ - String jsonStr = "{\"data\":\"1234\"}"; - webSocketInstrComm.sendToAllClient(jsonStr); - return new Result("0","成功!"); - } +// @ApiOperation("测试服务") +// @GetMapping("/test1") +// public Result test1(){ +// String jsonStr = "{\"data\":\"1234\"}"; +// webSocketInstrComm.sendToAllClient(jsonStr); +// return new Result("0","成功!"); +// } } diff --git a/zycx/src/main/java/com/czlis/zycx/service/impl/ZycxServiceImpl.java b/zycx/src/main/java/com/czlis/zycx/service/impl/ZycxServiceImpl.java index 4d9ec2f..87ce2c8 100644 --- a/zycx/src/main/java/com/czlis/zycx/service/impl/ZycxServiceImpl.java +++ b/zycx/src/main/java/com/czlis/zycx/service/impl/ZycxServiceImpl.java @@ -8,6 +8,8 @@ import com.czlis.common.core.domain.Result; import com.czlis.common.core.domain.entity.lis.LabIntersendList; import com.czlis.common.core.domain.entity.lis.LabReqdetail; import com.czlis.common.core.domain.entity.lis.LabReqmain; +import com.czlis.common.utils.SecurityUtils; +import com.czlis.common.utils.ip.IpUtils; import com.czlis.interfaceCommon.constants.LisinterfaceNameConstants; import com.czlis.interfaceCommon.pojo.entity.ReportParam; import com.czlis.interfaceCommon.pojo.inter.GetReqInterface; @@ -16,7 +18,10 @@ import com.czlis.interfaceCommon.utils.CommonUtil; import com.czlis.interfaceCommon.utils.LisInterfaceUtil; import com.czlis.interfaceCommon.utils.LisUtil; import com.czlis.interfaceCommon.utils.ReportUtil; +import com.czlis.interfaceCommon.websocket.ChatWebSocketServer; import com.czlis.interfaceCommon.websocket.WebSocketInstrComm; +import com.czlis.interfaceCommon.websocket.WebSocketMessage; +import com.czlis.interfaceCommon.websocket.WebSocketUser; import com.czlis.zycx.mapper.LabIntersendListMapper; import com.czlis.zycx.mapper.ZycxMapper; import com.czlis.zycx.pojo.QuerySqdVO; @@ -312,9 +317,26 @@ public class ZycxServiceImpl implements ZycxService { labIntersendList.setUserid(userId); labIntersendList.setCreatedate(commonUtil.getCurrentTime()); labIntersendListMapper.insert(labIntersendList); - return new Result("0","打印成功!",guid); + WebSocketMessage wsMessage=new WebSocketMessage(); + wsMessage.setMsgType("lisbarprint"); + wsMessage.setData("lisbarprint://"+guid); + wsMessage.setSenderId(SecurityUtils.getUsername()); + return sendprintmsg(JSONUtil.toJsonStr(wsMessage)); + // return new Result("0","打印成功!",guid); + } + + public Result sendprintmsg(String msg){ + String ip= IpUtils.getIpAddr(); + List printUsers = ChatWebSocketServer.getOnlineUsersByLoginIp(ip); + if (printUsers.isEmpty()) { + System.out.println("当前客户端打印插件不在线"); + return new Result("-1","当前客户端打印插件不在线"); + } + WebSocketUser webSocketUser=printUsers.get(0); + ChatWebSocketServer.sendToUser(webSocketUser.getUserId(), msg); + System.out.println("已给客户端[" + webSocketUser.getUsername() + "]推送打印通知,IP:" + webSocketUser.getLoginIp()); + return new Result("0","打印成功"); } - }