This commit is contained in:
tangw 2026-01-25 14:07:39 +08:00
parent bd9d44c5fb
commit 9cbe312933
7 changed files with 243 additions and 39 deletions

View File

@ -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> T getEndpointInstance(Class<T> clazz) throws InstantiationException {
// 双重校验:上下文存在 + 容器中有该Bean
if (applicationContext != null) {
try {
return applicationContext.getBean(clazz);
} catch (BeansException e) {
// 容器中无该Bean时,降级使用默认实例化逻辑(兼容测试场景)
return super.getEndpointInstance(clazz);
}
}
// 上下文未初始化时,走父类默认逻辑
return super.getEndpointInstance(clazz);
}
}

View File

@ -4,7 +4,9 @@ import com.alibaba.fastjson2.JSON;
import com.czlis.common.core.domain.model.LoginUser; import com.czlis.common.core.domain.model.LoginUser;
import com.czlis.common.utils.SecurityUtils; import com.czlis.common.utils.SecurityUtils;
import com.czlis.common.utils.spring.SpringUtils; import com.czlis.common.utils.spring.SpringUtils;
import com.czlis.interfaceCommon.config.SpringContextWebSocketConfigurator;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.SpringConfigurator; import org.springframework.web.socket.server.standard.SpringConfigurator;
import javax.websocket.*; import javax.websocket.*;
@ -24,7 +26,8 @@ import java.util.concurrent.ConcurrentHashMap;
* 工具方法暴露:在 WebSocket 端点类中提供静态工具方法(如 sendToUser、broadcast),其他业务直接调用该方法,无需关心实例创建; * 工具方法暴露:在 WebSocket 端点类中提供静态工具方法(如 sendToUser、broadcast),其他业务直接调用该方法,无需关心实例创建;
* 避免重复连接:连接的创建 / 销毁由 WebSocket 协议自动管理(@OnOpen 新增、@OnClose 移除),业务层不干预实例创建。 * 避免重复连接:连接的创建 / 销毁由 WebSocket 协议自动管理(@OnOpen 新增、@OnClose 移除),业务层不干预实例创建。
*/ */
@ServerEndpoint(value = "/ws/msgserver/{userId}", configurator = SpringConfigurator.class) @Component
@ServerEndpoint(value = "/ws/msgserver/{userId}", configurator = SpringContextWebSocketConfigurator.class)
public class ChatWebSocketServer { public class ChatWebSocketServer {
// 🌟 核心固定参数白名单:这些参数不存入动态属性(单独解析为固定身份属性) // 🌟 核心固定参数白名单:这些参数不存入动态属性(单独解析为固定身份属性)
private static final Set<String> CORE_PARAMS = new HashSet<>(Arrays.asList("token", "username", "role")); private static final Set<String> CORE_PARAMS = new HashSet<>(Arrays.asList("token", "username", "role"));
@ -94,22 +97,47 @@ public class ChatWebSocketServer {
try { try {
// 1. 基础校验:消息为空/连接关闭 → 直接返回 // 1. 基础校验:消息为空/连接关闭 → 直接返回
if (message == null || message.trim().isEmpty() || !session.isOpen()) { if (message == null || message.trim().isEmpty() || !session.isOpen()) {
System.out.println("消息为空或连接已关闭,忽略处理:userId=" + userId);
return; return;
} }
// 2. 解析消息为统一协议对象(JSON解析) // 2. 前置校验:判断是否为合法JSON(避免非JSON字符串直接解析)
WebSocketMessage wsMessage = JSON.parseObject(message, WebSocketMessage.class); String trimMsg = message.trim();
if (wsMessage == null) { if (!isValidJson(trimMsg)) {
sendToUser(userId, "错误:消息格式不合法,请按JSON格式发送"); String errorMsg = "错误:消息格式必须是JSON字符串,当前消息:" + trimMsg;
sendToUser(userId, errorMsg);
System.out.println("非JSON消息被拒绝:userId=" + userId + ",消息=" + trimMsg);
return; 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) { if (wsMessage.getTimestamp() == null || wsMessage.getTimestamp() == 0) {
wsMessage.setTimestamp(System.currentTimeMillis()); wsMessage.setTimestamp(System.currentTimeMillis());
} }
// 3. 补充发送者ID(从路径参数获取,避免客户端伪造)
wsMessage.setSenderId(userId); wsMessage.setSenderId(userId);
// 6. 生成唯一消息ID(避免重试队列重复)
if (wsMessage.getMsgId() == null || wsMessage.getMsgId().isEmpty()) {
wsMessage.setMsgId(userId + "_" + System.currentTimeMillis());
}
// 4. 获取Spring管理的业务层Bean,转发消息处理 // 4. 获取Spring管理的业务层Bean,转发消息处理
WebSocketMsgService msgService = SpringUtils.getBean(WebSocketMsgService.class); WebSocketMsgService msgService = SpringUtils.getBean(WebSocketMsgService.class);
if (msgService == null) { if (msgService == null) {
@ -190,7 +218,26 @@ public class ChatWebSocketServer {
} }
return roleUsers; return roleUsers;
} }
/**
* 根据登录IP筛选在线用户(精确匹配)
* @param loginIp 要查询的登录IP(如 "192.168.1.100")
* @return 匹配该IP的在线且连接有效的用户列表
*/
public static List<WebSocketUser> getOnlineUsersByLoginIp(String loginIp) {
List<WebSocketUser> 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列表(兼容原有逻辑) * 获取在线用户的userId列表(兼容原有逻辑)
*/ */
@ -291,5 +338,19 @@ public class ChatWebSocketServer {
user.removeDynamicAttr(attrKey); user.removeDynamicAttr(attrKey);
return true; 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;
}
}
} }

View File

@ -11,6 +11,7 @@ import lombok.NoArgsConstructor;
@AllArgsConstructor @AllArgsConstructor
@NoArgsConstructor @NoArgsConstructor
public class WebSocketMessage { public class WebSocketMessage {
private String msgId;
// 消息类型(如CHAT/NOTICE/ORDER/HEARTBEAT) // 消息类型(如CHAT/NOTICE/ORDER/HEARTBEAT)
private String msgType; private String msgType;
// 业务数据(JSON字符串,可解析为对应业务DTO) // 业务数据(JSON字符串,可解析为对应业务DTO)

View File

@ -1,17 +1,11 @@
package com.czlis.interfaceCommon.websocket; package com.czlis.interfaceCommon.websocket;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.czlis.common.annotation.Async;
import com.czlis.common.core.redis.RedisCache; import com.czlis.common.core.redis.RedisCache;
import com.czlis.common.utils.spring.SpringUtils; 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 org.springframework.stereotype.Service;
import javax.websocket.Session; import javax.websocket.Session;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/**业务层(Spring Bean)专注于「消息解析、权限校验、业务逻辑处理、结果推送」,支持不同类型消息的差异化处理。 /**业务层(Spring Bean)专注于「消息解析、权限校验、业务逻辑处理、结果推送」,支持不同类型消息的差异化处理。
@ -32,6 +26,7 @@ public class WebSocketMsgServiceImpl implements WebSocketMsgService {
ChatWebSocketServer.sendToUser(wsMessage.getSenderId(), "提示:消息已处理,无需重复发送"); ChatWebSocketServer.sendToUser(wsMessage.getSenderId(), "提示:消息已处理,无需重复发送");
return; return;
} }
System.err.println("wsMessage=" + wsMessage);
try { try {
// 1. 基础校验:消息类型、发送者ID // 1. 基础校验:消息类型、发送者ID
String msgType = wsMessage.getMsgType(); String msgType = wsMessage.getMsgType();
@ -170,22 +165,82 @@ public class WebSocketMsgServiceImpl implements WebSocketMsgService {
private void handleHeartbeatMsg(WebSocketMessage wsMessage) { private void handleHeartbeatMsg(WebSocketMessage wsMessage) {
String senderId = wsMessage.getSenderId(); 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) { public void setClientInfo(WebSocketMessage wsMessage) {
// 1. 前置空值校验:避免空指针异常
if (wsMessage == null) {
System.err.println("setClientInfo失败:wsMessage为null");
return;
}
String senderId = wsMessage.getSenderId(); String senderId = wsMessage.getSenderId();
String data = wsMessage.getData(); String data = wsMessage.getData();
Map<String, Object> 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<String, Object> 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); WebSocketUser user = ChatWebSocketServer.getOnlineUser(senderId);
if (user != null) { if (user == null) {
for (Map.Entry<String, Object> entry : dataStrMap.entrySet()) { System.err.println("setClientInfo失败:未找到在线用户,senderId=" + senderId);
String key = entry.getKey(); return;
String value =(String)entry.getValue(); }
user.setDynamicAttr(key, value); // // 额外校验连接是否有效
// if (!user.isConnected()) {
// System.err.println("setClientInfo失败:用户连接已关闭,senderId=" + senderId);
// // 可选:移除离线用户
// ChatWebSocketServer.removeOnlineUser(senderId);
// return;
// }
// 4. 遍历更新属性:优化判断逻辑,避免重复赋值
for (Map.Entry<String, Object> 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);
} }
} }

View File

@ -1,7 +1,6 @@
package com.czlis.interfaceCommon.websocket; package com.czlis.interfaceCommon.websocket;
import javax.websocket.Session; import javax.websocket.Session;
import java.net.InetSocketAddress;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@ -10,12 +9,12 @@ import java.util.concurrent.ConcurrentHashMap;
* 封装WebSocket用户连接信息:聚合Session + 身份属性 + 连接元数据 * 封装WebSocket用户连接信息:聚合Session + 身份属性 + 连接元数据
*/ */
public class WebSocketUser { public class WebSocketUser {
// 核心字段:用户唯一标识 // 核心字段:用户唯一标识(仍保持final,不建议修改)
private final String userId; private final String userId;
// 身份属性(可根据业务扩展) // 身份属性(移除final修饰符,支持修改)
private final String username; // 用户名 private String username; // 用户名
private final String role; // 角色(如ADMIN/USER) private String role; // 角色(如ADMIN/USER)
private final String loginIp; // 登录IP private String loginIp; // 登录IP
// 连接元数据 // 连接元数据
private final Session session; // WebSocket连接会话 private final Session session; // WebSocket连接会话
private final LocalDateTime connectTime; // 连接建立时间 private final LocalDateTime connectTime; // 连接建立时间
@ -38,18 +37,34 @@ public class WebSocketUser {
return userId; return userId;
} }
// ========== 可修改属性(getter + setter) ==========
public String getUsername() { public String getUsername() {
return username; return username;
} }
// 添加username的setter方法
public void setUsername(String username) {
this.username = username;
}
public String getRole() { public String getRole() {
return role; return role;
} }
// 添加role的setter方法
public void setRole(String role) {
this.role = role;
}
public String getLoginIp() { public String getLoginIp() {
return loginIp; return loginIp;
} }
// 添加loginIp的setter方法
public void setLoginIp(String loginIp) {
this.loginIp = loginIp;
}
public Session getSession() { public Session getSession() {
return session; return session;
} }

View File

@ -33,8 +33,8 @@ public class DictController extends BaseController {
@Autowired @Autowired
private ComDictService comDictService; private ComDictService comDictService;
@Autowired // @Autowired
WebSocketInstrComm webSocketInstrComm; // WebSocketInstrComm webSocketInstrComm;
/** /**
* 获取字典列表 * 获取字典列表
@ -107,11 +107,11 @@ public class DictController extends BaseController {
return comDictService.insertComDictList(comDictList, updateSupport); return comDictService.insertComDictList(comDictList, updateSupport);
} }
@ApiOperation("测试服务") // @ApiOperation("测试服务")
@GetMapping("/test1") // @GetMapping("/test1")
public Result test1(){ // public Result test1(){
String jsonStr = "{\"data\":\"1234\"}"; // String jsonStr = "{\"data\":\"1234\"}";
webSocketInstrComm.sendToAllClient(jsonStr); // webSocketInstrComm.sendToAllClient(jsonStr);
return new Result("0","成功!"); // return new Result("0","成功!");
} // }
} }

View File

@ -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.LabIntersendList;
import com.czlis.common.core.domain.entity.lis.LabReqdetail; import com.czlis.common.core.domain.entity.lis.LabReqdetail;
import com.czlis.common.core.domain.entity.lis.LabReqmain; 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.constants.LisinterfaceNameConstants;
import com.czlis.interfaceCommon.pojo.entity.ReportParam; import com.czlis.interfaceCommon.pojo.entity.ReportParam;
import com.czlis.interfaceCommon.pojo.inter.GetReqInterface; 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.LisInterfaceUtil;
import com.czlis.interfaceCommon.utils.LisUtil; import com.czlis.interfaceCommon.utils.LisUtil;
import com.czlis.interfaceCommon.utils.ReportUtil; import com.czlis.interfaceCommon.utils.ReportUtil;
import com.czlis.interfaceCommon.websocket.ChatWebSocketServer;
import com.czlis.interfaceCommon.websocket.WebSocketInstrComm; 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.LabIntersendListMapper;
import com.czlis.zycx.mapper.ZycxMapper; import com.czlis.zycx.mapper.ZycxMapper;
import com.czlis.zycx.pojo.QuerySqdVO; import com.czlis.zycx.pojo.QuerySqdVO;
@ -312,9 +317,26 @@ public class ZycxServiceImpl implements ZycxService {
labIntersendList.setUserid(userId); labIntersendList.setUserid(userId);
labIntersendList.setCreatedate(commonUtil.getCurrentTime()); labIntersendList.setCreatedate(commonUtil.getCurrentTime());
labIntersendListMapper.insert(labIntersendList); 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<WebSocketUser> 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","打印成功");
} }
} }