WebSocket
parent
a2cacdb802
commit
57d1caecbf
|
|
@ -61,7 +61,7 @@ public class Jackson {
|
|||
try {
|
||||
return objectMapper.writeValueAsString(o);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Jackson 序列化失败", e);
|
||||
// log.error("Jackson 序列化失败", e);
|
||||
throw Exceptions.error(e, "Jackson 序列化失败");
|
||||
}
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ public class Jackson {
|
|||
try {
|
||||
return objectMapper.writeValueAsBytes(o);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Jackson 序列化失败", e);
|
||||
// log.error("Jackson 序列化失败", e);
|
||||
throw Exceptions.error(e, "Jackson 序列化失败");
|
||||
}
|
||||
}
|
||||
|
|
@ -94,7 +94,7 @@ public class Jackson {
|
|||
try {
|
||||
return objectMapper.readValue(json, objectMapper.getTypeFactory().constructType(type));
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Jackson 反序列化失败", e);
|
||||
// log.error("Jackson 反序列化失败", e);
|
||||
throw Exceptions.error(e, "Jackson 反序列化失败");
|
||||
}
|
||||
}
|
||||
|
|
@ -103,7 +103,7 @@ public class Jackson {
|
|||
try {
|
||||
return objectMapper.readValue(json, objectMapper.getTypeFactory().constructType(type));
|
||||
} catch (Exception e) {
|
||||
log.error("Jackson 反序列化失败", e);
|
||||
// log.error("Jackson 反序列化失败", e);
|
||||
throw Exceptions.error(e, "Jackson 反序列化失败");
|
||||
}
|
||||
}
|
||||
|
|
@ -112,7 +112,7 @@ public class Jackson {
|
|||
try {
|
||||
return objectMapper.readValue(json, objectMapper.getTypeFactory().constructType(type));
|
||||
} catch (Exception e) {
|
||||
log.error("Jackson 反序列化失败", e);
|
||||
// log.error("Jackson 反序列化失败", e);
|
||||
throw Exceptions.error(e, "Jackson 反序列化失败");
|
||||
}
|
||||
}
|
||||
|
|
@ -128,7 +128,7 @@ public class Jackson {
|
|||
try {
|
||||
return xmlMapper.writeValueAsString(o);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Jackson 序列化失败", e);
|
||||
// log.error("Jackson 序列化失败", e);
|
||||
throw Exceptions.error(e, "Jackson 序列化失败");
|
||||
}
|
||||
}
|
||||
|
|
@ -143,7 +143,7 @@ public class Jackson {
|
|||
try {
|
||||
return xmlMapper.writeValueAsBytes(o);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Jackson 序列化失败", e);
|
||||
// log.error("Jackson 序列化失败", e);
|
||||
throw Exceptions.error(e, "Jackson 序列化失败");
|
||||
}
|
||||
}
|
||||
|
|
@ -152,7 +152,7 @@ public class Jackson {
|
|||
try {
|
||||
return xmlMapper.readValue(json, xmlMapper.getTypeFactory().constructType(type));
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Jackson 反序列化失败", e);
|
||||
// log.error("Jackson 反序列化失败", e);
|
||||
throw Exceptions.error(e, "Jackson 反序列化失败");
|
||||
}
|
||||
}
|
||||
|
|
@ -161,7 +161,7 @@ public class Jackson {
|
|||
try {
|
||||
return xmlMapper.readValue(json, xmlMapper.getTypeFactory().constructType(type));
|
||||
} catch (Exception e) {
|
||||
log.error("Jackson 反序列化失败", e);
|
||||
// log.error("Jackson 反序列化失败", e);
|
||||
throw Exceptions.error(e, "Jackson 反序列化失败");
|
||||
}
|
||||
}
|
||||
|
|
@ -170,7 +170,7 @@ public class Jackson {
|
|||
try {
|
||||
return xmlMapper.readValue(json, xmlMapper.getTypeFactory().constructType(type));
|
||||
} catch (Exception e) {
|
||||
log.error("Jackson 反序列化失败", e);
|
||||
// log.error("Jackson 反序列化失败", e);
|
||||
throw Exceptions.error(e, "Jackson 反序列化失败");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.njzscloud</groupId>
|
||||
<artifactId>njzscloud-common</artifactId>
|
||||
<version>0.0.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>njzscloud-common-ws</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.njzscloud</groupId>
|
||||
<artifactId>njzscloud-common-core</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.njzscloud</groupId>
|
||||
<artifactId>njzscloud-common-security</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-websocket</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package com.njzscloud.common.ws.config;
|
||||
|
||||
import com.njzscloud.common.ws.support.ImpartialWebSocketHandler;
|
||||
import com.njzscloud.common.ws.support.TokenHandshakeInterceptor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
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;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocket
|
||||
@RequiredArgsConstructor
|
||||
@EnableConfigurationProperties(WebsocketProperties.class)
|
||||
public class WebSocketAutoConfiguration implements WebSocketConfigurer {
|
||||
|
||||
private final WebsocketProperties websocketProperties;
|
||||
|
||||
// websocket-heartbeat-js
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
String path = websocketProperties.getPath();
|
||||
registry.addHandler(new ImpartialWebSocketHandler(), path == null || path.isEmpty() ? "/fdx" : path)
|
||||
.addInterceptors(new TokenHandshakeInterceptor())
|
||||
.setAllowedOrigins("*");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ServletServerContainerFactoryBean createWebSocketContainer() {
|
||||
Duration asyncSendTimeout = websocketProperties.getAsyncSendTimeout();
|
||||
Duration maxSessionIdleTimeout = websocketProperties.getMaxSessionIdleTimeout();
|
||||
DataSize maxTextMessageBufferSize = websocketProperties.getMaxTextMessageBufferSize();
|
||||
DataSize maxBinaryMessageBufferSize = websocketProperties.getMaxBinaryMessageBufferSize();
|
||||
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
|
||||
|
||||
if (asyncSendTimeout != null) {
|
||||
long seconds = asyncSendTimeout.getSeconds();
|
||||
container.setAsyncSendTimeout(seconds * 1000);
|
||||
}
|
||||
|
||||
if (maxSessionIdleTimeout != null) {
|
||||
long seconds = maxSessionIdleTimeout.getSeconds();
|
||||
container.setMaxSessionIdleTimeout(seconds * 1000);
|
||||
}
|
||||
|
||||
if (maxTextMessageBufferSize != null) {
|
||||
long size = maxTextMessageBufferSize.toBytes();
|
||||
container.setMaxTextMessageBufferSize((int) size);
|
||||
}
|
||||
|
||||
if (maxBinaryMessageBufferSize != null) {
|
||||
long size = maxBinaryMessageBufferSize.toBytes();
|
||||
container.setMaxBinaryMessageBufferSize((int) size);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.njzscloud.common.ws.config;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* websocket 配置
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@ConfigurationProperties("ws")
|
||||
public class WebsocketProperties {
|
||||
/**
|
||||
* websocket 请求地址, 默认 /fdx
|
||||
*/
|
||||
private String path = "/fdx";
|
||||
|
||||
/**
|
||||
* 发送超时时间
|
||||
*/
|
||||
private Duration asyncSendTimeout;
|
||||
|
||||
/**
|
||||
* 会话超时时间, 默认 15min
|
||||
*/
|
||||
private Duration maxSessionIdleTimeout = Duration.ofMinutes(15);
|
||||
|
||||
/**
|
||||
* 文本消息缓存大小, 默认 2MB
|
||||
*/
|
||||
private DataSize maxTextMessageBufferSize = DataSize.ofMegabytes(2);
|
||||
|
||||
/**
|
||||
* 二进制消息缓存大小, 默认 2MB
|
||||
*/
|
||||
private DataSize maxBinaryMessageBufferSize = DataSize.ofMegabytes(2);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.njzscloud.common.ws.support;
|
||||
|
||||
/**
|
||||
* 常量
|
||||
*/
|
||||
public class Constants {
|
||||
/**
|
||||
* TOKEN 属性
|
||||
*/
|
||||
public static final String SESSION_ATTRIBUTE_TOKEN = "token";
|
||||
|
||||
/**
|
||||
* 客户端 Id 属性
|
||||
*/
|
||||
public static final String SESSION_ATTRIBUTE_CID = "cid";
|
||||
|
||||
/**
|
||||
* 用户 Id 属性
|
||||
*/
|
||||
public static final String SESSION_ATTRIBUTE_UID = "uid";
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package com.njzscloud.common.ws.support;
|
||||
|
||||
import com.njzscloud.common.core.ex.ExceptionMsg;
|
||||
import com.njzscloud.common.core.utils.R;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.socket.*;
|
||||
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
// TODO websocket 消息处理器, 消息转发
|
||||
|
||||
/**
|
||||
* websocket 处理器<br/>
|
||||
* 不支持分片消息<br/>
|
||||
* 支持文本、二进制信息
|
||||
*/
|
||||
@Slf4j
|
||||
public class ImpartialWebSocketHandler extends AbstractWebSocketHandler {
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||
Map<String, Object> attributes = session.getAttributes();
|
||||
String token = (String) attributes.get(Constants.SESSION_ATTRIBUTE_TOKEN);
|
||||
String cid = (String) attributes.get(Constants.SESSION_ATTRIBUTE_CID);
|
||||
String uid = (String) attributes.get(Constants.SESSION_ATTRIBUTE_UID);
|
||||
|
||||
Websocket.optLink(false, it -> {
|
||||
Wsdoor wsdoor = new Wsdoor(uid, cid, token, session);
|
||||
Map<String, Wsdoor> map = it.get(uid);
|
||||
if (map == null) {
|
||||
map = new HashMap<>();
|
||||
map.put(cid, wsdoor);
|
||||
it.put(uid, map);
|
||||
return;
|
||||
}
|
||||
map.put(cid, wsdoor);
|
||||
});
|
||||
|
||||
Websocket.send(uid, cid, new WsMsg().setEvent("connected")
|
||||
.setData(R.success(cid, "连接成功"))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
Map<String, Object> attributes = session.getAttributes();
|
||||
String cid = (String) attributes.get(Constants.SESSION_ATTRIBUTE_CID);
|
||||
String uid = (String) attributes.get(Constants.SESSION_ATTRIBUTE_UID);
|
||||
String payload = message.getPayload();
|
||||
WsMsg wsMsg = WsMsg.of(payload);
|
||||
if (wsMsg == null) {
|
||||
Websocket.send(uid, cid, new WsMsg().setEvent("error")
|
||||
.setData(R.failed(ExceptionMsg.CLI_ERR_MSG, "消息格式错误"))
|
||||
);
|
||||
return;
|
||||
}
|
||||
Websocket.dispatch(wsMsg
|
||||
.setUid(uid)
|
||||
.setCid(cid)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
|
||||
Map<String, Object> attributes = session.getAttributes();
|
||||
String token = (String) attributes.get(Constants.SESSION_ATTRIBUTE_TOKEN);
|
||||
String cid = (String) attributes.get(Constants.SESSION_ATTRIBUTE_CID);
|
||||
String uid = (String) attributes.get(Constants.SESSION_ATTRIBUTE_UID);
|
||||
BinaryMessage binaryMessage = new BinaryMessage(message.getPayload().array());
|
||||
session.sendMessage(binaryMessage);
|
||||
|
||||
log.info("二进制消息: {}", message.getPayloadLength());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
|
||||
log.info("Pong...");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
|
||||
Map<String, Object> attributes = session.getAttributes();
|
||||
String token = (String) attributes.get(Constants.SESSION_ATTRIBUTE_TOKEN);
|
||||
String cid = (String) attributes.get(Constants.SESSION_ATTRIBUTE_CID);
|
||||
String uid = (String) attributes.get(Constants.SESSION_ATTRIBUTE_UID);
|
||||
log.error("连接失败: {}", cid, exception);
|
||||
Websocket.optLink(false, uid, it -> it.remove(cid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
|
||||
Map<String, Object> attributes = session.getAttributes();
|
||||
String token = (String) attributes.get(Constants.SESSION_ATTRIBUTE_TOKEN);
|
||||
String cid = (String) attributes.get(Constants.SESSION_ATTRIBUTE_CID);
|
||||
String uid = (String) attributes.get(Constants.SESSION_ATTRIBUTE_UID);
|
||||
log.info("连接关闭: {} {}", cid, closeStatus);
|
||||
|
||||
Websocket.optLink(false, uid, it -> it.remove(cid));
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package com.njzscloud.common.ws.support;
|
||||
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.njzscloud.common.security.support.UserDetail;
|
||||
import com.njzscloud.common.security.util.SecurityUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Map;
|
||||
|
||||
// TODO TOKEN 处理器
|
||||
|
||||
/**
|
||||
* websocket Token 拦截器<br/>
|
||||
* 子协议格式: Auth [token],[cid], 如: Auth aimsn,niosdawq<br/>
|
||||
* token: 用户登录凭证; cid: 客户端号, 推荐使用 token Id
|
||||
*/
|
||||
@Slf4j
|
||||
public class TokenHandshakeInterceptor implements HandshakeInterceptor {
|
||||
private static final String SEC_WEB_SOCKET_PROTOCOL = "Sec-WebSocket-Protocol";
|
||||
// private static final Pattern AUTH_PATTERN = Pattern.compile("^Auth (?<token>[a-zA-Z0-9-.:_~+/]+=*),(?<cid>[a-zA-Z0-9-.:_~+/]+=*)$", Pattern.CASE_INSENSITIVE);
|
||||
private static final String Authorization = "authorization";
|
||||
private static final String Cid = "cid";
|
||||
|
||||
@Override
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler wsHandler, Map<String, Object> attributes) {
|
||||
try {
|
||||
HttpServletRequest req = ((ServletServerHttpRequest) request).getServletRequest();
|
||||
String authorization = req.getParameter(Authorization);
|
||||
// String cid = req.getParameter(Cid);
|
||||
if (StrUtil.isBlank(authorization)/* || StrUtil.isBlank(cid) */) return false;
|
||||
|
||||
String uid = this.resolve(authorization);
|
||||
if (StrUtil.isBlank(uid)) return false;
|
||||
String cid = IdUtil.fastSimpleUUID();
|
||||
attributes.put(Constants.SESSION_ATTRIBUTE_TOKEN, authorization);
|
||||
attributes.put(Constants.SESSION_ATTRIBUTE_CID, cid);
|
||||
attributes.put(Constants.SESSION_ATTRIBUTE_UID, uid);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[Websocket 拦截器] 数据解析失败", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
|
||||
try {
|
||||
HttpServletRequest httpRequest = ((ServletServerHttpRequest) request).getServletRequest();
|
||||
HttpServletResponse httpResponse = ((ServletServerHttpResponse) response).getServletResponse();
|
||||
String token = httpRequest.getHeader(SEC_WEB_SOCKET_PROTOCOL);
|
||||
if (StrUtil.isNotEmpty(token)) {
|
||||
httpResponse.addHeader(SEC_WEB_SOCKET_PROTOCOL, token);
|
||||
}
|
||||
} catch (ClassCastException e) {
|
||||
log.error("[Websocket 拦截器] 类型转换失败: {}、{}", request.getClass(), response.getClass(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String resolve(String token) {
|
||||
UserDetail userDetail = SecurityUtil.parseToken(token);
|
||||
if (userDetail == null) return null;
|
||||
return userDetail.getUserId().toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.njzscloud.common.ws.support;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.METHOD})
|
||||
public @interface WSListener {
|
||||
String value();
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
package com.njzscloud.common.ws.support;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* websocket 工具
|
||||
*/
|
||||
@Slf4j
|
||||
public class Websocket {
|
||||
/**
|
||||
* 连接缓存<br/>
|
||||
* 用户Id:客户端Id:Wsdoor<br/>
|
||||
*/
|
||||
static final Map<String, Map<String, Wsdoor>> LINK_CACHE = new HashMap<>();
|
||||
static final Map<String, Consumer<WsMsg>> LISTENERS = new ConcurrentHashMap<>();
|
||||
|
||||
static final ReentrantReadWriteLock LINK_CACHE_LOCK = new ReentrantReadWriteLock();
|
||||
|
||||
static final ReentrantReadWriteLock.ReadLock LINK_CACHE_READ_LOCK = LINK_CACHE_LOCK.readLock();
|
||||
|
||||
static final ReentrantReadWriteLock.WriteLock LINK_CACHE_WRITE_LOCK = LINK_CACHE_LOCK.writeLock();
|
||||
|
||||
public static boolean online(String uid, String cid) {
|
||||
Websocket.LINK_CACHE_READ_LOCK.lock();
|
||||
try {
|
||||
return LINK_CACHE.containsKey(uid) && LINK_CACHE.get(uid).containsKey(cid);
|
||||
} finally {
|
||||
Websocket.LINK_CACHE_READ_LOCK.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作 websocket 连接<br/>
|
||||
* readonly 为 true 时, 使用读锁, 否则使用写锁<br/>
|
||||
*
|
||||
* @param readonly 是否只读
|
||||
* @param consumer 操作方法
|
||||
*/
|
||||
static void optLink(boolean readonly, Consumer<Map<String, Map<String, Wsdoor>>> consumer) {
|
||||
if (readonly) {
|
||||
try {
|
||||
Websocket.LINK_CACHE_READ_LOCK.lock();
|
||||
consumer.accept(LINK_CACHE);
|
||||
} finally {
|
||||
Websocket.LINK_CACHE_READ_LOCK.unlock();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Websocket.LINK_CACHE_WRITE_LOCK.lock();
|
||||
consumer.accept(LINK_CACHE);
|
||||
} finally {
|
||||
Websocket.LINK_CACHE_WRITE_LOCK.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作 websocket 连接<br/>
|
||||
* readonly 为 true 时, 使用读锁, 否则使用写锁<br/>
|
||||
*
|
||||
* @param readonly 是否只读
|
||||
* @param uid 用户 Id
|
||||
* @param consumer 操作方法
|
||||
*/
|
||||
static void optLink(boolean readonly, String uid, Consumer<Map<String, Wsdoor>> consumer) {
|
||||
if (readonly) {
|
||||
try {
|
||||
Websocket.LINK_CACHE_READ_LOCK.lock();
|
||||
Map<String, Wsdoor> map = LINK_CACHE.get(uid);
|
||||
consumer.accept(map);
|
||||
} finally {
|
||||
Websocket.LINK_CACHE_READ_LOCK.unlock();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Websocket.LINK_CACHE_WRITE_LOCK.lock();
|
||||
Map<String, Wsdoor> map = LINK_CACHE.get(uid);
|
||||
consumer.accept(map);
|
||||
} finally {
|
||||
Websocket.LINK_CACHE_WRITE_LOCK.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作 websocket 连接<br/>
|
||||
* readonly 为 true 时, 使用读锁, 否则使用写锁<br/>
|
||||
*
|
||||
* @param readonly 是否只读
|
||||
* @param uid 用户 Id
|
||||
* @param cid 客户端 Id
|
||||
* @param consumer 操作方法
|
||||
*/
|
||||
static void optLink(boolean readonly, String uid, String cid, Consumer<Wsdoor> consumer) {
|
||||
if (readonly) {
|
||||
try {
|
||||
Websocket.LINK_CACHE_READ_LOCK.lock();
|
||||
Map<String, Wsdoor> map = LINK_CACHE.get(uid);
|
||||
Wsdoor user = null;
|
||||
if (map != null) user = map.get(cid);
|
||||
consumer.accept(user);
|
||||
} finally {
|
||||
Websocket.LINK_CACHE_READ_LOCK.unlock();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
Websocket.LINK_CACHE_WRITE_LOCK.lock();
|
||||
Map<String, Wsdoor> map = LINK_CACHE.get(uid);
|
||||
Wsdoor user = null;
|
||||
if (map != null) user = map.get(cid);
|
||||
consumer.accept(user);
|
||||
} finally {
|
||||
Websocket.LINK_CACHE_WRITE_LOCK.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void addListener(String event, Consumer<WsMsg> consumer) {
|
||||
LISTENERS.put(event, consumer);
|
||||
}
|
||||
|
||||
public static void removeListener(String event, Consumer<WsMsg> consumer) {
|
||||
LISTENERS.remove(event, consumer);
|
||||
}
|
||||
|
||||
public static void dispatch(WsMsg wsMsg) {
|
||||
if (wsMsg == null) return;
|
||||
Consumer<WsMsg> consumer = LISTENERS.get(wsMsg.event);
|
||||
if (consumer != null) {
|
||||
try {
|
||||
consumer.accept(wsMsg);
|
||||
} catch (Exception e) {
|
||||
log.error("WebSocket 消息处理失败, 事件: {}, 数据: {}", wsMsg.event, wsMsg, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送文本数据
|
||||
*
|
||||
* @param uid 用户 Id
|
||||
* @param cid 客户端 Id
|
||||
* @param wsMsg 消息
|
||||
*/
|
||||
public static void send(String uid, String cid, WsMsg wsMsg) {
|
||||
optLink(true, uid, cid, it -> {
|
||||
if (it != null) it.send(wsMsg);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送文本数据
|
||||
*
|
||||
* @param uid 用户 Id
|
||||
* @param wsMsg 消息
|
||||
*/
|
||||
public static void send(String uid, WsMsg wsMsg) {
|
||||
optLink(true, uid, it -> {
|
||||
if (it == null) return;
|
||||
|
||||
Collection<Wsdoor> wsdoors = it.values();
|
||||
|
||||
if (wsdoors.isEmpty()) return;
|
||||
|
||||
for (Wsdoor wsdoor : wsdoors) wsdoor.send(wsMsg);
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.njzscloud.common.ws.support;
|
||||
|
||||
|
||||
import com.njzscloud.common.core.jackson.Jackson;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.experimental.Accessors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* websocket 消息内容
|
||||
*/
|
||||
@Slf4j
|
||||
@Getter
|
||||
@Setter
|
||||
@Accessors(chain = true)
|
||||
public class WsMsg {
|
||||
|
||||
/**
|
||||
* 事件
|
||||
*/
|
||||
public String event;
|
||||
|
||||
/**
|
||||
* 客户端 Id
|
||||
*/
|
||||
public String cid;
|
||||
|
||||
/**
|
||||
* 用户 Id
|
||||
*/
|
||||
public String uid;
|
||||
|
||||
/**
|
||||
* 数据内容
|
||||
*/
|
||||
public Object data;
|
||||
|
||||
public static WsMsg of(String json) {
|
||||
try {
|
||||
return Jackson.toBean(json, WsMsg.class);
|
||||
} catch (Exception e) {
|
||||
log.error("WebSocket 消息反序列化失败, 数据: {}", json, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Jackson.toJsonStr(this);
|
||||
}
|
||||
|
||||
public <T> T extractData(Class<T> clazz) {
|
||||
return Jackson.toBean(Jackson.toJsonStr(data), clazz);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.njzscloud.common.ws.support;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
/**
|
||||
* websocket 会话描述
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class Wsdoor {
|
||||
|
||||
/**
|
||||
* 用户 Id
|
||||
*/
|
||||
public final String uid;
|
||||
|
||||
/**
|
||||
* 客户端 Id
|
||||
*/
|
||||
public final String cid;
|
||||
|
||||
/**
|
||||
* TOKEN
|
||||
*/
|
||||
public final String token;
|
||||
|
||||
/**
|
||||
* 连接会话
|
||||
*/
|
||||
public final WebSocketSession session;
|
||||
|
||||
/**
|
||||
* 发送文本数据
|
||||
*
|
||||
* @param wsMsg 消息
|
||||
*/
|
||||
public boolean send(WsMsg wsMsg) {
|
||||
try {
|
||||
session.sendMessage(new TextMessage(wsMsg.toString()));
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("消息发送失败", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
|
||||
com.njzscloud.common.ws.config.WebSocketAutoConfiguration
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
<module>njzscloud-common-http</module>
|
||||
<module>njzscloud-common-wechat</module>
|
||||
<module>njzscloud-common-localizer</module>
|
||||
<module>njzscloud-common-ws</module>
|
||||
</modules>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@
|
|||
<groupId>com.njzscloud</groupId>
|
||||
<artifactId>njzscloud-common-sn</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.njzscloud</groupId>
|
||||
<artifactId>njzscloud-common-ws</artifactId>
|
||||
</dependency>
|
||||
<!-- <dependency>
|
||||
<groupId>com.njzscloud</groupId>
|
||||
<artifactId>njzscloud-common-redis</artifactId>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package com.njzscloud.supervisory.biz.pojo.param;
|
||||
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@ToString
|
||||
@Accessors(chain = true)
|
||||
public class LocationTrackHistoryParam {
|
||||
Long orderId;
|
||||
LocalDateTime startTime;
|
||||
LocalDateTime endTime;
|
||||
Integer speed;
|
||||
|
||||
public Integer getSpeed() {
|
||||
return speed == null ? 1 : speed;
|
||||
}
|
||||
|
||||
public Long getOrderId() {
|
||||
Assert.notNull(orderId, "订单ID不能为空");
|
||||
return orderId;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.njzscloud.supervisory.biz.service;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.thread.ThreadUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
|
@ -9,11 +8,16 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.njzscloud.common.core.tuple.Tuple3;
|
||||
import com.njzscloud.common.core.utils.R;
|
||||
import com.njzscloud.common.mp.support.PageParam;
|
||||
import com.njzscloud.common.mp.support.PageResult;
|
||||
import com.njzscloud.common.mqtt.support.MqttMsg;
|
||||
import com.njzscloud.common.ws.support.Websocket;
|
||||
import com.njzscloud.common.ws.support.WsMsg;
|
||||
import com.njzscloud.supervisory.biz.mapper.TruckLocationTrackMapper;
|
||||
import com.njzscloud.supervisory.biz.pojo.entity.TruckLocationTrackEntity;
|
||||
import com.njzscloud.supervisory.biz.pojo.param.LocationTrackHistoryParam;
|
||||
import com.njzscloud.supervisory.order.pojo.result.RealtimeLocationResult;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
|
@ -31,6 +35,7 @@ import java.util.concurrent.CompletableFuture;
|
|||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 车辆定位数据
|
||||
|
|
@ -41,6 +46,8 @@ public class TruckLocationTrackService extends ServiceImpl<TruckLocationTrackMap
|
|||
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1));
|
||||
HashMap<String, List<SseEmitter>> emitters = new HashMap<>();
|
||||
ReentrantLock lock = new ReentrantLock();
|
||||
List<Tuple3<String, String, String>> realtimeDataListener = new LinkedList<>();
|
||||
ReentrantLock realtimeDataListenerLock = new ReentrantLock();
|
||||
|
||||
|
||||
public static void send(SseEmitter emitter, String eventName, TruckLocationTrackEntity data) {
|
||||
|
|
@ -385,15 +392,14 @@ public class TruckLocationTrackService extends ServiceImpl<TruckLocationTrackMap
|
|||
RealtimeLocationResult realtimeLocationResult = msg.getMsg(RealtimeLocationResult.class);
|
||||
if (realtimeLocationResult != null) {
|
||||
String gpsId = realtimeLocationResult.getTerminalId();
|
||||
lock.lock();
|
||||
/* lock.lock();
|
||||
List<SseEmitter> emitterList = emitters.get(gpsId);
|
||||
lock.unlock();
|
||||
if (CollUtil.isEmpty(emitterList)) {
|
||||
// log.info("未找到GPS {} 的实时数据订阅者", gpsId);
|
||||
return;
|
||||
}
|
||||
|
||||
for (SseEmitter emitter : emitterList) {
|
||||
} */
|
||||
/* for (SseEmitter emitter : emitterList) {
|
||||
send(emitter, "realtimeData", new TruckLocationTrackEntity()
|
||||
.setId(IdUtil.getSnowflakeNextId())
|
||||
.setTerminalId(gpsId)
|
||||
|
|
@ -405,8 +411,213 @@ public class TruckLocationTrackService extends ServiceImpl<TruckLocationTrackMap
|
|||
.setDirection(realtimeLocationResult.getDirection())
|
||||
.setOverspeed(realtimeLocationResult.isOverspeed())
|
||||
.setCompensate(realtimeLocationResult.getType() == 1));
|
||||
} */
|
||||
List<Tuple3<String, String, String>> listeners;
|
||||
realtimeDataListenerLock.lock();
|
||||
try {
|
||||
realtimeDataListener.removeIf(it -> !Websocket.online(it.get_0(), it.get_1()));
|
||||
listeners = realtimeDataListener.stream()
|
||||
.filter(tuple3 -> tuple3.get_2().equals(gpsId))
|
||||
.map(tuple3 -> Tuple3.create(tuple3.get_0(), tuple3.get_1(), tuple3.get_2()))
|
||||
.collect(Collectors.toList());
|
||||
} finally {
|
||||
realtimeDataListenerLock.unlock();
|
||||
}
|
||||
|
||||
for (Tuple3<String, String, String> tuple3 : listeners) {
|
||||
String uid = tuple3.get_0();
|
||||
String cid = tuple3.get_1();
|
||||
Websocket.send(uid, cid, new WsMsg()
|
||||
.setEvent("realtimeData")
|
||||
.setUid(uid)
|
||||
.setCid(cid)
|
||||
.setData(new TruckLocationTrackEntity()
|
||||
.setId(IdUtil.getSnowflakeNextId())
|
||||
.setTerminalId(gpsId)
|
||||
.setLatitude(realtimeLocationResult.getLatitude())
|
||||
.setLongitude(realtimeLocationResult.getLongitude())
|
||||
.setAltitude(realtimeLocationResult.getAltitude())
|
||||
.setSpeed(realtimeLocationResult.getSpeed())
|
||||
.setLocationTime(LocalDateTime.parse(realtimeLocationResult.getTime(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
|
||||
.setDirection(realtimeLocationResult.getDirection())
|
||||
.setOverspeed(realtimeLocationResult.isOverspeed())
|
||||
.setCompensate(realtimeLocationResult.getType() == 1))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onApplicationReady() {
|
||||
Websocket.addListener("/truck_location_track/history", msg -> {
|
||||
LocationTrackHistoryParam param = msg.extractData(LocationTrackHistoryParam.class);
|
||||
LocalDateTime startTime = param.getStartTime();
|
||||
LocalDateTime endTime = param.getEndTime();
|
||||
Long orderId = param.getOrderId();
|
||||
Integer speed = param.getSpeed();
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
int currentPage = 1;
|
||||
// 分页查询,每次500条
|
||||
while (true) {
|
||||
// 检查是否被取消
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
log.info("任务被取消");
|
||||
return;
|
||||
}
|
||||
|
||||
Page<TruckLocationTrackEntity> page = new Page<>(currentPage, 500);
|
||||
page.addOrder(OrderItem.asc("location_time"));
|
||||
|
||||
// 分页查询当前页数据
|
||||
IPage<TruckLocationTrackEntity> resultPage = this.page(page, Wrappers.lambdaQuery(TruckLocationTrackEntity.class)
|
||||
.eq(TruckLocationTrackEntity::getOrderId, orderId)
|
||||
.ge(startTime != null, TruckLocationTrackEntity::getLocationTime, startTime)
|
||||
.le(endTime != null, TruckLocationTrackEntity::getLocationTime, endTime));
|
||||
|
||||
List<TruckLocationTrackEntity> records = resultPage.getRecords();
|
||||
if (records.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理当前页数据,按时间间隔推送
|
||||
for (TruckLocationTrackEntity record : records) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
log.info("任务被取消");
|
||||
return;
|
||||
}
|
||||
if (speed > 100) {
|
||||
if (!ThreadUtil.sleep(speed)) {
|
||||
log.info("任务被取消");
|
||||
return;
|
||||
}
|
||||
}
|
||||
String uid = msg.getUid();
|
||||
String cid = msg.getCid();
|
||||
if (!Websocket.online(uid, cid)) {
|
||||
return;
|
||||
}
|
||||
Websocket.send(uid, cid, new WsMsg()
|
||||
.setEvent("historyData")
|
||||
.setData(R.success(record))
|
||||
);
|
||||
}
|
||||
|
||||
// 判断是否为最后一页
|
||||
if (currentPage >= resultPage.getPages()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 继续查询下一页
|
||||
currentPage++;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
// 其他异常
|
||||
log.error("查询历史轨迹异常", e);
|
||||
}
|
||||
}, threadPoolExecutor);
|
||||
});
|
||||
|
||||
Websocket.addListener("/truck_location_track/realtime", msg -> {
|
||||
LocationTrackHistoryParam param = msg.extractData(LocationTrackHistoryParam.class);
|
||||
Long orderId = param.getOrderId();
|
||||
String uid = msg.getUid();
|
||||
String cid = msg.getCid();
|
||||
String gpsId = baseMapper.gpsId(orderId);
|
||||
realtimeDataListenerLock.lock();
|
||||
try {
|
||||
realtimeDataListener.add(Tuple3.create(uid, cid, gpsId));
|
||||
} finally {
|
||||
realtimeDataListenerLock.unlock();
|
||||
}
|
||||
|
||||
CompletableFuture.runAsync(() -> {
|
||||
while (true) {
|
||||
ThreadUtil.sleep(3000);
|
||||
realtimeDataListenerLock.lock();
|
||||
try {
|
||||
realtimeDataListener.removeIf(it -> !Websocket.online(it.get_0(), it.get_1()));
|
||||
} finally {
|
||||
realtimeDataListenerLock.unlock();
|
||||
}
|
||||
|
||||
List<Tuple3<String, String, String>> listeners = realtimeDataListener;
|
||||
if (listeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Tuple3<String, String, String> tuple3 : listeners) {
|
||||
if (!Websocket.online(uid, cid)) {
|
||||
return;
|
||||
}
|
||||
Websocket.send(uid, cid, new WsMsg()
|
||||
.setEvent("realtimeData")
|
||||
.setData(R.success(new TruckLocationTrackEntity()))
|
||||
);
|
||||
}
|
||||
}
|
||||
}, threadPoolExecutor);
|
||||
|
||||
|
||||
/* CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
int currentPage = 1;
|
||||
// 分页查询,每次500条
|
||||
while (true) {
|
||||
// 检查是否被取消
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
log.info("任务被取消");
|
||||
return;
|
||||
}
|
||||
|
||||
Page<TruckLocationTrackEntity> page = new Page<>(currentPage, 500);
|
||||
page.addOrder(OrderItem.asc("location_time"));
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
// 分页查询当前页数据
|
||||
IPage<TruckLocationTrackEntity> resultPage = this.page(page, Wrappers.lambdaQuery(TruckLocationTrackEntity.class)
|
||||
.eq(TruckLocationTrackEntity::getOrderId, orderId)
|
||||
.ge(TruckLocationTrackEntity::getLocationTime, now));
|
||||
|
||||
List<TruckLocationTrackEntity> records = resultPage.getRecords();
|
||||
if (records.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理当前页数据,按时间间隔推送
|
||||
for (TruckLocationTrackEntity record : records) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
log.info("任务被取消");
|
||||
return;
|
||||
}
|
||||
if (!ThreadUtil.sleep(1000)) {
|
||||
log.info("任务被取消");
|
||||
return;
|
||||
}
|
||||
Websocket.send(msg.getUid(), msg.getCid(), new WsMsg()
|
||||
.setEvent("historyData")
|
||||
.setUid(msg.getUid())
|
||||
.setCid(msg.getCid())
|
||||
.setData(record)
|
||||
);
|
||||
}
|
||||
|
||||
// 判断是否为最后一页
|
||||
if (currentPage >= resultPage.getPages()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 继续查询下一页
|
||||
currentPage++;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
// 其他异常
|
||||
log.error("查询历史轨迹异常", e);
|
||||
}
|
||||
}, threadPoolExecutor); */
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.njzscloud.supervisory.config;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.njzscloud.supervisory.biz.service.TruckLocationTrackService;
|
||||
import com.njzscloud.supervisory.device.service.DeviceLocalizerService;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SpringReady {
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void onApplicationReady() {
|
||||
SpringUtil.getBean(DeviceLocalizerService.class).onApplicationReady();
|
||||
SpringUtil.getBean(TruckLocationTrackService.class).onApplicationReady();
|
||||
}
|
||||
}
|
||||
|
|
@ -14,8 +14,6 @@ import com.njzscloud.supervisory.device.pojo.result.DeviceInfoResult;
|
|||
import com.njzscloud.supervisory.device.pojo.result.HeartbeatResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
|
|
@ -68,7 +66,6 @@ public class DeviceLocalizerService extends ServiceImpl<DeviceLocalizerMapper, D
|
|||
return PageResult.of(this.page(pageParam.toPage(), Wrappers.<DeviceLocalizerEntity>query(deviceLocalizerEntity)));
|
||||
}
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void onApplicationReady() {
|
||||
List<DeviceLocalizerEntity> localizerEntities = this.list();
|
||||
List<String> collect = localizerEntities.stream().map(DeviceLocalizerEntity::getTerminalId).distinct().collect(Collectors.toList());
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ public class OrderPagingSearchParam {
|
|||
* 订单状态; 字典代码:order_status
|
||||
*/
|
||||
private String orderStatus;
|
||||
/**
|
||||
* 1-->待审核、2-->已审核
|
||||
*/
|
||||
private String auditType;
|
||||
private Long stationId;
|
||||
private CheckStatus checkStatus;
|
||||
|
||||
|
|
|
|||
|
|
@ -452,6 +452,22 @@ public class OrderInfoService extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
|
|||
if (Boolean.TRUE.equals(orderPagingSearchParam.getIsCertificatePaging())) {
|
||||
ew.isNotNull("a.certificate_sn");
|
||||
}
|
||||
String sn = orderPagingSearchParam.getSn();
|
||||
String nickname = orderPagingSearchParam.getNickname();
|
||||
String phone = orderPagingSearchParam.getPhone();
|
||||
String licensePlate = orderPagingSearchParam.getLicensePlate();
|
||||
LocalDateTime startTime = orderPagingSearchParam.getStartTime();
|
||||
LocalDateTime endTime = orderPagingSearchParam.getEndTime();
|
||||
|
||||
ew
|
||||
.like(StrUtil.isNotBlank(sn), "a.sn", sn)
|
||||
.like(StrUtil.isNotBlank(nickname), "a.contacts", nickname)
|
||||
.like(StrUtil.isNotBlank(phone), "a.phone", phone)
|
||||
.like(StrUtil.isNotBlank(licensePlate), "e.license_plate", licensePlate)
|
||||
.ge(startTime != null, "e.create_time", startTime)
|
||||
.le(endTime != null, "e.create_time", endTime)
|
||||
;
|
||||
|
||||
if (auditStatus != null) {
|
||||
if (auditStatus == AuditStatus.DaiShenHe) {
|
||||
ew
|
||||
|
|
@ -479,6 +495,23 @@ public class OrderInfoService extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
|
|||
.or(CollUtil.isNotEmpty(cityAreaList), it2 -> it2.in("b.area", cityAreaList).eq("a.audit_status", AuditStatus.BoHui))
|
||||
);
|
||||
}
|
||||
} else {
|
||||
String auditType = orderPagingSearchParam.getAuditType();
|
||||
if ("1".equals(auditType)) {
|
||||
ew.eq("a.order_status", OrderStatus.YiJieDan)
|
||||
.and(it ->
|
||||
it.in(CollUtil.isNotEmpty(areaList), "b.area", areaList).in("a.audit_status", AuditStatus.QuDaiShenHe, AuditStatus.ShiDaiShenHe)
|
||||
.or(CollUtil.isNotEmpty(cityList), it1 -> it1.in("b.area", cityList).eq("a.audit_status", AuditStatus.ShiDaiShenHe))
|
||||
.or(CollUtil.isNotEmpty(cityAreaList), it2 -> it2.in("b.area", cityAreaList).eq("a.audit_status", AuditStatus.DaiShenHe))
|
||||
);
|
||||
} else if ("2".equals(auditType)) {
|
||||
ew
|
||||
.in("a.order_status", OrderStatus.YiJieDan, OrderStatus.QingYunZhong, OrderStatus.YiJinChang, OrderStatus.YiChuChang, OrderStatus.YiWanCheng)
|
||||
.and(it ->
|
||||
it.in(CollUtil.isNotEmpty(areaList), "b.area", areaList).eq("a.audit_status", AuditStatus.ShiDaiShenHe)
|
||||
.or(CollUtil.isNotEmpty(cityList), it1 -> it1.in("b.area", cityList).in("a.audit_status", AuditStatus.TongGuo, AuditStatus.BoHui))
|
||||
.or(CollUtil.isNotEmpty(cityAreaList), it2 -> it2.in("b.area", cityAreaList).in("a.audit_status", AuditStatus.TongGuo, AuditStatus.BoHui))
|
||||
);
|
||||
} else {
|
||||
ew
|
||||
.in("a.order_status", OrderStatus.YiJieDan, OrderStatus.QingYunZhong, OrderStatus.YiJinChang, OrderStatus.YiChuChang, OrderStatus.YiWanCheng)
|
||||
|
|
@ -488,6 +521,7 @@ public class OrderInfoService extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
|
|||
.or(CollUtil.isNotEmpty(cityAreaList), it2 -> it2.in("b.area", cityAreaList)))
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
return PageResult.of(baseMapper.paging(page, ew));
|
||||
}
|
||||
|
|
@ -1138,7 +1172,8 @@ public class OrderInfoService extends ServiceImpl<OrderInfoMapper, OrderInfoEnti
|
|||
ew.like(StrUtil.isNotBlank(searchParam.getPhone()), "a.phone", searchParam.getPhone());
|
||||
ew.like(StrUtil.isNotBlank(searchParam.getNickname()), "a.contacts", searchParam.getNickname());
|
||||
ew.ge(startTime != null, "a.create_time", startTime);
|
||||
ew.le(endTime != null, "a.create_time", endTime);;
|
||||
ew.le(endTime != null, "a.create_time", endTime);
|
||||
;
|
||||
historyEW(searchParam, null, ew);
|
||||
List<OrderExportResult> list = baseMapper.exportList(ew);
|
||||
List<Map<String, Object>> downList = new ArrayList<>();
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ spring:
|
|||
- /bulletin/detail
|
||||
- /bulletin/paging
|
||||
- /truck_location_track/**
|
||||
- /fdx
|
||||
app:
|
||||
default-place:
|
||||
province: 320000
|
||||
|
|
|
|||
5
pom.xml
5
pom.xml
|
|
@ -52,6 +52,11 @@
|
|||
<artifactId>njzscloud-common-core</artifactId>
|
||||
<version>0.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.njzscloud</groupId>
|
||||
<artifactId>njzscloud-common-ws</artifactId>
|
||||
<version>0.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.njzscloud</groupId>
|
||||
<artifactId>njzscloud-common-sichen</artifactId>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,548 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<title>WebSocket测试工具</title>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #3b82f6;
|
||||
--secondary-color: #10b981;
|
||||
--danger-color: #ef4444;
|
||||
--dark-color: #1e293b;
|
||||
--light-color: #f8fafc;
|
||||
--gray-color: #94a3b8;
|
||||
--border-color: #e2e8f0;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f1f5f9;
|
||||
color: var(--dark-color);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 10px;
|
||||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
header p {
|
||||
color: var(--gray-color);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.control-panel {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
height: 600px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--dark-color);
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px 16px;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s, transform 0.1s;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #2563eb;
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #059669;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: var(--danger-color);
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #dc2626;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.message-area {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 600px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.message-history {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
height: 500px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message-controls {
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.message-input-area {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.message-input-area textarea {
|
||||
flex: 1;
|
||||
resize: none;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 16px;
|
||||
max-width: 80%;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message-sent {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.message-received {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
position: relative;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message-sent .message-content {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
.message-received .message-content {
|
||||
background-color: #e2e8f0;
|
||||
color: var(--dark-color);
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 0.8rem;
|
||||
color: var(--gray-color);
|
||||
margin-top: 4px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background-color: rgba(16, 185, 129, 0.1);
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.connection-info {
|
||||
font-size: 0.9rem;
|
||||
color: var(--gray-color);
|
||||
margin-top: 5px;
|
||||
font-family: monospace;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--gray-color);
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.empty-state i {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
max-width: 400px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1><i class="fas fa-plug"></i> WebSocket 测试工具</h1>
|
||||
<p>简单高效地测试WebSocket连接和通信</p>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="control-panel">
|
||||
<div class="form-group">
|
||||
<label for="ws-url">WebSocket 地址</label>
|
||||
<input id="ws-url" placeholder="ws://example.com/ws 或 wss://example.com/ws" type="text"
|
||||
value="ws://127.0.0.1:10086/fdx">
|
||||
<div class="connection-info" id="connection-status">未连接</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="ws-authorization">TOKEN</label>
|
||||
<input id="ws-authorization" placeholder="TOKEN"
|
||||
type="text"
|
||||
value="MSwxLGNlMWU2MmJhNWExNzQ1YzE4NDAxNGNlN2Q5MTkxODU5LDE3NTkyMjAzNjMxODEsMCxQQVNTV09SRA==">
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-success" id="connect-btn">
|
||||
<i class="fas fa-play"></i> 连接
|
||||
</button>
|
||||
<button class="btn btn-danger" disabled id="disconnect-btn">
|
||||
<i class="fas fa-stop"></i> 断开
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>连接信息</label>
|
||||
<div class="connection-info" id="connection-details">未连接到服务器</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-area">
|
||||
<!-- 新增消息控制栏,包含清除按钮 -->
|
||||
<div class="message-controls">
|
||||
<button class="btn" id="clear-btn">
|
||||
<i class="fas fa-trash"></i> 清除消息
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="message-history" id="message-history">
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-comments"></i>
|
||||
<p>连接到WebSocket服务器后,消息将显示在这里</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-input-area">
|
||||
<textarea id="message-input" placeholder="输入要发送的消息..."></textarea>
|
||||
<button class="btn" disabled id="send-btn">
|
||||
<i class="fas fa-paper-plane"></i> 发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 获取DOM元素
|
||||
const wsUrlInput = document.getElementById('ws-url');
|
||||
const wsAuthorizationInput = document.getElementById('ws-authorization');
|
||||
const connectBtn = document.getElementById('connect-btn');
|
||||
const disconnectBtn = document.getElementById('disconnect-btn');
|
||||
const sendBtn = document.getElementById('send-btn');
|
||||
const clearBtn = document.getElementById('clear-btn'); // 获取清除按钮
|
||||
const messageInput = document.getElementById('message-input');
|
||||
const messageHistory = document.getElementById('message-history');
|
||||
const connectionStatus = document.getElementById('connection-status');
|
||||
const connectionDetails = document.getElementById('connection-details');
|
||||
|
||||
// WebSocket实例
|
||||
let websocket = null;
|
||||
|
||||
// 连接WebSocket
|
||||
connectBtn.addEventListener('click', () => {
|
||||
let url = wsUrlInput.value.trim();
|
||||
if (!url) {
|
||||
alert('请输入WebSocket地址');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证URL格式
|
||||
if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
|
||||
alert('WebSocket地址必须以ws://或wss://开头');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取协议(如果有)
|
||||
const authorization = wsAuthorizationInput.value.trim();
|
||||
try {
|
||||
// 创建WebSocket连接
|
||||
url = `${url}?authorization=${authorization}`;
|
||||
websocket = new WebSocket(url);
|
||||
|
||||
// 连接成功
|
||||
websocket.onopen = () => {
|
||||
updateConnectionStatus(true);
|
||||
addSystemMessage('已成功连接到服务器');
|
||||
|
||||
// 显示连接详情
|
||||
connectionDetails.textContent = `地址: ${url}`;
|
||||
};
|
||||
|
||||
// 接收消息
|
||||
websocket.onmessage = (event) => {
|
||||
addMessage(event.data, false);
|
||||
};
|
||||
|
||||
// 连接关闭
|
||||
websocket.onclose = (event) => {
|
||||
updateConnectionStatus(false);
|
||||
addSystemMessage(`连接已关闭 (代码: ${event.code}, 原因: ${event.reason || '无'})`);
|
||||
connectionDetails.textContent = '未连接到服务器';
|
||||
};
|
||||
|
||||
// 连接错误
|
||||
websocket.onerror = (error) => {
|
||||
addSystemMessage(`发生错误: ${error.message || '未知错误'}`, true);
|
||||
};
|
||||
} catch (error) {
|
||||
alert(`连接失败: ${error.message}`);
|
||||
updateConnectionStatus(false);
|
||||
}
|
||||
});
|
||||
|
||||
// 断开连接
|
||||
disconnectBtn.addEventListener('click', () => {
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
websocket.close(1000, '客户端主动断开连接');
|
||||
websocket = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 发送消息
|
||||
sendBtn.addEventListener('click', sendMessage);
|
||||
|
||||
// 清除消息
|
||||
clearBtn.addEventListener('click', () => {
|
||||
// 恢复空状态显示
|
||||
messageHistory.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-comments"></i>
|
||||
<p>连接到WebSocket服务器后,消息将显示在这里</p>
|
||||
</div>
|
||||
`;
|
||||
// addSystemMessage('消息历史已清除');
|
||||
});
|
||||
|
||||
// 按Enter发送消息,Shift+Enter换行
|
||||
messageInput.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
});
|
||||
|
||||
// 发送消息函数
|
||||
function sendMessage() {
|
||||
const message = messageInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
||||
websocket.send(message);
|
||||
addMessage(message, true);
|
||||
// messageInput.value = '';
|
||||
} else {
|
||||
addSystemMessage('无法发送消息,未连接到服务器', true);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加消息到历史记录
|
||||
function addMessage(content, isSent) {
|
||||
// 清除空状态提示
|
||||
if (messageHistory.querySelector('.empty-state')) {
|
||||
messageHistory.innerHTML = '';
|
||||
}
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${isSent ? 'message-sent' : 'message-received'}`;
|
||||
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString();
|
||||
|
||||
messageDiv.innerHTML = `
|
||||
<div class="message-content">${escapeHtml(content)}</div>
|
||||
<div class="message-time">${timeString}</div>
|
||||
`;
|
||||
|
||||
messageHistory.appendChild(messageDiv);
|
||||
messageHistory.scrollTop = messageHistory.scrollHeight;
|
||||
}
|
||||
|
||||
// 添加系统消息
|
||||
function addSystemMessage(content, isError = false) {
|
||||
// 清除空状态提示
|
||||
if (messageHistory.querySelector('.empty-state')) {
|
||||
messageHistory.innerHTML = '';
|
||||
}
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = 'message';
|
||||
|
||||
const now = new Date();
|
||||
const timeString = now.toLocaleTimeString();
|
||||
|
||||
messageDiv.innerHTML = `
|
||||
<div class="message-content" style="background-color: ${isError ? '#fee2e2' : '#eff6ff'}; color: ${isError ? '#dc2626' : '#1e40af'}">
|
||||
<i class="${isError ? 'fas fa-exclamation-circle' : 'fas fa-info-circle'}"></i> ${escapeHtml(content)}
|
||||
</div>
|
||||
<div class="message-time">${timeString}</div>
|
||||
`;
|
||||
|
||||
messageHistory.appendChild(messageDiv);
|
||||
messageHistory.scrollTop = messageHistory.scrollHeight;
|
||||
}
|
||||
|
||||
// 更新连接状态
|
||||
function updateConnectionStatus(isConnected) {
|
||||
if (isConnected) {
|
||||
connectBtn.disabled = true;
|
||||
disconnectBtn.disabled = false;
|
||||
sendBtn.disabled = false;
|
||||
wsUrlInput.disabled = true;
|
||||
wsAuthorizationInput.disabled = true;
|
||||
|
||||
connectionStatus.innerHTML = '已连接 <span class="status status-connected">在线</span>';
|
||||
} else {
|
||||
connectBtn.disabled = false;
|
||||
disconnectBtn.disabled = true;
|
||||
sendBtn.disabled = true;
|
||||
wsUrlInput.disabled = false;
|
||||
wsAuthorizationInput.disabled = false;
|
||||
|
||||
connectionStatus.innerHTML = '未连接 <span class="status status-disconnected">离线</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// HTML转义函数,防止XSS
|
||||
function escapeHtml(unsafe) {
|
||||
return unsafe
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in New Issue