diff --git a/njzscloud-common/njzscloud-common-ws/pom.xml b/njzscloud-common/njzscloud-common-ws/pom.xml
new file mode 100644
index 0000000..ae641b0
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-ws/pom.xml
@@ -0,0 +1,38 @@
+
+ 4.0.0
+
+ com.njzscloud
+ njzscloud-common
+ 0.0.1
+
+
+ njzscloud-common-ws
+ jar
+
+
+ UTF-8
+
+
+
+
+ com.njzscloud
+ njzscloud-common-core
+ provided
+
+
+ com.njzscloud
+ njzscloud-common-security
+ provided
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ provided
+
+
+ org.springframework
+ spring-websocket
+
+
+
diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/config/WebSocketAutoConfiguration.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/config/WebSocketAutoConfiguration.java
new file mode 100644
index 0000000..29870d3
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/config/WebSocketAutoConfiguration.java
@@ -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;
+ }
+}
diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/config/WebsocketProperties.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/config/WebsocketProperties.java
new file mode 100644
index 0000000..3f2c0c1
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/config/WebsocketProperties.java
@@ -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);
+}
diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Constants.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Constants.java
new file mode 100644
index 0000000..adb1b09
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Constants.java
@@ -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";
+
+}
diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/ImpartialWebSocketHandler.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/ImpartialWebSocketHandler.java
new file mode 100644
index 0000000..bc1edd9
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/ImpartialWebSocketHandler.java
@@ -0,0 +1,97 @@
+package com.njzscloud.common.ws.support;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import com.njzscloud.common.core.ex.ExceptionMsg;
+import com.njzscloud.common.core.utils.R;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.AbstractWebSocketHandler;
+
+
+/**
+ * websocket 处理器
+ * 不支持分片消息
+ * 支持文本、二进制信息
+ */
+@Slf4j
+public class ImpartialWebSocketHandler extends AbstractWebSocketHandler {
+
+ @Override
+ public void afterConnectionEstablished(WebSocketSession session) throws Exception {
+ Wsdoor wsdoor = Wsdoor.of(session);
+ String cid = wsdoor.getCid();
+ Long uid = wsdoor.getUid();
+ wsdoor.send(new WsMsg()
+ .setEvent("connected")
+ .setData(R.success(
+ MapUtil.builder()
+ .put("uid", uid)
+ .put("cid", cid)
+ .build()
+ , "连接成功")));
+ }
+
+ @Override
+ protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
+ Wsdoor wsdoor = Wsdoor.of(session);
+ WsMsg wsMsg = WsMsg.of(wsdoor, message);
+
+ if (wsMsg == null
+ || StrUtil.isBlank(wsMsg.getAction())
+ || StrUtil.isBlank(wsMsg.getEvent())) {
+ wsdoor.send(new WsMsg().setEvent("error")
+ .setData(R.failed(ExceptionMsg.CLI_ERR_MSG, "消息格式错误")));
+ session.close();
+ return;
+ }
+ String action = wsMsg.getAction();
+
+ if (!Websocket.ACTION_PUBLISH.equals(action)
+ && !Websocket.ACTION_PING.equals(action)
+ && !Websocket.ACTION_UNSUBSCRIBE.equals(action)
+ // && !Websocket.ACTION_PONG.equals(action)
+ && !Websocket.ACTION_SUBSCRIBE.equals(action)
+ ) {
+ wsdoor.send(new WsMsg().setEvent("error")
+ .setData(R.failed(ExceptionMsg.CLI_ERR_MSG, "消息格式错误")));
+ session.close();
+ return;
+ }
+
+ String event = wsMsg.getEvent();
+ if (Websocket.ACTION_PUBLISH.equals(action)) {
+ Websocket.publish(wsMsg);
+ } else if (Websocket.ACTION_SUBSCRIBE.equals(action)) {
+ WsMsg.User from = wsMsg.getFrom();
+ log.info("用户Id:{},客户端Id:{},订阅:{}", from.getUid(), from.getCid(), event);
+ Websocket.subscribe(event, wsdoor);
+ } else if (Websocket.ACTION_UNSUBSCRIBE.equals(action)) {
+ WsMsg.User from = wsMsg.getFrom();
+ log.info("用户Id:{},客户端Id:{},取消订阅:{}", from.getUid(), from.getCid(), event);
+ Websocket.unsubscribe(event, wsdoor);
+ } else {
+ wsdoor.send(new WsMsg().setAction(Websocket.ACTION_PONG));
+ }
+ }
+
+ @Override
+ public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
+ Wsdoor wsdoor = Wsdoor.of(session);
+ String cid = wsdoor.getCid();
+ Long uid = wsdoor.getUid();
+ log.error("连接失败: {} {}", cid, uid, exception);
+ Websocket.unsubscribe(wsdoor);
+ }
+
+ @Override
+ public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
+ Wsdoor wsdoor = Wsdoor.of(session);
+ String cid = wsdoor.getCid();
+ Long uid = wsdoor.getUid();
+ log.info("连接关闭: {} {} {}", cid, uid, closeStatus);
+ Websocket.unsubscribe(wsdoor);
+ }
+}
diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/TokenHandshakeInterceptor.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/TokenHandshakeInterceptor.java
new file mode 100644
index 0000000..4dc18b0
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/TokenHandshakeInterceptor.java
@@ -0,0 +1,76 @@
+package com.njzscloud.common.ws.support;
+
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import com.njzscloud.common.core.tuple.Tuple2;
+import com.njzscloud.common.security.support.Token;
+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 拦截器
+ * 子协议格式: Auth [token],[cid], 如: Auth aimsn,niosdawq
+ * token: 用户登录凭证; cid: 客户端号, 推荐使用 token Id
+ */
+@Slf4j
+public class TokenHandshakeInterceptor implements HandshakeInterceptor {
+ private static final String SEC_WEB_SOCKET_PROTOCOL = "Sec-WebSocket-Protocol";
+ private static final String Authorization = "authorization";
+
+ @Override
+ public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler wsHandler, Map attributes) {
+ try {
+ HttpServletRequest req = ((ServletServerHttpRequest) request).getServletRequest();
+ String authorization = req.getParameter(Authorization);
+ // if (StrUtil.isBlank(authorization)) return false;
+
+ Tuple2 resolved = this.resolve(authorization);
+ if (resolved == null) return false;
+ Long uid = resolved.get_0();
+ String cid = resolved.get_1();
+ attributes.put(Constants.SESSION_ATTRIBUTE_TOKEN, StrUtil.isBlank(authorization) ? cid : 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 Tuple2 resolve(String tokenStr) {
+ if (StrUtil.isBlank(tokenStr)) return Tuple2.create(1L, IdUtil.fastSimpleUUID());
+ UserDetail userDetail = SecurityUtil.parseToken(tokenStr);
+ if (userDetail == null) return null;
+ Token token = userDetail.getToken();
+ if (token == null) return null;
+ return Tuple2.create(userDetail.getUserId(), token.getTid());
+ }
+}
diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/WSListener.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/WSListener.java
new file mode 100644
index 0000000..86b5c3b
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/WSListener.java
@@ -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();
+}
diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Websocket.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Websocket.java
new file mode 100644
index 0000000..f1e7fc5
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Websocket.java
@@ -0,0 +1,175 @@
+package com.njzscloud.common.ws.support;
+
+import cn.hutool.core.collection.CollUtil;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * websocket 工具
+ */
+@Slf4j
+public class Websocket {
+
+ public static final String ACTION_PUBLISH = "publish";
+ public static final String ACTION_PING = "ping";
+ public static final String ACTION_PONG = "pong";
+ public static final String ACTION_SUBSCRIBE = "subscribe";
+ public static final String ACTION_UNSUBSCRIBE = "unsubscribe";
+
+ /**
+ * 客户端事件订阅列表
+ */
+ static final ConcurrentHashMap> EVENT_CACHE = new ConcurrentHashMap<>();
+ static final ReentrantReadWriteLock EVENT_CACHE_LOCK = new ReentrantReadWriteLock();
+ static final ReentrantReadWriteLock.ReadLock EVENT_CACHE_READ_LOCK = EVENT_CACHE_LOCK.readLock();
+ static final ReentrantReadWriteLock.WriteLock EVENT_CACHE_WRITE_LOCK = EVENT_CACHE_LOCK.writeLock();
+
+ static final ConcurrentHashMap>> LISTENER_CACHE = new ConcurrentHashMap<>();
+ static final ReentrantReadWriteLock LISTENER_CACHE_LOCK = new ReentrantReadWriteLock();
+ static final ReentrantReadWriteLock.ReadLock LISTENER_CACHE_READ_LOCK = LISTENER_CACHE_LOCK.readLock();
+ static final ReentrantReadWriteLock.WriteLock LISTENER_CACHE_WRITE_LOCK = LISTENER_CACHE_LOCK.writeLock();
+
+ public static boolean publish(WsMsg wsMsg) {
+ wsMsg.setAction(ACTION_PUBLISH);
+ // 总数,成功数
+ Integer[] success = {0, 0};
+ String event = wsMsg.getEvent();
+ List to = wsMsg.getTo();
+ Set uids;
+ Set cids;
+ if (CollUtil.isNotEmpty(to)) {
+ uids = to.stream().map(WsMsg.User::getUid).collect(Collectors.toSet());
+ cids = to.stream().map(WsMsg.User::getCid).collect(Collectors.toSet());
+ } else {
+ uids = Collections.emptySet();
+ cids = Collections.emptySet();
+ }
+ EVENT_CACHE_READ_LOCK.lock();
+ try {
+ EVENT_CACHE.computeIfPresent(event, (k, v) -> {
+ if (CollUtil.isEmpty(v)) return v;
+ List wsdoors = v;
+ if (!uids.isEmpty() || !cids.isEmpty()) {
+ wsdoors = v.stream().filter(it -> {
+ Long uid = it.getUid();
+ String cid = it.getCid();
+ return cids.contains(cid) || uids.contains(uid);
+ }).collect(Collectors.toList());
+ }
+ success[1] = wsdoors.size();
+ for (Wsdoor wsdoor : wsdoors) {
+ boolean send = wsdoor.send(wsMsg);
+ if (send) success[1] = success[1] + 1;
+ }
+ return v;
+ });
+ } finally {
+ EVENT_CACHE_READ_LOCK.unlock();
+ }
+
+ LISTENER_CACHE_READ_LOCK.lock();
+ try {
+ LISTENER_CACHE.computeIfPresent(event, (k, v) -> {
+ if (CollUtil.isEmpty(v)) return v;
+ success[1] = success[1] + v.size();
+ for (Consumer listener : v) {
+ try {
+ listener.accept(wsMsg);
+ success[1] = success[1] + 1;
+ } catch (Exception e) {
+ log.error("事件 {} 监听方法执行异常", event, e);
+ }
+ }
+ return v;
+ });
+ } finally {
+ LISTENER_CACHE_READ_LOCK.unlock();
+ }
+
+ return success[1] > 0;
+ }
+
+ public static boolean publish(String event, Object data) {
+ return publish(new WsMsg()
+ .setAction(ACTION_PUBLISH)
+ .setEvent(event)
+ .setData(data));
+ }
+
+ public static void subscribe(String event, Wsdoor wsdoor) {
+ EVENT_CACHE_READ_LOCK.lock();
+ try {
+ EVENT_CACHE.computeIfAbsent(event, k -> new LinkedList<>());
+
+ EVENT_CACHE.computeIfPresent(event, (k, v) -> {
+ v.add(wsdoor);
+ return v;
+ });
+ } finally {
+ EVENT_CACHE_READ_LOCK.unlock();
+ }
+ }
+
+ public static void unsubscribe(Wsdoor wsdoor) {
+ EVENT_CACHE_WRITE_LOCK.lock();
+ try {
+ EVENT_CACHE.forEach((k, v) -> {
+ v.removeIf(it -> it.equals(wsdoor));
+ });
+ EVENT_CACHE.entrySet().removeIf(it -> CollUtil.isEmpty(it.getValue()));
+ } finally {
+ EVENT_CACHE_WRITE_LOCK.unlock();
+ }
+ }
+
+ public static void unsubscribe(String event, Wsdoor wsdoor) {
+ EVENT_CACHE_WRITE_LOCK.lock();
+ try {
+ EVENT_CACHE.computeIfPresent(event, (k, v) -> {
+ v.removeIf(it -> it.equals(wsdoor));
+ return v;
+ });
+ EVENT_CACHE.entrySet().removeIf(it -> CollUtil.isEmpty(it.getValue()));
+ } finally {
+ EVENT_CACHE_WRITE_LOCK.unlock();
+ }
+ }
+
+ /**
+ * 系统内部专用订阅, 用于系统事件通知
+ *
+ * @param event 事件名称
+ */
+ public static void subscribe(String event, Consumer listener) {
+ LISTENER_CACHE_READ_LOCK.lock();
+ try {
+ LISTENER_CACHE.computeIfAbsent(event, k -> new LinkedList<>());
+ LISTENER_CACHE.computeIfPresent(event, (k, v) -> {
+ v.add(listener);
+ return v;
+ });
+ } finally {
+ LISTENER_CACHE_READ_LOCK.unlock();
+ }
+ }
+
+ public static void unsubscribe(String event, Consumer listener) {
+ LISTENER_CACHE_READ_LOCK.lock();
+ try {
+ LISTENER_CACHE.computeIfPresent(event, (k, v) -> {
+ v.removeIf(it -> it == listener);
+ return v;
+ });
+ } finally {
+ LISTENER_CACHE_READ_LOCK.unlock();
+ }
+ }
+}
diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/WsMsg.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/WsMsg.java
new file mode 100644
index 0000000..14a64c9
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/WsMsg.java
@@ -0,0 +1,92 @@
+package com.njzscloud.common.ws.support;
+
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.njzscloud.common.core.jackson.Jackson;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.socket.TextMessage;
+
+import java.util.List;
+
+/**
+ * websocket 消息内容
+ */
+@Slf4j
+@Getter
+@Setter
+@Accessors(chain = true)
+public class WsMsg {
+ /**
+ * 发送方
+ */
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private User from;
+
+ /**
+ * 接收方
+ */
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private List to;
+
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private String action;
+
+ /**
+ * 事件
+ */
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private String event;
+
+ /**
+ * 数据内容
+ */
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private Object data;
+
+ public static WsMsg of(Wsdoor wsdoor, TextMessage message) {
+ String payload = message.getPayload();
+ try {
+ WsMsg wsMsg = Jackson.toBean(payload, WsMsg.class);
+ if (wsMsg != null) {
+ wsMsg.setFrom(new User()
+ .setUid(wsdoor.getUid())
+ .setCid(wsdoor.getCid())
+ );
+ }
+ return wsMsg;
+ } catch (Exception e) {
+ log.error("WebSocket 消息反序列化失败, 数据: {}", payload, e);
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return Jackson.toJsonStr(this);
+ }
+
+ public T extractData(Class clazz) {
+ return Jackson.toBean(Jackson.toJsonStr(data), clazz);
+ }
+
+ @Getter
+ @Setter
+ @ToString
+ @Accessors(chain = true)
+ public static class User {
+ /**
+ * 用户 Id
+ */
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ private Long uid;
+ /**
+ * 客户端 Id
+ */
+ @JsonInclude(JsonInclude.Include.NON_EMPTY)
+ private String cid;
+ }
+}
diff --git a/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Wsdoor.java b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Wsdoor.java
new file mode 100644
index 0000000..386f163
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-ws/src/main/java/com/njzscloud/common/ws/support/Wsdoor.java
@@ -0,0 +1,78 @@
+package com.njzscloud.common.ws.support;
+
+import cn.hutool.core.map.MapUtil;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.Map;
+import java.util.Objects;
+
+@Slf4j
+@Getter
+@Setter
+@ToString
+@Accessors(chain = true)
+public class Wsdoor {
+
+ /**
+ * 用户 Id
+ */
+ private final Long uid;
+ /**
+ * 客户端 Id
+ */
+ private final String cid;
+ /**
+ * TOKEN
+ */
+ private final String token;
+ /**
+ * 连接会话
+ */
+ private final WebSocketSession session;
+
+ private Wsdoor(WebSocketSession session) {
+ Map attributes = session.getAttributes();
+ this.uid = MapUtil.getLong(attributes, "uid");
+ this.cid = MapUtil.getStr(attributes, "cid");
+ this.token = MapUtil.getStr(attributes, "token");
+ this.session = session;
+ }
+
+ public static Wsdoor of(WebSocketSession session) {
+ return new Wsdoor(session);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
+ Wsdoor wsdoor = (Wsdoor) o;
+ return Objects.equals(uid, wsdoor.uid) && Objects.equals(cid, wsdoor.cid);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(uid, cid);
+ }
+
+ /**
+ * 发送文本数据
+ *
+ * @param wsMsg 消息
+ */
+ public boolean send(WsMsg wsMsg) {
+ try {
+ if (!session.isOpen()) return false;
+ session.sendMessage(new TextMessage(wsMsg.toString()));
+ return true;
+ } catch (Exception e) {
+ log.error("消息发送失败", e);
+ }
+ return false;
+ }
+}
diff --git a/njzscloud-common/njzscloud-common-ws/src/main/resources/META-INF/spring.factories b/njzscloud-common/njzscloud-common-ws/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000..9e894b6
--- /dev/null
+++ b/njzscloud-common/njzscloud-common-ws/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
+ com.njzscloud.common.ws.config.WebSocketAutoConfiguration
diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/statistics/controller/SupervisionStatisticsController.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/statistics/controller/SupervisionStatisticsController.java
index abc858c..6150115 100644
--- a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/statistics/controller/SupervisionStatisticsController.java
+++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/statistics/controller/SupervisionStatisticsController.java
@@ -1,19 +1,30 @@
package com.njzscloud.supervisory.statistics.controller;
-/* @Slf4j
+import com.njzscloud.common.core.utils.R;
+import com.njzscloud.supervisory.statistics.service.SupervisionStatisticsService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Map;
+
+@Slf4j
@CrossOrigin
@RestController
@RequiredArgsConstructor
-@RequestMapping("/statistics/supervision") */
+@RequestMapping("/statistics/supervision")
public class SupervisionStatisticsController {
- // private final SupervisionStatisticsService supervisionStatisticsService;
+ private final SupervisionStatisticsService supervisionStatisticsService;
- /* @GetMapping("/obtain_data")
+ @GetMapping("/obtain_data")
public R> obtainData() {
long l = System.currentTimeMillis();
Map data = supervisionStatisticsService.obtainData();
System.out.println("耗时: " + (System.currentTimeMillis() - l));
return R.success(data);
- } */
+ }
}
diff --git a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/statistics/mapper/ShouJuStatisticsMapper.java b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/statistics/mapper/ShouJuStatisticsMapper.java
index 6aea025..3f812fd 100644
--- a/njzscloud-svr/src/main/java/com/njzscloud/supervisory/statistics/mapper/ShouJuStatisticsMapper.java
+++ b/njzscloud-svr/src/main/java/com/njzscloud/supervisory/statistics/mapper/ShouJuStatisticsMapper.java
@@ -6,6 +6,7 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
+import java.util.Map;
@Mapper
public interface ShouJuStatisticsMapper {
@@ -28,4 +29,6 @@ public interface ShouJuStatisticsMapper {
Double disposeWeight(@Param("startTime") Long startTime, @Param("endTime") Long endTime);
List getOrderAmountSummary(@Param("startTime") Long startTime, @Param("endTime") Long endTime);
+
+ List