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.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<String> 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<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列表(兼容原有逻辑)
*/
@ -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;
}
}
}

View File

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

View File

@ -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<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);
if (user != null) {
for (Map.Entry<String, Object> 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<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;
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;
}

View File

@ -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","成功!");
// }
}

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.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<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","打印成功");
}
}