chenbiao 1 년 전
부모
커밋
491e3beb6f
48개의 변경된 파일3006개의 추가작업 그리고 0개의 파일을 삭제
  1. 38 0
      midjourney-netty-websocket-spring-boot-starter/pom.xml
  2. 23 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/NettyWebsocketAutoConfiguration.java
  3. 52 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/WebsocketProperties.java
  4. 15 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/HandshakeBefore.java
  5. 15 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/OnClose.java
  6. 15 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/OnError.java
  7. 15 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/OnEvent.java
  8. 15 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/OnMessage.java
  9. 15 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/OnOpen.java
  10. 16 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/PathParam.java
  11. 17 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/WsServerEndpoint.java
  12. 12 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/exception/WebsocketDeploymentException.java
  13. 24 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/netty/AttributeKeyConstant.java
  14. 39 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/netty/GenericHandler.java
  15. 82 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/netty/HttpRequestHandler.java
  16. 69 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/netty/NettyWebsocketServer.java
  17. 51 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/netty/WebSocketServerHandler.java
  18. 112 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/netty/WebsocketActionDispatch.java
  19. 31 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/socket/Session.java
  20. 26 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/HttpHeadersMethodArgumentResolver.java
  21. 24 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/IdleEventMethodArgumentResolver.java
  22. 18 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/MethodArgumentResolver.java
  23. 64 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/MethodParamsBuild.java
  24. 31 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/PathParaMethodArgumentResolver.java
  25. 24 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/SessionMethodArgumentResolver.java
  26. 28 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/TextMethodArgumentResolver.java
  27. 83 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/WebSocketAnnotationPostProcessor.java
  28. 139 0
      midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/WebsocketServerEndpoint.java
  29. 3 0
      midjourney-netty-websocket-spring-boot-starter/src/main/resources/META-INF/spring.factories
  30. 88 0
      midjourney-netty-websocket-web/pom.xml
  31. 17 0
      midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/Application.java
  32. 22 0
      midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/controller/NotifyController.java
  33. 40 0
      midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/MyMetaObjectHandler.java
  34. 37 0
      midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/MybatisPlusConfig.java
  35. 11 0
      midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/mapper/MidjourneyUserConversationMapper.java
  36. 11 0
      midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/mapper/MidjourneyUserMapper.java
  37. 36 0
      midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/model/BaseEntity.java
  38. 18 0
      midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/model/dto/SendMsgReq.java
  39. 57 0
      midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/model/entity/MidjourneyUser.java
  40. 142 0
      midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/model/entity/MidjourneyUserConversation.java
  41. 55 0
      midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/model/enums/WsMessageTypeEnum.java
  42. 778 0
      midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/redis/RedisService.java
  43. 62 0
      midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/service/MidjourneyService.java
  44. 222 0
      midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/service/SocketServer.java
  45. 96 0
      midjourney-netty-websocket-web/src/main/resources/application.yml
  46. 111 0
      midjourney-netty-websocket-web/src/main/resources/config/logback-debug.xml
  47. 105 0
      midjourney-netty-websocket-web/src/main/resources/config/logback.xml
  48. 2 0
      pom.xml

+ 38 - 0
midjourney-netty-websocket-spring-boot-starter/pom.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <artifactId>netflix</artifactId>
+        <groupId>com.netflix</groupId>
+        <version>1.0</version>
+    </parent>
+    <artifactId>midjourney-netty-websocket-spring-boot-starter</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-autoconfigure</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-autoconfigure-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-all</artifactId>
+            <version>4.1.89.Final</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>1.18.26</version>
+        </dependency>
+    </dependencies>
+
+</project>

+ 23 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/NettyWebsocketAutoConfiguration.java

@@ -0,0 +1,23 @@
+package com.yhlxj.netty.websocket.starter;
+
+import com.yhlxj.netty.websocket.starter.support.WebSocketAnnotationPostProcessor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @Author : chan
+ * @Date :2024/9/6
+ */
+@Configuration
+public class NettyWebsocketAutoConfiguration {
+
+    @Bean
+    public WebSocketAnnotationPostProcessor webSocketAnnotationPostProcessor() {
+        return new WebSocketAnnotationPostProcessor();
+    }
+
+    @Bean
+    public WebsocketProperties websocketProperties() {
+        return new WebsocketProperties();
+    }
+}

+ 52 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/WebsocketProperties.java

@@ -0,0 +1,52 @@
+package com.yhlxj.netty.websocket.starter;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+@ConfigurationProperties(prefix = WebsocketProperties.WEBSOCKET_PREFIX)
+@Data
+public class WebsocketProperties {
+
+    public static final String WEBSOCKET_PREFIX = "netty.websocket";
+
+    private Integer port;
+
+    private Integer bossThreadNums = 1;
+
+    private Integer workerThreadNums = 2;
+
+    /**
+     * 连接超时时间
+     */
+    private Integer connectTimeout = 15000;
+    /**
+     * TCP 连接的请求队列的最大长度,默认128
+     */
+    private Integer backLog = 128;
+
+    /**
+     * 消息是否立即发送
+     */
+    private boolean tcpNoDelay = true;
+
+    /**
+     * 心跳读超时时间
+     */
+    private Integer readerIdleTimeSeconds = 60;
+
+    /**
+     * 心跳写超时时间
+     */
+    private Integer writerIdleTimeSeconds = 60;
+
+    private Integer allIdleTimeSeconds = 60;
+
+    /**
+     *
+     */
+    private Integer maxContentLength = 65536;
+}

+ 15 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/HandshakeBefore.java

@@ -0,0 +1,15 @@
+package com.yhlxj.netty.websocket.starter.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface HandshakeBefore {
+}

+ 15 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/OnClose.java

@@ -0,0 +1,15 @@
+package com.yhlxj.netty.websocket.starter.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface OnClose {
+}

+ 15 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/OnError.java

@@ -0,0 +1,15 @@
+package com.yhlxj.netty.websocket.starter.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface OnError {
+}

+ 15 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/OnEvent.java

@@ -0,0 +1,15 @@
+package com.yhlxj.netty.websocket.starter.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface OnEvent {
+}

+ 15 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/OnMessage.java

@@ -0,0 +1,15 @@
+package com.yhlxj.netty.websocket.starter.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface OnMessage {
+}

+ 15 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/OnOpen.java

@@ -0,0 +1,15 @@
+package com.yhlxj.netty.websocket.starter.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface OnOpen {
+}

+ 16 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/PathParam.java

@@ -0,0 +1,16 @@
+package com.yhlxj.netty.websocket.starter.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface PathParam {
+    String value() default "";
+}

+ 17 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/annotations/WsServerEndpoint.java

@@ -0,0 +1,17 @@
+package com.yhlxj.netty.websocket.starter.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface WsServerEndpoint {
+
+    String value() default "/ws/{arg}";
+}

+ 12 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/exception/WebsocketDeploymentException.java

@@ -0,0 +1,12 @@
+package com.yhlxj.netty.websocket.starter.exception;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+public class WebsocketDeploymentException extends RuntimeException{
+
+    public WebsocketDeploymentException(String message) {
+        super(message);
+    }
+}

+ 24 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/netty/AttributeKeyConstant.java

@@ -0,0 +1,24 @@
+package com.yhlxj.netty.websocket.starter.netty;
+
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import io.netty.util.AttributeKey;
+
+import java.util.Map;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+public class AttributeKeyConstant {
+    public static final AttributeKey<String> PATH_KEY = AttributeKey.valueOf("WEBSOCKET_PATH");
+
+    public static final AttributeKey<FullHttpRequest> fullHttpRequest = AttributeKey.valueOf("FullHttpRequest");
+    public static AttributeKey<TextWebSocketFrame> textWebSocketFrame = AttributeKey.valueOf("TextWebSocketFrame");
+
+    public static AttributeKey<Object> idleStateEvent = AttributeKey.valueOf("IdleStateEvent");
+
+    public static AttributeKey<Throwable> throwable = AttributeKey.valueOf("Throwable");
+
+    public static AttributeKey<Map<String,String>> uriTemplateVariables = AttributeKey.valueOf("uriTemplateVariables");
+}

+ 39 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/netty/GenericHandler.java

@@ -0,0 +1,39 @@
+package com.yhlxj.netty.websocket.starter.netty;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+public class GenericHandler extends ChannelInboundHandlerAdapter {
+
+    private final WebsocketActionDispatch websocketActionDispatch;
+
+    public GenericHandler(WebsocketActionDispatch websocketActionDispatch) {
+        this.websocketActionDispatch = websocketActionDispatch;
+    }
+
+    @Override
+    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
+
+        super.channelInactive(ctx);
+    }
+
+    @Override
+    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
+        String uri = ctx.channel().attr(AttributeKeyConstant.PATH_KEY).get();
+        ctx.channel().attr(AttributeKeyConstant.idleStateEvent).set(evt);
+        websocketActionDispatch.dispatch(uri, WebsocketActionDispatch.Action.EVENT,ctx.channel());
+        super.userEventTriggered(ctx, evt);
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        String uri = ctx.channel().attr(AttributeKeyConstant.PATH_KEY).get();
+        ctx.channel().attr(AttributeKeyConstant.throwable).set(cause);
+        websocketActionDispatch.dispatch(uri, WebsocketActionDispatch.Action.ERROR,ctx.channel());
+        super.exceptionCaught(ctx, cause);
+    }
+}

+ 82 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/netty/HttpRequestHandler.java

@@ -0,0 +1,82 @@
+package com.yhlxj.netty.websocket.starter.netty;
+
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame;
+import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
+import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory;
+
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+@ChannelHandler.Sharable
+public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
+
+    private final WebsocketActionDispatch websocketActionDispatch;
+
+    public HttpRequestHandler(WebsocketActionDispatch websocketActionDispatch) {
+        this.websocketActionDispatch = websocketActionDispatch;
+    }
+
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
+        // 校验请求路径
+        boolean pass = verifyRequest(request);
+        if (!pass) {
+            ctx.close();
+        }
+        // 参数传递到WebsocketHandler
+        ctx.channel().attr(AttributeKeyConstant.fullHttpRequest).set(request);
+        ctx.channel().attr(AttributeKeyConstant.PATH_KEY).set(request.uri());
+        Map<String, String> uriTemplateVariables = websocketActionDispatch.getUriTemplateVariables(request.uri());
+        ctx.channel().attr(AttributeKeyConstant.uriTemplateVariables).set(uriTemplateVariables);
+        websocketActionDispatch.dispatch(request.uri(), WebsocketActionDispatch.Action.HAND_SHAKE, ctx.channel());
+        WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(getWebSocketLocation(request), null, true, 65536);
+        WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(request);
+        if (handshaker == null) {
+            WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
+        } else {
+            handshaker.handshake(ctx.channel(), request).addListener(future -> {
+                if (future.isSuccess()) {
+                    websocketActionDispatch.dispatch(request.uri(), WebsocketActionDispatch.Action.OPEN,ctx.channel());
+                } else {
+                    handshaker.close(ctx.channel(), new CloseWebSocketFrame());
+                }
+            });
+        }
+    }
+
+    private static String getWebSocketLocation(FullHttpRequest req) {
+        String location = req.headers().get(HttpHeaderNames.HOST) + req.uri();
+        return "ws://" + location;
+    }
+
+    /**
+     * 验证请求是否是Http升级Websocket
+     * 并且验证uri是否合法
+     * @param request
+     * @return
+     */
+    private boolean verifyRequest(FullHttpRequest request) {
+        HttpHeaders headers = request.headers();
+        String connection = headers.get("Connection");
+        String upgrade = headers.get("Upgrade");
+        String host = headers.get("Host");
+        if (Objects.isNull(connection) || Objects.isNull(upgrade) || Objects.isNull(host)) {
+            return false;
+        } else if (!"Upgrade".equalsIgnoreCase(connection) || !"websocket".equalsIgnoreCase(upgrade))  {
+            return false;
+        } else if (!"GET".equalsIgnoreCase(request.method().name())) {
+            return false;
+        }
+        return websocketActionDispatch.verifyUri(request.uri());
+    }
+}

+ 69 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/netty/NettyWebsocketServer.java

@@ -0,0 +1,69 @@
+package com.yhlxj.netty.websocket.starter.netty;
+
+import com.yhlxj.netty.websocket.starter.WebsocketProperties;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.*;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpServerCodec;
+import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator;
+import io.netty.handler.timeout.IdleStateHandler;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+public class NettyWebsocketServer {
+
+    private final WebsocketActionDispatch websocketActionDispatch;
+
+    private WebsocketProperties websocketProperties;
+
+    public NettyWebsocketServer(WebsocketActionDispatch websocketActionDispatch,WebsocketProperties websocketProperties) {
+        this.websocketActionDispatch = websocketActionDispatch;
+        this.websocketProperties = websocketProperties;
+    }
+
+    public void start() throws InterruptedException {
+        NioEventLoopGroup boss = new NioEventLoopGroup(websocketProperties.getBossThreadNums());
+        NioEventLoopGroup worker = new NioEventLoopGroup(websocketProperties.getWorkerThreadNums());
+        ServerBootstrap bootstrap = new ServerBootstrap();
+        bootstrap.group(boss,worker)
+                .channel(NioServerSocketChannel.class)
+                .childHandler(new ChannelInitializer<NioSocketChannel>() {
+                    @Override
+                    protected void initChannel(NioSocketChannel channel) {
+                        ChannelPipeline pipeline = channel.pipeline();
+                        pipeline.addLast(new HttpServerCodec())
+                                .addLast(new HttpObjectAggregator(websocketProperties.getMaxContentLength()))
+                                .addLast(new IdleStateHandler(websocketProperties.getReaderIdleTimeSeconds()
+                                        ,websocketProperties.getWriterIdleTimeSeconds()
+                                        ,websocketProperties.getAllIdleTimeSeconds()))
+                                .addLast(new HttpRequestHandler(websocketActionDispatch))
+                                .addLast(new WebSocketFrameAggregator(Integer.MAX_VALUE))
+                                .addLast(new GenericHandler(websocketActionDispatch))
+                                .addLast(new WebSocketServerHandler(websocketActionDispatch));
+                    }
+                })
+                // 连接超时时间
+                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS,websocketProperties.getConnectTimeout())
+                // TCP 连接的请求队列的最大长度
+                .option(ChannelOption.SO_BACKLOG,websocketProperties.getBackLog())
+                // 消息是否立即发送
+                .option(ChannelOption.TCP_NODELAY,websocketProperties.isTcpNoDelay())
+                // TCP 建立连接后,每隔一段时间就会对连接做一次探测
+                .childOption(ChannelOption.SO_KEEPALIVE,Boolean.TRUE);
+
+        ChannelFuture channelFuture = bootstrap.bind(websocketProperties.getPort()).sync();
+        Channel serverChannle = channelFuture.channel();
+        serverChannle.closeFuture().addListener(new ChannelFutureListener() {
+            @Override
+            public void operationComplete(ChannelFuture channelFuture) throws Exception {
+                boss.shutdownGracefully();
+                worker.shutdownGracefully();
+            }
+        });
+    }
+}

+ 51 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/netty/WebSocketServerHandler.java

@@ -0,0 +1,51 @@
+package com.yhlxj.netty.websocket.starter.netty;
+
+import io.netty.channel.ChannelFutureListener;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.handler.codec.http.websocketx.*;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+@ChannelHandler.Sharable
+public class WebSocketServerHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
+
+    private final WebsocketActionDispatch websocketActionDispatch;
+
+    public WebSocketServerHandler(WebsocketActionDispatch websocketActionDispatch) {
+        this.websocketActionDispatch = websocketActionDispatch;
+    }
+
+    @Override
+    protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame msg) throws Exception {
+        handleWebSocketFrame(ctx, msg);
+    }
+
+    private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
+        // 获取上下文传递过来的uri,给分发器分发任务使用
+        String uri = ctx.channel().attr(AttributeKeyConstant.PATH_KEY).get();
+        if (frame instanceof TextWebSocketFrame) {
+            ctx.channel().attr(AttributeKeyConstant.textWebSocketFrame).set((TextWebSocketFrame) frame);
+            websocketActionDispatch.dispatch(uri, WebsocketActionDispatch.Action.MESSAGE,ctx.channel());
+            return;
+        }
+        if (frame instanceof PingWebSocketFrame) {
+            ctx.writeAndFlush(new PongWebSocketFrame(frame.content().retain()));
+            return;
+        }
+        if (frame instanceof CloseWebSocketFrame) {
+            websocketActionDispatch.dispatch(uri, WebsocketActionDispatch.Action.CLOSE,ctx.channel());
+            ctx.writeAndFlush(frame.retainedDuplicate()).addListener(ChannelFutureListener.CLOSE);
+            return;
+        }
+        if (frame instanceof BinaryWebSocketFrame) {
+            return;
+        }
+        if (frame instanceof PongWebSocketFrame) {
+            return;
+        }
+    }
+}

+ 112 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/netty/WebsocketActionDispatch.java

@@ -0,0 +1,112 @@
+package com.yhlxj.netty.websocket.starter.netty;
+
+import com.yhlxj.netty.websocket.starter.support.MethodParamsBuild;
+import com.yhlxj.netty.websocket.starter.support.WebsocketServerEndpoint;
+import io.netty.channel.Channel;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.ReflectionUtils;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+public class WebsocketActionDispatch {
+
+    private AntPathMatcher antPathMatcher = new AntPathMatcher();
+
+    private final static Map<String, WebsocketServerEndpoint> endpointMap = new ConcurrentHashMap<>(16);
+
+
+    /**
+     * 验证请求路径是否合法
+     * @param uri
+     * @return
+     */
+    protected boolean verifyUri(String uri) {
+        return endpointMap.keySet().stream().anyMatch(e -> antPathMatcher.match(e, uri));
+    }
+
+    /**
+     * 添加websocket服务
+     * @param endpoint
+     */
+     public void addWebsocketServerEndpoint(WebsocketServerEndpoint endpoint) {
+        endpointMap.putIfAbsent(endpoint.getPath(),endpoint);
+    }
+
+
+    /**
+     * uri匹配对应的websocket服务
+     * @param uri
+     * @return
+     */
+    protected WebsocketServerEndpoint matchServerEndpoint(String uri) {
+        for (Map.Entry<String, WebsocketServerEndpoint> entry : endpointMap.entrySet()) {
+            if (antPathMatcher.match(entry.getKey(),uri)) {
+                return entry.getValue();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 事件分发到具体的方法
+     * @param uri
+     * @param action
+     */
+    protected void dispatch(String uri, Action action, Channel channel) {
+        WebsocketServerEndpoint websocketServerEndpoint = matchServerEndpoint(uri);
+        if (Objects.nonNull(websocketServerEndpoint)) {
+            Method method = null;
+            Object obj = websocketServerEndpoint.getObject();
+            switch (action) {
+                case HAND_SHAKE:
+                    method = websocketServerEndpoint.getOnHandShake();
+                    break;
+                case OPEN:
+                    method = websocketServerEndpoint.getOnOpen();
+                    break;
+                case CLOSE:
+                    method = websocketServerEndpoint.getOnClose();
+                    break;
+                case MESSAGE:
+                    method = websocketServerEndpoint.getOnMessage();
+                    break;
+                case EVENT:
+                    method = websocketServerEndpoint.getOnEvent();
+                    break;
+                case ERROR:
+                    method = websocketServerEndpoint.getOnError();
+                    break;
+                default:
+                    break;
+            }
+            if (Objects.nonNull(method)) {
+                Object[] args = new MethodParamsBuild().getMethodArgumentValues(method,channel);
+                ReflectionUtils.invokeMethod(method,obj,args);
+            }
+        }
+
+    }
+
+    public Map<String,String> getUriTemplateVariables(String lookupPath) {
+        WebsocketServerEndpoint websocketServerEndpoint = matchServerEndpoint(lookupPath);
+        return antPathMatcher.extractUriTemplateVariables(websocketServerEndpoint.getPath(), lookupPath);
+    }
+
+
+
+     enum Action {
+        HAND_SHAKE,
+        OPEN,
+        CLOSE,
+        MESSAGE,
+        EVENT,
+        ERROR
+    }
+}

+ 31 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/socket/Session.java

@@ -0,0 +1,31 @@
+package com.yhlxj.netty.websocket.starter.socket;
+
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+
+import java.util.Objects;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+public class Session {
+    private final Channel channel;
+
+    public Session(Channel channel) {
+        this.channel = channel;
+    }
+
+    public void sendText(String text) {
+        channel.writeAndFlush(new TextWebSocketFrame(text));
+    }
+
+    public void close() {
+        channel.close();
+    }
+
+    public String getId() {
+        return channel.id().asShortText();
+    }
+
+}

+ 26 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/HttpHeadersMethodArgumentResolver.java

@@ -0,0 +1,26 @@
+package com.yhlxj.netty.websocket.starter.support;
+
+import com.yhlxj.netty.websocket.starter.netty.AttributeKeyConstant;
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.HttpHeaders;
+import org.springframework.core.MethodParameter;
+
+import java.util.Objects;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+public class HttpHeadersMethodArgumentResolver implements MethodArgumentResolver {
+    @Override
+    public boolean supportsParameter(MethodParameter parameter) {
+        return Objects.equals(parameter.getParameterType(), HttpHeaders.class);
+    }
+
+    @Override
+    public Object resolveArgument(MethodParameter parameter, Channel channel) {
+        FullHttpRequest fullHttpRequest = channel.attr(AttributeKeyConstant.fullHttpRequest).get();
+        return Objects.nonNull(fullHttpRequest) ? fullHttpRequest.headers() : null;
+    }
+}

+ 24 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/IdleEventMethodArgumentResolver.java

@@ -0,0 +1,24 @@
+package com.yhlxj.netty.websocket.starter.support;
+
+import com.yhlxj.netty.websocket.starter.annotations.OnEvent;
+import com.yhlxj.netty.websocket.starter.netty.AttributeKeyConstant;
+import io.netty.channel.Channel;
+import org.springframework.core.MethodParameter;
+
+import java.util.Objects;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+public class IdleEventMethodArgumentResolver implements MethodArgumentResolver {
+    @Override
+    public boolean supportsParameter(MethodParameter parameter) {
+        return parameter.getMethod().isAnnotationPresent(OnEvent.class) && Objects.equals(parameter.getParameterType(),Object.class);
+    }
+
+    @Override
+    public Object resolveArgument(MethodParameter parameter, Channel channel) {
+        return channel.attr(AttributeKeyConstant.idleStateEvent).get();
+    }
+}

+ 18 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/MethodArgumentResolver.java

@@ -0,0 +1,18 @@
+package com.yhlxj.netty.websocket.starter.support;
+
+import io.netty.channel.Channel;
+import org.springframework.core.MethodParameter;
+import org.springframework.lang.Nullable;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+public interface MethodArgumentResolver {
+
+
+    boolean supportsParameter(MethodParameter parameter);
+
+    @Nullable
+    Object resolveArgument(MethodParameter parameter, Channel channel);
+}

+ 64 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/MethodParamsBuild.java

@@ -0,0 +1,64 @@
+package com.yhlxj.netty.websocket.starter.support;
+
+import io.netty.channel.Channel;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.util.ObjectUtils;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+public class MethodParamsBuild {
+
+    private static List<MethodArgumentResolver> resolvers = new ArrayList<>(10);
+
+    private volatile ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
+
+
+    static {
+        resolvers.add(new SessionMethodArgumentResolver());
+        resolvers.add(new PathParaMethodArgumentResolver());
+        resolvers.add(new TextMethodArgumentResolver());
+        resolvers.add(new IdleEventMethodArgumentResolver());
+        resolvers.add(new HttpHeadersMethodArgumentResolver());
+    }
+    private static final Object[] EMPTY_ARGS = new Object[0];
+
+
+    public Object[] getMethodArgumentValues(Method method, Channel channel) {
+        MethodParameter[] parameters = getMethodParameters(method);
+        if (ObjectUtils.isEmpty(parameters)) {
+            return EMPTY_ARGS;
+        }
+        Object[] args = new Object[parameters.length];
+        for (int i = 0; i < parameters.length; i++) {
+            Object value = null;
+            for (MethodArgumentResolver resolver : resolvers) {
+                if (resolver.supportsParameter(parameters[i])) {
+                    value = resolver.resolveArgument(parameters[i],channel);
+                    break;
+                }
+            }
+            args[i] = value;
+        }
+        return args;
+    }
+
+    private MethodParameter[] getMethodParameters(Method method) {
+        Parameter[] parameters = method.getParameters();
+        MethodParameter[] methodParameters = new MethodParameter[parameters.length];
+        for (int i = 0; i < parameters.length; i++) {
+            MethodParameter parameter = new MethodParameter(method,i);
+            parameter.initParameterNameDiscovery(parameterNameDiscoverer);
+            methodParameters[i] = parameter;
+        }
+        return methodParameters;
+    }
+}

+ 31 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/PathParaMethodArgumentResolver.java

@@ -0,0 +1,31 @@
+package com.yhlxj.netty.websocket.starter.support;
+
+import com.yhlxj.netty.websocket.starter.annotations.PathParam;
+import com.yhlxj.netty.websocket.starter.netty.AttributeKeyConstant;
+import io.netty.channel.Channel;
+import org.springframework.core.MethodParameter;
+import org.springframework.util.StringUtils;
+
+import java.util.Map;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+public class PathParaMethodArgumentResolver implements MethodArgumentResolver {
+    @Override
+    public boolean supportsParameter(MethodParameter parameter) {
+        return parameter.hasParameterAnnotation(PathParam.class);
+    }
+
+    @Override
+    public Object resolveArgument(MethodParameter parameter, Channel channel) {
+        Map<String, String> uriTemplateVariables = channel.attr(AttributeKeyConstant.uriTemplateVariables).get();
+        String name = parameter.getParameterName();
+        PathParam annotation = parameter.getParameterAnnotation(PathParam.class);
+        if (StringUtils.hasLength(annotation.value())) {
+            name = annotation.value();
+        }
+        return uriTemplateVariables.get(name);
+    }
+}

+ 24 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/SessionMethodArgumentResolver.java

@@ -0,0 +1,24 @@
+package com.yhlxj.netty.websocket.starter.support;
+
+import com.yhlxj.netty.websocket.starter.socket.Session;
+import io.netty.channel.Channel;
+import org.springframework.core.MethodParameter;
+
+import java.util.Objects;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+public class SessionMethodArgumentResolver implements MethodArgumentResolver{
+
+    @Override
+    public boolean supportsParameter(MethodParameter parameter) {
+        return Objects.equals(Session.class,parameter.getParameterType());
+    }
+
+    @Override
+    public Object resolveArgument(MethodParameter parameter, Channel channel) {
+        return new Session(channel);
+    }
+}

+ 28 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/TextMethodArgumentResolver.java

@@ -0,0 +1,28 @@
+package com.yhlxj.netty.websocket.starter.support;
+
+import com.yhlxj.netty.websocket.starter.annotations.OnMessage;
+import com.yhlxj.netty.websocket.starter.netty.AttributeKeyConstant;
+import io.netty.channel.Channel;
+import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
+import org.springframework.core.MethodParameter;
+
+import java.util.Objects;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+public class TextMethodArgumentResolver implements MethodArgumentResolver {
+
+    @Override
+    public boolean supportsParameter(MethodParameter parameter) {
+        return parameter.getMethod().isAnnotationPresent(OnMessage.class)
+                && Objects.equals(parameter.getParameterType(),String.class);
+    }
+
+    @Override
+    public Object resolveArgument(MethodParameter parameter, Channel channel) {
+        TextWebSocketFrame text = channel.attr(AttributeKeyConstant.textWebSocketFrame).get();
+        return text.text();
+    }
+}

+ 83 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/WebSocketAnnotationPostProcessor.java

@@ -0,0 +1,83 @@
+package com.yhlxj.netty.websocket.starter.support;
+
+import com.yhlxj.netty.websocket.starter.WebsocketProperties;
+import com.yhlxj.netty.websocket.starter.annotations.WsServerEndpoint;
+import com.yhlxj.netty.websocket.starter.netty.NettyWebsocketServer;
+import com.yhlxj.netty.websocket.starter.netty.WebsocketActionDispatch;
+import lombok.SneakyThrows;
+import org.springframework.aop.support.AopUtils;
+import org.springframework.beans.factory.SmartInitializingSingleton;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
+import org.springframework.core.type.filter.AnnotationTypeFilter;
+import org.springframework.util.ClassUtils;
+
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+public class WebSocketAnnotationPostProcessor implements SmartInitializingSingleton {
+    @Autowired
+    private DefaultListableBeanFactory beanFactory;
+
+    @Autowired
+    private WebsocketProperties websocketProperties;
+
+    @Override
+    public void afterSingletonsInstantiated() {
+        String[] beanNamesForAnnotation = beanFactory.getBeanNamesForAnnotation(SpringBootApplication.class);
+        String applicationStartBean = beanNamesForAnnotation[0];
+        Object bean = beanFactory.getBean(applicationStartBean);
+        String basePackage = ClassUtils.getPackageName(bean.getClass());
+        scanWebsocketServiceBeans(basePackage,beanFactory);
+        registerServerEndpoints();
+    }
+
+    @SneakyThrows
+    private void registerServerEndpoints() {
+        String[] beanNamesForAnnotation = beanFactory.getBeanNamesForAnnotation(WsServerEndpoint.class);
+        WebsocketActionDispatch actionDispatch = new WebsocketActionDispatch();
+        for (String beanName : beanNamesForAnnotation) {
+            Class<?> beanType = beanFactory.getType(beanName);
+            Class<?> targetClass = getTargetClass(beanType);
+
+            WsServerEndpoint wsServerEndpoint = targetClass.getAnnotation(WsServerEndpoint.class);
+            WebsocketServerEndpoint websocketServerEndpoint = new WebsocketServerEndpoint(targetClass
+                    ,beanFactory.getBean(targetClass),wsServerEndpoint.value());
+            actionDispatch.addWebsocketServerEndpoint(websocketServerEndpoint);
+        }
+        NettyWebsocketServer websocketServer = new NettyWebsocketServer(actionDispatch,websocketProperties);
+        // 启动websocket
+        websocketServer.start();
+    }
+
+
+    /**
+     * 扫描WsServerEndpoint的Bean
+     * @param packagesToScan 扫描包路径
+     * @param registry
+     */
+    private void scanWebsocketServiceBeans(String packagesToScan, BeanDefinitionRegistry registry) {
+
+        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(registry);
+        // 扫描 @WsServerEndpoint标注的类
+        scanner.addIncludeFilter(new AnnotationTypeFilter(WsServerEndpoint.class));
+        scanner.scan(packagesToScan);
+    }
+
+    /**
+     * 获取类型的目标类型
+     * @param clazz
+     * @return
+     */
+    public Class<?> getTargetClass(Class<?> clazz) {
+        if (AopUtils.isCglibProxy(clazz)) {
+            return clazz.getSuperclass();
+        }
+        return clazz;
+    }
+}

+ 139 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/java/com/yhlxj/netty/websocket/starter/support/WebsocketServerEndpoint.java

@@ -0,0 +1,139 @@
+package com.yhlxj.netty.websocket.starter.support;
+
+import com.yhlxj.netty.websocket.starter.annotations.*;
+import com.yhlxj.netty.websocket.starter.exception.WebsocketDeploymentException;
+import lombok.Getter;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+@Getter
+public class WebsocketServerEndpoint {
+
+    /**
+     * @WsServerEndpoint配置的路径
+     */
+    private String path;
+
+    /**
+     * 握手前调用的目标方法
+     */
+    private Method onHandShake;
+
+    /**
+     * 连接关闭事件调用的目标方法
+     */
+    private Method onClose;
+
+    /**
+     * 触发心跳事件调用的目标方法
+     */
+    private Method onEvent;
+
+    /**
+     * 连接成功调用的目标方法
+     */
+    private Method onOpen;
+
+    /**
+     * 收到消息调用的目标方法
+     */
+    private Method onMessage;
+
+    /**
+     * 错误事件调用的目标方法
+     */
+    private Method onError;
+
+    /**
+     * path对应@WsServerEndpoint修饰的类
+     */
+    private Object object;
+
+    public WebsocketServerEndpoint(Class<?> pojoClazz,Object o,String path) {
+        this.object = o;
+        this.path = path;
+        AtomicReference<Method> handShake = new AtomicReference<>();
+        AtomicReference<Method> close = new AtomicReference<>();
+        AtomicReference<Method> event = new AtomicReference<>();
+        AtomicReference<Method> open = new AtomicReference<>();
+        AtomicReference<Method> message = new AtomicReference<>();
+        AtomicReference<Method> error = new AtomicReference<>();
+
+        Method[] pojoClazzMethods = null;
+        Class<?> currentClazz = pojoClazz;
+        while (!currentClazz.equals(Object.class)) {
+            Method[] currentClazzMethods = currentClazz.getDeclaredMethods();
+            if (currentClazz == pojoClazz) {
+                pojoClazzMethods = currentClazzMethods;
+            }
+            for (Method method : currentClazzMethods) {
+                if (Objects.nonNull(method.getAnnotation(HandshakeBefore.class))) {
+                    methodFill(currentClazz,method,pojoClazz,handShake, HandshakeBefore.class);
+                } else if (Objects.nonNull(method.getAnnotation(OnClose.class))) {
+                    methodFill(currentClazz,method,pojoClazz,close,OnClose.class);
+                } else if (Objects.nonNull(method.getAnnotation(OnEvent.class))) {
+                    methodFill(currentClazz,method,pojoClazz,event,OnEvent.class);
+                } else if (Objects.nonNull(method.getAnnotation(OnOpen.class))) {
+                    methodFill(currentClazz,method,pojoClazz,open,OnOpen.class);
+                } else if (Objects.nonNull(method.getAnnotation(OnMessage.class))) {
+                    methodFill(currentClazz,method,pojoClazz,message,OnMessage.class);
+                } else if (Objects.nonNull(method.getAnnotation(OnError.class))) {
+                    methodFill(currentClazz,method,pojoClazz,error,OnError.class);
+                }
+            }
+            currentClazz = currentClazz.getSuperclass();
+            this.onHandShake = handShake.get();
+            this.onClose = close.get();
+            this.onEvent = event.get();
+            this.onOpen = open.get();
+            this.onMessage = message.get();
+            this.onError = error.get();
+        }
+    }
+
+
+    private void methodFill(Class<?> currentClazz, Method method, Class<?> pojoClazz, AtomicReference<Method> point, Class annotation) {
+        checkPublic(method);
+        if (Objects.isNull(point.get())) {
+            point.set(method);
+        } else {
+            if (currentClazz == pojoClazz ||
+                    !isMethodOverride(point.get(), method)) {
+                throw new WebsocketDeploymentException(
+                        "wsServerEndpoint.duplicateAnnotation " + annotation.getSimpleName());
+            }
+        }
+    }
+
+    /**
+     * 判断方法是否public
+     * @param m
+     * @throws WebsocketDeploymentException
+     */
+    private void checkPublic(Method m) throws WebsocketDeploymentException {
+        if (!Modifier.isPublic(m.getModifiers())) {
+            throw new WebsocketDeploymentException(
+                    "pojoMethodMapping.methodNotPublic " + m.getName());
+        }
+    }
+
+    /**
+     * 判断方法是否重写方法
+     * @param method1
+     * @param method2
+     * @throws WebsocketDeploymentException
+     */
+    private boolean isMethodOverride(Method method1, Method method2) {
+        return (method1.getName().equals(method2.getName())
+                && method1.getReturnType().equals(method2.getReturnType())
+                && Arrays.equals(method1.getParameterTypes(), method2.getParameterTypes()));
+    }
+}

+ 3 - 0
midjourney-netty-websocket-spring-boot-starter/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,3 @@
+# Auto Configure
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.yhlxj.netty.websocket.starter.NettyWebsocketAutoConfiguration

+ 88 - 0
midjourney-netty-websocket-web/pom.xml

@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.4.3</version>
+        <relativePath/> <!-- lookup parent from repository -->
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>midjourney-netty-websocket-web</artifactId>
+
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.netflix</groupId>
+            <artifactId>midjourney-netty-websocket-spring-boot-starter</artifactId>
+            <version>1.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.netflix</groupId>
+            <artifactId>netflix-common</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>mysql</groupId>
+                    <artifactId>mysql-connector-java</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.bouncycastle</groupId>
+                    <artifactId>bcprov-jdk15on</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.google.zxing</groupId>
+                    <artifactId>core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-test</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.alipay.sdk</groupId>
+                    <artifactId>alipay-sdk-java</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-aop</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.google.guava</groupId>
+                    <artifactId>guava</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.tencentcloudapi</groupId>
+                    <artifactId>tencentcloud-sdk-java-sms</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.tencentcloudapi</groupId>
+                    <artifactId>tencentcloud-sdk-java-faceid</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.tencentcloudapi</groupId>
+                    <artifactId>tencentcloud-sdk-java</artifactId>
+                </exclusion>
+            </exclusions>
+            <version>1.0</version>
+        </dependency>
+
+    </dependencies>
+
+
+</project>

+ 17 - 0
midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/Application.java

@@ -0,0 +1,17 @@
+package com.yhlxj.netty.websocket.midjounrey;
+
+import com.yhlxj.netty.websocket.midjounrey.service.SocketServer;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+@SpringBootApplication
+public class Application {
+    public static void main(String[] args)  {
+        SpringApplication.run(Application.class).getBean(SocketServer.class);
+    }
+}

+ 22 - 0
midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/controller/NotifyController.java

@@ -0,0 +1,22 @@
+package com.yhlxj.netty.websocket.midjounrey.controller;
+
+import com.yhlxj.netty.websocket.midjounrey.dao.model.dto.SendMsgReq;
+import com.yhlxj.netty.websocket.midjounrey.service.SocketServer;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+@RestController("/msg")
+public class NotifyController {
+
+    @RequestMapping("/send")
+    public String sendMsg(@RequestBody SendMsgReq sendMsgReq){
+        SocketServer.sendMessage(sendMsgReq.getMsg(),sendMsgReq.getUserToken());
+        return "success";
+    }
+
+}

+ 40 - 0
midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/MyMetaObjectHandler.java

@@ -0,0 +1,40 @@
+package com.yhlxj.netty.websocket.midjounrey.dao;
+
+import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
+import org.apache.ibatis.reflection.MetaObject;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+
+/**
+ * @author chan
+ * @className MyMetaObjectHandler
+ * @description
+ * @date 2020/7/29 2:44 下午
+ */
+@Component
+public class MyMetaObjectHandler implements MetaObjectHandler {
+    @Override
+    public void insertFill(MetaObject metaObject) {
+        //根据属性名称设置值
+        this.setFieldValByName("createdTime",new Date(),metaObject);
+        this.setFieldValByName("updateTime",new Date(),metaObject);
+        //this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
+        //this.fillStrategy(metaObject, "createTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug请升级到之后的版本如`3.3.1.8-SNAPSHOT`)
+        /* 上面选其一使用,下面的已过时(注意 strictInsertFill 有多个方法,详细查看源码) */
+        //this.setFieldValByName("operator", "Jerry", metaObject);
+        //this.setInsertFieldValByName("operator", "Jerry", metaObject);
+
+    }
+
+    @Override
+    public void updateFill(MetaObject metaObject) {
+
+        this.setFieldValByName("updateTime", new Date(), metaObject);
+        //this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); // 起始版本 3.3.0(推荐使用)
+        //this.fillStrategy(metaObject, "updateTime", LocalDateTime.now()); // 也可以使用(3.3.0 该方法有bug请升级到之后的版本如`3.3.1.8-SNAPSHOT`)
+        /* 上面选其一使用,下面的已过时(注意 strictUpdateFill 有多个方法,详细查看源码) */
+        //this.setFieldValByName("operator", "Tom", metaObject);
+        //this.setUpdateFieldValByName("operator", "Tom", metaObject);
+    }
+}

+ 37 - 0
midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/MybatisPlusConfig.java

@@ -0,0 +1,37 @@
+package com.yhlxj.netty.websocket.midjounrey.dao;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+/**
+ * @author chan
+ * @className MybatisConfig
+ * @date 2020/7/15 3:05 下午
+ */
+
+@EnableTransactionManagement
+@Configuration
+@MapperScan(basePackages = {"com.yhlxj.netty.websocket.midjounrey.dao.mapper"})
+public class MybatisPlusConfig {
+
+    /**
+     * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
+     */
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
+        return interceptor;
+    }
+
+    @Bean
+    public ConfigurationCustomizer configurationCustomizer() {
+        return configuration -> configuration.setUseDeprecatedExecutor(false);
+    }
+}

+ 11 - 0
midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/mapper/MidjourneyUserConversationMapper.java

@@ -0,0 +1,11 @@
+package com.yhlxj.netty.websocket.midjounrey.dao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yhlxj.netty.websocket.midjounrey.dao.model.entity.MidjourneyUserConversation;
+
+/**
+ * @author zwhui
+ * @date 2024/4/23 16:03
+ */
+public interface MidjourneyUserConversationMapper extends BaseMapper<MidjourneyUserConversation> {
+}

+ 11 - 0
midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/mapper/MidjourneyUserMapper.java

@@ -0,0 +1,11 @@
+package com.yhlxj.netty.websocket.midjounrey.dao.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yhlxj.netty.websocket.midjounrey.dao.model.entity.MidjourneyUser;
+
+/**
+ * @author zwhui
+ * @date 2024/4/16 17:46
+ */
+public interface MidjourneyUserMapper extends BaseMapper<MidjourneyUser> {
+}

+ 36 - 0
midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/model/BaseEntity.java

@@ -0,0 +1,36 @@
+package com.yhlxj.netty.websocket.midjounrey.dao.model;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.Date;
+
+/**
+ * @author chan
+ * @className BaseEntity
+ * @description
+ * @date 2020/7/29 2:28 下午
+ */
+@Data
+@Accessors(chain = true)
+public class BaseEntity {
+
+    @TableId(value = "id",type =IdType.AUTO)
+    private Long id;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private Date createdTime;
+
+    /**
+     * 修改时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Date updateTime;
+}

+ 18 - 0
midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/model/dto/SendMsgReq.java

@@ -0,0 +1,18 @@
+package com.yhlxj.netty.websocket.midjounrey.dao.model.dto;
+
+import lombok.Data;
+
+/**
+ * @author chan
+ * @date 2024/9/6 18:34
+ */
+@Data
+public class SendMsgReq {
+
+    private String userToken;
+
+    private String msg;
+
+    private String taskId;
+
+}

+ 57 - 0
midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/model/entity/MidjourneyUser.java

@@ -0,0 +1,57 @@
+package com.yhlxj.netty.websocket.midjounrey.dao.model.entity;
+
+/**
+ * @author zwhui
+ * @date 2024/4/16 17:01
+ */
+
+import com.ejlchina.searcher.bean.SearchBean;
+import com.yhlxj.netty.websocket.midjounrey.dao.model.BaseEntity;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@SearchBean(tables = "midjourney_user")
+public class MidjourneyUser extends BaseEntity {
+
+    /**
+     * 名称
+     */
+    private String name;
+
+    /**
+     * 头像
+     */
+    private String img;
+
+    /**
+     * 用户凭证
+     */
+    private String userToken;
+
+    /**
+     * 过期时间
+     */
+    private Date expireTime;
+
+    /**
+     * 车票id
+     */
+    private Long relationId;
+
+    /**
+     * mj fast次数
+     */
+    private Integer mjFastNum;
+
+    /**
+     * mj relax次数
+     */
+    private Integer mjRelaxNum;
+
+    /**
+     * 拉黑
+     */
+    private Boolean isBlack;
+}

+ 142 - 0
midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/model/entity/MidjourneyUserConversation.java

@@ -0,0 +1,142 @@
+package com.yhlxj.netty.websocket.midjounrey.dao.model.entity;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.ejlchina.searcher.bean.DbIgnore;
+import com.ejlchina.searcher.bean.SearchBean;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.yhlxj.netty.websocket.midjounrey.dao.model.BaseEntity;
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.Serializable;
+
+/**
+ * @author zwhui
+ * @date 2024/4/23 11:10
+ */
+@Data
+@SearchBean(tables = "midjourney_user_conversation")
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class MidjourneyUserConversation extends BaseEntity implements Serializable {
+
+    private Long userId;
+
+    /**
+     * plus服务任务id
+     */
+    private String taskId;
+
+    /**
+     * IMAGINE,UPSCALE,VARIATION,ZOOM,PAN,DESCRIBE,BLEND,SHORTEN
+     */
+    private String action;
+
+    /**
+     * FAST模式服务商 id
+     */
+    private Long apiChannelId;
+
+    /**
+     * 频道id
+     */
+    private Long channelId;
+
+    /**
+     * plus服务实例id
+     */
+    private Long instanceId;
+
+    /**
+     * 模式 1:fast 2:relax
+     */
+    private Integer mode;
+
+    /**
+     *  MID_JOURNEY
+     *  NIJI_JOURNEY
+     */
+    private String BotType;
+
+    /**
+     * 开始时间
+     */
+    private Long startTime;
+
+    /**
+     * 完成时间
+     */
+    private Long finishTime;
+
+    /**
+     * 提示词
+     */
+    private String prompt;
+
+    /**
+     * 描述
+     */
+    private String description;
+
+    /**
+     * 任务状态,可用值:IN_PROGRESS,FAILURE,SUCCESS,CANCEL
+     */
+    private String status;
+
+    /**
+     * 失败原因
+     */
+    private String failReason;
+
+    /**
+     * 进度
+     */
+    private String progress;
+
+    /**
+     * 图片地址
+     */
+    private String imageUrl;
+
+    private String originalImageUrl;
+
+    private String thumbnailImageUrl;
+
+    /**
+     * 按钮
+     */
+    private String buttons;
+
+    /**
+     * 属性
+     */
+    private String properties;
+
+    /**
+     * 书签
+     */
+    private Boolean bookmark;
+
+    /**
+     * 图片是否失效
+     */
+    private Boolean imageInvalid;
+
+    @DbIgnore
+    @TableField(exist = false)
+    private Integer progressNum = -1;
+
+    public MidjourneyUserConversation setProgress(String progress) {
+        this.progress = progress;
+        try {
+            if(StringUtils.isNotBlank(progress)){
+                this.progressNum = Integer.parseInt(this.progress.replace("%",""));
+            }
+            if ("FAILURE".equals(this.getStatus())){
+                this.progressNum = 100;
+            }
+        }catch (Exception e) {
+
+        }
+        return this;
+    }
+}

+ 55 - 0
midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/dao/model/enums/WsMessageTypeEnum.java

@@ -0,0 +1,55 @@
+package com.yhlxj.netty.websocket.midjounrey.dao.model.enums;
+
+import lombok.Getter;
+
+import java.util.stream.Stream;
+
+/**
+ * @author chan
+ * @date 2024/6/27 18:43
+ */
+@Getter
+public enum WsMessageTypeEnum {
+
+
+    PING("ping"), PONG("pong"), MESSAGE("msg"),
+
+    //客服端反馈子类型
+    CLIENT_TASK_ID("task_id"),//开始连接
+    CLIENT_CLOSE("close"),//关闭连接
+    CLIENT_ACK("ack"),//收到
+    CLIENT_RESURGENCE("resurgence"),//客服端复活
+
+    //服务端发送子类型
+    SERVER_TASK_INFO("task_info"),//服务端发送问题
+    SERVER_TASK_NOT_FIND("task_not_find"),
+    SERVER_ERROR("error"),//错误信息
+    SERVER_FAIL("fail"),//错误信息
+    ;
+
+    public final String type;
+
+    WsMessageTypeEnum(String type) {
+        this.type = type;
+    }
+
+    /**
+     * @author chan 验证枚举
+     * @return boolean
+     **/
+    public static boolean valid(String type) {
+        return Stream.of(WsMessageTypeEnum.values()).anyMatch(val -> val.getType().equals(type));
+    }
+
+    /**
+     *
+     * @author chan
+     * @param type
+     * @return
+     */
+    @SuppressWarnings("rawtypes")
+    public static WsMessageTypeEnum getMessageType(String type){
+        return Stream.of(WsMessageTypeEnum.values())
+                .filter(enums-> enums.getType().equals(type)).findFirst().get();
+    }
+}

+ 778 - 0
midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/redis/RedisService.java

@@ -0,0 +1,778 @@
+package com.yhlxj.netty.websocket.midjounrey.redis;
+
+import com.cyksj.common.util.StringUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ZSetOperations;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.Resource;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * @description 定义常用的 Redis操作
+ * @author chan
+ * @date 2021-03-31 5:34 下午
+ */
+@Service
+@SuppressWarnings("all")
+public class RedisService {
+
+    @Resource
+    private RedisTemplate<String, Object> redisTemplate;
+
+    public static String env = "dev";
+
+    @Value("${spring.profiles.active}")
+    private String active;
+
+    @PostConstruct
+    public void init(){
+        env = active + ":";
+    }
+
+
+    /**
+     * 指定缓存失效时间
+     *
+     * @param key  键
+     * @param time 时间(秒)
+     * @return Boolean
+     */
+    public Boolean expire(String key, Long time) {
+        try {
+            if (time > 0) {
+                redisTemplate.expire(key, time, TimeUnit.SECONDS);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 根据key获取过期时间
+     *
+     * @param key 键 不能为 null
+     * @return 时间(秒) 返回 0代表为永久有效
+     */
+    public Long getExpire(String key) {
+        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
+    }
+
+    /**
+     * 判断 key是否存在
+     *
+     * @param key 键
+     * @return true 存在 false不存在
+     */
+    public Boolean hasKey(String key) {
+        try {
+            return redisTemplate.hasKey(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 删除缓存
+     *
+     * @param key 可以传一个值 或多个
+     */
+    public void del(String... key) {
+        if (key != null && key.length > 0) {
+            if (key.length == 1) {
+                redisTemplate.delete(key[0]);
+            } else {
+                redisTemplate.delete(Arrays.asList(key));
+            }
+        }
+    }
+
+    /**
+     * 普通缓存获取
+     *
+     * @param key 键
+     * @return 值
+     */
+    public Object get(String key) {
+        return key == null ? null : redisTemplate.opsForValue().get(key);
+    }
+
+    /**
+     * String缓存获取
+     * @param key 键
+     * @return 值
+     */
+    public String getStr(String key) {
+            return key == null ? null : StringUtil.getString(redisTemplate.opsForValue().get(key));
+    }
+
+    /**
+     * 普通缓存放入
+     *
+     * @param key   键
+     * @param value 值
+     * @return true成功 false失败
+     */
+    public Boolean set(String key, Object value) {
+        try {
+            redisTemplate.opsForValue().set(key, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 普通缓存放入并设置时间
+     *
+     * @param key   键
+     * @param value 值
+     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
+     * @return true成功 false 失败
+     */
+    public Boolean set(String key, Object value, Long time) {
+        try {
+            if (time > 0) {
+                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
+            } else {
+                set(key, value);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 递增
+     *
+     * @param key   键
+     * @param delta 要增加几(大于0)
+     * @return Long
+     */
+    public Long incr(String key, Long delta) {
+        if (delta < 0) {
+            throw new RuntimeException("递增因子必须大于0");
+        }
+        return redisTemplate.opsForValue().increment(key, delta);
+    }
+
+    /**
+     * 递减
+     *
+     * @param key   键
+     * @param delta 要减少几
+     * @return Long
+     */
+    public Long decr(String key, Long delta) {
+        if (delta < 0) {
+            throw new RuntimeException("递减因子必须大于0");
+        }
+        return redisTemplate.opsForValue().increment(key, -delta);
+    }
+
+    /**
+     * HashGet
+     *
+     * @param key  键 不能为 null
+     * @param item 项 不能为 null
+     * @return 值
+     */
+    public Object hget(String key, String item) {
+        return redisTemplate.opsForHash().get(key, item);
+    }
+
+    /**
+     * 获取 hashKey对应的所有键值
+     *
+     * @param key 键
+     * @return 对应的多个键值
+     */
+    public Map<Object, Object> hmget(String key) {
+        return redisTemplate.opsForHash().entries(key);
+    }
+
+    /**
+     * HashSet
+     *
+     * @param key 键
+     * @param map 对应多个键值
+     * @return true 成功 false 失败
+     */
+    public Boolean hmset(String key, Map<Object, Object> map) {
+        try {
+            redisTemplate.opsForHash().putAll(key, map);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * HashSet 并设置时间
+     *
+     * @param key  键
+     * @param map  对应多个键值
+     * @param time 时间(秒)
+     * @return true成功 false失败
+     */
+    public Boolean hmset(String key, Map<Object, Object> map, Long time) {
+        try {
+            redisTemplate.opsForHash().putAll(key, map);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 向一张hash表中放入数据,如果不存在将创建
+     *
+     * @param key   键
+     * @param item  项
+     * @param value 值
+     * @return true 成功 false失败
+     */
+    public Boolean hset(String key, String item, Object value) {
+        try {
+            redisTemplate.opsForHash().put(key, item, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 向一张hash表中放入数据,如果不存在将创建
+     *
+     * @param key   键
+     * @param item  项
+     * @param value 值
+     * @param time  时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
+     * @return true 成功 false失败
+     */
+    public Boolean hset(String key, String item, Object value, Long time) {
+        try {
+            redisTemplate.opsForHash().put(key, item, value);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 删除hash表中的值
+     *
+     * @param key  键 不能为 null
+     * @param item 项 可以使多个不能为 null
+     */
+    public void hdel(String key, Object... item) {
+        redisTemplate.opsForHash().delete(key, item);
+    }
+
+    /**
+     * 判断hash表中是否有该项的值
+     *
+     * @param key  键 不能为 null
+     * @param item 项 不能为 null
+     * @return true 存在 false不存在
+     */
+    public Boolean hHasKey(String key, String item) {
+        return redisTemplate.opsForHash().hasKey(key, item);
+    }
+
+    /**
+     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
+     *
+     * @param key  键
+     * @param item 项
+     * @param by   要增加几(大于0)
+     * @return Double
+     */
+    public Double hincr(String key, String item, Double by) {
+        return redisTemplate.opsForHash().increment(key, item, by);
+    }
+
+    /**
+     * hash递减
+     *
+     * @param key  键
+     * @param item 项
+     * @param by   要减少记(小于0)
+     * @return Double
+     */
+    public Double hdecr(String key, String item, Double by) {
+        return redisTemplate.opsForHash().increment(key, item, -by);
+    }
+
+    /**
+     * 根据 key获取 Set中的所有值
+     *
+     * @param key 键
+     * @return Set
+     */
+    public Set<Object> sGet(String key) {
+        try {
+            return redisTemplate.opsForSet().members(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 根据value从一个set中查询,是否存在
+     *
+     * @param key   键
+     * @param value 值
+     * @return true 存在 false不存在
+     */
+    public Boolean sHasKey(String key, Object value) {
+        try {
+            return redisTemplate.opsForSet().isMember(key, value);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将数据放入set缓存
+     *
+     * @param key    键
+     * @param values 值 可以是多个
+     * @return 成功个数
+     */
+    public Long sSet(String key, Object... values) {
+        try {
+            return redisTemplate.opsForSet().add(key, values);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+    /**
+     * 将set数据放入缓存
+     *
+     * @param key    键
+     * @param time   时间(秒)
+     * @param values 值 可以是多个
+     * @return 成功个数
+     */
+    public Long sSetAndTime(String key, Long time, Object... values) {
+        try {
+            Long count = redisTemplate.opsForSet().add(key, values);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return count;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+    /**
+     * 获取set缓存的长度
+     *
+     * @param key 键
+     * @return Long
+     */
+    public Long sGetSetSize(String key) {
+        try {
+            return redisTemplate.opsForSet().size(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+    /**
+     * 移除值为value的
+     *
+     * @param key    键
+     * @param values 值 可以是多个
+     * @return 移除的个数
+     */
+    public Long setRemove(String key, Object... values) {
+        try {
+            return redisTemplate.opsForSet().remove(key, values);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+    /**
+     * 获取list缓存的内容
+     *
+     * @param key   键
+     * @param start 开始
+     * @param end   结束 0 到 -1代表所有值
+     * @return List
+     */
+    public List<Object> lGet(String key, Long start, Long end) {
+        try {
+            return redisTemplate.opsForList().range(key, start, end);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 获取list缓存的长度
+     *
+     * @param key 键
+     * @return Long
+     */
+    public Long lGetListSize(String key) {
+        try {
+            return redisTemplate.opsForList().size(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+    /**
+     * 通过索引 获取list中的值
+     *
+     * @param key   键
+     * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;
+     *              index<0时,-1,表尾,-2倒数第二个元素,依次类推
+     * @return Object
+     */
+    public Object lGetIndex(String key, Long index) {
+        try {
+            return redisTemplate.opsForList().index(key, index);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @return Boolean
+     */
+    public Boolean lSet(String key, Object value) {
+        try {
+            redisTemplate.opsForList().rightPush(key, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @param time  时间(秒)
+     * @return Boolean
+     */
+    public Boolean lSet(String key, Object value, Long time) {
+        try {
+            redisTemplate.opsForList().rightPush(key, value);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @return Boolean
+     */
+    public Boolean lSet(String key, List<Object> value) {
+        try {
+            redisTemplate.opsForList().rightPushAll(key, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 将list放入缓存
+     *
+     * @param key   键
+     * @param value 值
+     * @param time  时间(秒)
+     * @return Boolean
+     */
+    public Boolean lSet(String key, List<Object> value, Long time) {
+        try {
+            redisTemplate.opsForList().rightPushAll(key, value);
+            if (time > 0) {
+                expire(key, time);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 根据索引修改list中的某条数据
+     *
+     * @param key   键
+     * @param index 索引
+     * @param value 值
+     * @return Boolean
+     */
+    public Boolean lUpdateIndex(String key, Long index, Object value) {
+        try {
+            redisTemplate.opsForList().set(key, index, value);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 移除N个值为value
+     *
+     * @param key   键
+     * @param count 移除多少个
+     * @param value 值
+     * @return 移除的个数
+     */
+    public Long lRemove(String key, Long count, Object value) {
+        try {
+            return redisTemplate.opsForList().remove(key, count, value);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+    public boolean setNx(String key, Object value, Long time) {
+        if (time > 0) {
+            return redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
+        }
+        return false;
+    }
+
+
+    /**
+     * 添加元素到ZSet
+     */
+    public Boolean zAdd(String key, double score, Object value) {
+        try {
+            return redisTemplate.opsForZSet().add(key, value, score);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    // 增加ZSet元素的分数
+    public Double zIncrementScore(String key, String value, double delta) {
+        try {
+            return redisTemplate.opsForZSet().incrementScore(key, value, delta);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+
+    // 封装获取ZSet分数操作
+    public Double zScore(String key, Object value) {
+        try {
+            return redisTemplate.opsForZSet().score(key, value);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    // 封装ZSet添加或更新分数操作
+    public Boolean zAddOrUpdateScore(String key, Object value, double score) {
+        try {
+            return redisTemplate.opsForZSet().add(key, value, score);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * 获取ZSet的大小
+     */
+    public Long zCard(String key) {
+        try {
+            return redisTemplate.opsForZSet().zCard(key);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+    /**
+     * 获取ZSet的大小
+     */
+    public Long zCount(String key, long start, long end) {
+        try {
+            return redisTemplate.opsForZSet().count(key, start, end);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    // 封装获取ZSet范围内成员的方法
+    public Set<Object> zRange(String key, long start, long end) {
+        try {
+            ZSetOperations<String, Object> zSetOperations = redisTemplate.opsForZSet();
+            return zSetOperations.range(key, start, end);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+    /**
+     * 删除ZSet中分数范围内的元素
+     */
+    public Long zRemoveRangeByScore(String key, double min, double max) {
+        try {
+            return redisTemplate.opsForZSet().removeRangeByScore(key, min, max);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+    /**
+     * 返回有序集 key 中,所有分数介于 min 和 max 之间(包括等于 min 或 max)的成员。
+     * 有序集成员按分数值递增(从小到大)次序排列。
+     * 此方法还支持限制返回的元素数量。
+     *
+     * @param key 有序集合的key
+     * @param min 分数范围的最小值
+     * @param max 分数范围的最大值
+     * @param offset 开始返回的索引位置
+     * @param count 返回的最大元素数量
+     * @return 介于最小值和最大值之间的成员的集合
+     */
+    public Set<Object> zRangeByScore(String key, double min, double max, long offset, long count) {
+        return redisTemplate.opsForZSet().rangeByScore(key, min, max, offset, count);
+    }
+
+    /**
+     * 从ZSet获取所有成员及其分数
+     * @param key ZSet的键
+     * @return ZSet中所有成员及其分数的集合
+     */
+    public Set<ZSetOperations.TypedTuple<Object>> zRangeWithScores(String key, long start, long end){
+        return redisTemplate.opsForZSet().rangeWithScores(key, start, end);
+    }
+
+    /**
+     * 检查指定值是否存在于有序集合中
+     * @param key
+     * @param value
+     * @return
+     */
+    public boolean checkValueExistsInZSet(String key, String value) {
+        // 检查指定值是否存在于有序集合中
+        return redisTemplate.opsForZSet().score(key, value) != null;
+    }
+
+    /**
+     * 从ZSet中删除指定的成员
+     *
+     * @param key ZSet的键
+     * @param value 要删除的成员值
+     * @return 被成功移除的成员数量
+     */
+    public Long zRemove(String key, Object value) {
+        try {
+            return redisTemplate.opsForZSet().remove(key, value);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return 0L;
+        }
+    }
+
+
+
+
+    public Set<String> keys(String name) {
+        return redisTemplate.keys(name);
+    }
+
+    @AllArgsConstructor
+    @Getter
+    public enum key {
+        MIDJOURNEY_FAST_LIMIT("midjourney:fast:limit:", "midjourney fast次数", 60 * 60 * 48L),
+        MIDJOURNEY_RELAX_LIMIT("midjourney:relax:limit:", "midjourney relax次数", 60 * 60 * 48L),
+        MIDJOURNEY_EXPIRE_TIME("midjourney:expire:time:", "midjourney expire time", 60 * 60 * 48L),
+        MIDJOURNEY_USER("midjourney:user:", "midjourney user", 60 * 60 * 2L),
+        MIDJOURNEY_CONVERSATION("midjourney:conversation:", "midjourney conversation", 60 * 60 * 5L),
+        MIDJOURNEY_ACCOUNT("midjourney:account:", "midjourney account", 60 * 60 * 1L),
+        MIDJOURNEY_FAST_ACCOUNT("midjourney:fast:account:", "midjourney fast account", 60 * 60 * 48L),
+        MIDJOURNEY_QUERY("midjourney:query:", "midjourney query", 60 * 60 * 2L),
+        MIDJOURNEY_PROGRESS("midjourney:task:progress:","task progress 任务进度", 60 * 60 * 1L),
+        MIDJOURNEY_USER_TIME("midjourney:user:time:", "midjourney user time", 60 * 60 * 24L),
+        MIDJOURNEY_USER_SUBMIT("midjourney:user:submit:", "midjourney user submit", 30L),
+        MIDJOURNEY_CAR_TASK("midjourney:car:task","midjourney car 任务数", 48 * 60 * 60L),
+        MIDJOURNEY_USER_AGAIN("midjourney:user:again","midjourney用户重试次数", 30L),
+        ;
+
+        private String name;
+
+        private String desc;
+
+        private long timeout;
+
+        public String getEnvName() {
+            return this.name + env;
+        }
+        public String getNameFormat(Object ... str) {
+            return String.format(this.getEnvName(), str);
+        }
+
+    }
+}

+ 62 - 0
midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/service/MidjourneyService.java

@@ -0,0 +1,62 @@
+package com.yhlxj.netty.websocket.midjounrey.service;
+
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.cyksj.common.util.QueryWrapperUtils;
+import com.yhlxj.netty.websocket.midjounrey.dao.mapper.MidjourneyUserConversationMapper;
+import com.yhlxj.netty.websocket.midjounrey.dao.mapper.MidjourneyUserMapper;
+import com.yhlxj.netty.websocket.midjounrey.dao.model.entity.MidjourneyUser;
+import com.yhlxj.netty.websocket.midjounrey.dao.model.entity.MidjourneyUserConversation;
+import com.yhlxj.netty.websocket.midjounrey.redis.RedisService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author zwhui
+ * @date 2024/4/23 15:44
+ */
+@Service
+@Slf4j
+@RequiredArgsConstructor
+public class MidjourneyService{
+
+    private final RedisService redisService;
+
+    private final MidjourneyUserMapper midjourneyUserMapper;
+
+    private final MidjourneyUserConversationMapper conversationMapper;
+
+    public MidjourneyUser getUser(String userToken) {
+        MidjourneyUser midjourneyUser = midjourneyUserMapper.selectOne(Wrappers.lambdaQuery(MidjourneyUser.class)
+                .eq(MidjourneyUser::getUserToken, userToken).last(" limit 1"));
+        return midjourneyUser;
+    }
+
+
+
+   
+    public MidjourneyUserConversation getConversationById(String id){
+
+        //从缓存中查询 该任务是否存在
+        MidjourneyUserConversation conversation;
+        Object obj = redisService.get(RedisService.key.MIDJOURNEY_CONVERSATION.getName() + id);
+        if(obj == null){
+            conversation = conversationMapper.selectOne(QueryWrapperUtils.buildWrapper((wrapper)->{
+                wrapper.eq(MidjourneyUserConversation::getTaskId, id);
+            }));
+            redisService.set(RedisService.key.MIDJOURNEY_CONVERSATION.getName() + id, conversation, RedisService.key.MIDJOURNEY_CONVERSATION.getTimeout());
+        }else {
+            try {
+                conversation = (MidjourneyUserConversation)obj;
+            }catch (Exception e){
+                String jsonStr = JSONUtil.toJsonStr(obj);
+                conversation = JSONUtil.parseObj(jsonStr).toBean(MidjourneyUserConversation.class);
+                redisService.set(RedisService.key.MIDJOURNEY_CONVERSATION.getName() + id, conversation, RedisService.key.MIDJOURNEY_CONVERSATION.getTimeout());
+            }
+        }
+
+        return conversation;
+    }
+
+}

+ 222 - 0
midjourney-netty-websocket-web/src/main/java/com/yhlxj/netty/websocket/midjounrey/service/SocketServer.java

@@ -0,0 +1,222 @@
+package com.yhlxj.netty.websocket.midjounrey.service;
+
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.cyksj.common.util.SpringCtxUtils;
+import com.yhlxj.netty.websocket.midjounrey.dao.model.entity.MidjourneyUser;
+import com.yhlxj.netty.websocket.midjounrey.dao.model.entity.MidjourneyUserConversation;
+import com.yhlxj.netty.websocket.midjounrey.dao.model.enums.WsMessageTypeEnum;
+import com.yhlxj.netty.websocket.starter.annotations.*;
+import com.yhlxj.netty.websocket.starter.socket.Session;
+import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.timeout.IdleStateEvent;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * @author chan
+ * @date 2024-09-06 22:39
+ */
+@WsServerEndpoint(value = "/ws/{userToken}/{taskId}")
+@Slf4j
+public class SocketServer {
+
+	private static ConcurrentMap<String, Set<Session>> sessionPool = new ConcurrentHashMap<>();
+	private static ConcurrentMap<String,String> sessionIds = new ConcurrentHashMap<>();
+
+	private static final String ADMIN = "yhlxj";
+
+	//@Autowired
+	private MidjourneyService midjourneyService;
+
+	@HandshakeBefore
+	public void handshakeBefore(HttpHeaders headers,@PathParam String userid) {
+		log.info("handshakeBefore userId: {}  host: {}", userid, headers.get("HOST"));
+	}
+
+	/**
+	 * 用户连接时触发
+	 */
+	@OnOpen
+	public void open(Session session,@PathParam(value="userToken") String userToken, @PathParam(value = "taskId") String taskId){
+		log.info("[WS建立连接],token:{}, taskId:{}", userToken, taskId);
+		//检查uniodId是否合法
+		if (StringUtils.isBlank(userToken) || StringUtils.isBlank(taskId)) {
+
+			log.error("[WS建立连接失败],[参数非法],{},{}", userToken, taskId);
+
+			session.sendText("入参错误");
+			session.close();
+		}
+		MidjourneyUser user = getUser(userToken);
+		if (user == null) {
+			log.error("[WS建立连接失败],[用户过期],{},{}", userToken, taskId);
+			session.sendText("用户过期");
+			session.close();
+		} else {
+			if (user.getIsBlack()) {
+				log.error("[WS建立连接失败],[用户拉黑],{},{}", userToken, taskId);
+				session.sendText("状态异常");
+				session.close();
+			}
+			MidjourneyUserConversation conversation = midjourneyService.getConversationById(taskId);
+
+			if (conversation == null) {
+				session.sendText(packMsg(WsMessageTypeEnum.SERVER_TASK_NOT_FIND, "任务不存在."));
+			} else {
+				session.sendText(packMsg(WsMessageTypeEnum.SERVER_TASK_INFO, new JSONObject(conversation)));
+			}
+		}
+		Set<Session> sessions = sessionPool.computeIfAbsent(userToken, k -> ConcurrentHashMap.newKeySet());
+		sessions.add(session);
+		sessionPool.put(userToken, sessions);
+		sessionIds.put(session.getId(), userToken);
+	}
+
+	/**
+	 * 收到信息时触发
+	 * @param message
+	 */
+	@OnMessage
+	public void onMessage(Session session, String message, @PathParam(value = "userToken") String userToken, @PathParam(value = "taskId") String taskId){
+		try {
+			log.info("[WS收到消息],{},{},{}", userToken, taskId, message);
+
+			JSONObject body = null;//消息具体内容
+
+			//处理消息类型,是轮询还是消息
+			switch (WsMessageTypeEnum.getMessageType(message.split(":")[0])) {
+				//ping pong
+				case PING:
+					session.sendText("pong");
+					return;
+				case MESSAGE:
+					body = JSONUtil.parseObj(message.substring(8));
+					break;
+				default:
+					log.error("[WS消息类型异常],{},{},{}", userToken, taskId, message);
+					return;
+			}
+
+			//根据子类型进行处理
+			if (WsMessageTypeEnum.getMessageType(body.getStr("type")) == WsMessageTypeEnum.CLIENT_TASK_ID) { //
+				MidjourneyService midjourneyService = SpringCtxUtils.getBean(MidjourneyService.class);
+				MidjourneyUserConversation conversation = midjourneyService.getConversationById(body.getStr("taskId"));
+				session.sendText(packMsg(WsMessageTypeEnum.SERVER_TASK_INFO, new JSONObject(conversation)));
+			} else {
+				log.error("[WS消息子类型异常],{},{},{}", userToken, taskId, message);
+			}
+
+		} catch (Exception e) {
+			log.error("[WS接收消息失败],{},{},{},{}", userToken, taskId, message, e);
+		}
+	}
+
+	/**
+	 * 连接关闭触发
+	 */
+	@OnClose
+	public void onClose(Session session,@PathParam String userToken){
+		Set<Session> sessions = sessionPool.get(userToken);
+		if (CollectionUtil.isNotEmpty(sessions)) {
+			sessions.removeIf(session1 -> session1.getId().equals(session.getId()));
+			sessionIds.remove(session.getId());
+		}
+		log.info("client【{}】【{}】断开连接",userToken, session.getId());
+	}
+
+	/**
+	 * 发生错误时触发
+	 * @param session
+	 * @param error
+	 */
+	@OnError
+	public void onError(Session session, Throwable error) {
+		error.printStackTrace();
+	}
+
+	/**
+	 * 发生事件时触发
+	 * @param session
+	 * @param evt
+	 */
+	@OnEvent
+	public void onEvent(Session session,@PathParam String userid, Object evt) {
+		if (evt instanceof IdleStateEvent) {
+			IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
+			switch (idleStateEvent.state()) {
+				case READER_IDLE:
+					log.info("clent : {} heartbeat read timeout event",userid);
+					break;
+				case WRITER_IDLE:
+					log.info("clent : {} heartbeat write timeout event",userid);
+					session.close();
+					break;
+				case ALL_IDLE:
+					log.info("clent : {} heartbeat all timeout event",userid);
+					break;
+				default:
+					break;
+			}
+		}
+	}
+
+	/**
+	 *信息发送的方法
+	 */
+	public static void sendMessage(WsMessageTypeEnum typeEnum,String message,String userToken){
+		sendMessage(packMsg(typeEnum, message), userToken);
+	}
+
+	public static void sendMessage(String message,String userToken){
+		Set<Session> sessions = sessionPool.get(userToken);
+		if (CollectionUtil.isNotEmpty(sessions)) {
+			sessions.forEach((session)->{
+				session.sendText(message);
+			});
+		}
+	}
+
+	/**
+	 * 获取当前连接数
+	 * @return
+	 */
+	public static int getOnlineNum(){
+		if(sessionIds.values().contains(ADMIN)) {
+
+			return sessionPool.size()-1;
+		}
+		return sessionPool.size();
+	}
+
+	/**
+	 * 获取在线用户名以逗号隔开
+	 * @return
+	 */
+	public static String getOnlineUsers(){
+		StringBuffer users = new StringBuffer();
+		for (String key : sessionIds.keySet()) {//ADMIN是服务端自己的连接,不能算在线人数
+			if (!ADMIN.equals(sessionIds.get(key))) {
+				users.append(sessionIds.get(key)+",");
+			}
+		}
+		return users.toString();
+	}
+
+
+	public MidjourneyUser getUser(String userToken) {
+		return midjourneyService.getUser(userToken);
+	}
+
+	public static String packMsg(WsMessageTypeEnum type, Object obj){
+		JSONObject result = new JSONObject();
+		result.putOpt("type", type.type);
+		result.putOpt("content", JSONUtil.toJsonStr(obj));
+		return WsMessageTypeEnum.MESSAGE.type + ":" + result.toString();
+	}
+}

+ 96 - 0
midjourney-netty-websocket-web/src/main/resources/application.yml

@@ -0,0 +1,96 @@
+server:
+  port: 8080
+
+netty:
+  websocket:
+    port: 8086
+    bossThreadNums: 1
+    workerThreadNums: 2
+    connectTimeout: 15000
+    backLog: 123
+    tcpNoDelay: true
+    readerIdleTimeSeconds: 30
+    writerIdleTimeSeconds: 30
+    allIdleTimeSeconds: 0
+    maxContentLength: 65536
+
+spring:
+  profiles:
+    active:
+      - ${activeProfile}
+  datasource:
+    type: com.zaxxer.hikari.HikariDataSource
+    url: jdbc:mysql://inside.liuliangbang.vip:33306/yhlxj?useAffectedRows=true
+    username: dev
+    password: Dev.12345
+    platform: mysql
+    continue-on-error: false
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    hikari:
+      pool-name: hikariCP-mysql-
+      maximum-pool-size: 10
+      minimum-idle: 6
+      idle-timeout: 30000
+      max-lifetime: 500000
+      read-only: false
+      connection-timeout: 30000
+      # 以下配置推荐mysql 5.7.27+ 或 8.0.18+
+      # https://cdn.oreillystatic.com/en/assets/1/event/21/Connector_J%20Performance%20Gems%20Presentation.pdf
+      data-source-properties: {
+        useSSL: false,
+        useAffectedRows: true,
+        useUnicode: true,
+        characterEncoding: utf8,
+        connectTimeout: 3000,
+        socketTimeout: 2000,
+
+        # 服务端缓存PrepStmts
+        useServerPrepStmts: true,
+        cachePrepStmts: true,
+        # 官方推荐250-500
+        prepStmtCacheSize: 512,
+        # 官方推荐2048
+        prepStmtCacheSqlLimit: 2048,
+
+        cacheServerConfiguration: true,
+        useLocalSessionState: true,
+        rewriteBatchedStatements: true,
+        useLocalTransactionState: true,
+        cacheResultSetMetadata: true,
+        elideSetAutoCommits: true,
+        maintainTimeStats: false,
+      }
+  redis:
+    # Redis数据库索引(默认为 0)
+    database: 1
+    # Redis服务器地址
+    host: r-bp11e3hvovqtzl0a27pd.redis.rds.aliyuncs.com
+    # Redis服务器连接端口
+    port: 6379
+    # Redis 密码
+    password: Shanxing12345
+    lettuce:
+      pool:
+        # 连接池中的最小空闲连接
+        min-idle: 8
+        # 连接池中的最大空闲连接
+        max-idle: 500
+        # 连接池最大连接数(使用负值表示没有限制)
+        max-active: 2000
+        # 连接池最大阻塞等待时间(使用负值表示没有限制)
+        max-wait: 10000
+    # 连接超时时间(毫秒)
+    timeout: 5000
+
+logging:
+  config: classpath:config/logback-debug.xml
+
+#mybatis
+mybatis:
+  mapper-locations: classpath:mapper/*Mapper.xml
+  executor-type: simple
+  type-aliases-package: com.cyksj.dao.model
+  configuration:
+    local-cache-scope: statement
+    cache-enabled: false
+

+ 111 - 0
midjourney-netty-websocket-web/src/main/resources/config/logback-debug.xml

@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+	<include resource="org/springframework/boot/logging/logback/defaults.xml" />
+	<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
+
+	<contextName>midjourney-server</contextName>
+
+	<property name="log_home" value="./logs"/>
+
+	<!-- 控制台输出 -->
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<!-- 日志输出的文件的格式 -->
+		<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} | [%magenta(%thread) %green(%5L)] %highlight(%-5level) %cyan(%logger{50}) : %msg %n</pattern>
+			<charset>UTF-8</charset>
+		</encoder>
+
+	</appender>
+	<!-- 按照每天生成日志文件 -->
+	<appender name="ROOT-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender" level="INFO">
+		<append>true</append>
+
+        <!-- 过滤器,只记录 warn 和 info 级别的日志 -->
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>WARN</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>NEUTRAL</onMismatch>
+        </filter>
+
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <level>INFO</level>
+            <onMatch>ACCEPT</onMatch>
+            <onMismatch>DENY</onMismatch>
+        </filter>
+
+	    <file>${log_home}/midjourney-default.log</file>
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<FileNamePattern>${log_home}/midjourney-default.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
+			<!-- kb/mb/gb -->
+			<maxFileSize>128MB</maxFileSize>
+			<!-- 日志文件保留天数 -->
+			<maxHistory>14</maxHistory>
+		</rollingPolicy>
+		<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} | [%magenta(%thread) %green(%5L)] %highlight(%-5level) %cyan(%logger{50}) : %msg %n</pattern>
+			<charset>UTF-8</charset>
+		</encoder>
+
+	</appender>
+
+	<!-- 按照每天生成日志文件 -->
+	<appender name="ERROR-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<append>true</append>
+
+		<!-- 过滤器,只记录 error 级别的日志 -->
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+			<level>ERROR</level>
+			<onMatch>ACCEPT</onMatch>
+			<onMismatch>DENY</onMismatch>
+		</filter>
+
+		<!-- 日志名称 -->
+		<file>${log_home}/midjourney-error.log</file>
+		<!-- 每天生成一个日志文件,保存15天的日志文件 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<!--日志文件输出的文件名:按天回滚 daily -->
+			<FileNamePattern>${log_home}/midjourney-error.log.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
+			<!-- kb/mb/gb -->
+			<maxFileSize>128MB</maxFileSize>
+			<!-- 日志文件保留天数 -->
+			<maxHistory>14</maxHistory>
+			<totalSizeCap>10GB</totalSizeCap>
+		</rollingPolicy>
+		<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+			<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
+			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} | [%magenta(%thread) %green(%5L)] %highlight(%-5level) %cyan(%logger{50}) : %msg %n</pattern>
+			<!-- 编码 -->
+			<charset>UTF-8</charset>
+		</encoder>
+	</appender>
+
+	<!-- 异步输出 -->
+	<appender name="ERROR_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
+		<!-- 不丢失日志.默认的. 如果discardingThreshold大于0,队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+		<discardingThreshold>0</discardingThreshold>
+		<queueSize>10240</queueSize>
+		<appender-ref ref="ERROR-APPENDER" />
+	</appender>
+
+    <!-- 异步输出 -->
+	<appender name="ROOT_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
+		<!-- 不丢失日志.默认的. 如果discardingThreshold大于0,队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+		<discardingThreshold>0</discardingThreshold>
+		<queueSize>10240</queueSize>
+		<appender-ref ref="ROOT-APPENDER" />
+	</appender>
+
+    <logger name="jdbc.audit" level="WARN" />
+    <logger name="jdbc.resultset" level="WARN" />
+    <logger name="jdbc.sqlonly" level="WARN" />
+    <logger name="jdbc.sqltiming" level="INFO" />
+    <logger name="jdbc.connection" level="WARN" />
+
+	<root level="INFO">
+		<appender-ref ref="STDOUT" />
+		<appender-ref ref="ERROR_ASYNC" />
+		<appender-ref ref="ROOT_ASYNC" />
+	</root>
+	
+	<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>
+</configuration>

+ 105 - 0
midjourney-netty-websocket-web/src/main/resources/config/logback.xml

@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+	<include resource="org/springframework/boot/logging/logback/defaults.xml" />
+	<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
+
+	<contextName>midjourney-server</contextName>
+
+	<property name="log_home" value="./logs"/>
+
+	<!-- 控制台输出 -->
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+		<!-- 日志输出的文件的格式 -->
+		<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} | [%magenta(%thread) %green(%5L)] %highlight(%-5level) %cyan(%logger{50}) : %msg %n</pattern>
+			<charset>UTF-8</charset>
+		</encoder>
+
+	</appender>
+	<!-- 按照每天生成日志文件 -->
+	<appender name="ROOT-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender" level="INFO">
+		<append>true</append>
+
+		<!-- 过滤器,只记录 warn 和 info 级别的日志 -->
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+			<level>WARN</level>
+			<onMatch>ACCEPT</onMatch>
+			<onMismatch>NEUTRAL</onMismatch>
+		</filter>
+
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+			<level>INFO</level>
+			<onMatch>ACCEPT</onMatch>
+			<onMismatch>DENY</onMismatch>
+		</filter>
+
+		<file>${log_home}/midjourney-default.log</file>
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<FileNamePattern>${log_home}/midjourney-netty-websocket-default.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
+			<!-- kb/mb/gb -->
+			<maxFileSize>128MB</maxFileSize>
+			<!-- 日志文件保留天数 -->
+			<maxHistory>14</maxHistory>
+		</rollingPolicy>
+		<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} | [%magenta(%thread) %green(%5L)] %highlight(%-5level) %cyan(%logger{50}) : %msg %n</pattern>
+			<charset>UTF-8</charset>
+		</encoder>
+
+	</appender>
+
+	<!-- 按照每天生成日志文件 -->
+	<appender name="ERROR-APPENDER" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<append>true</append>
+
+		<!-- 过滤器,只记录 error 级别的日志 -->
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+			<level>ERROR</level>
+			<onMatch>ACCEPT</onMatch>
+			<onMismatch>DENY</onMismatch>
+		</filter>
+
+		<!-- 日志名称 -->
+		<file>${log_home}/midjourney-netty-websocket-error.log</file>
+		<!-- 每天生成一个日志文件,保存15天的日志文件 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+			<!--日志文件输出的文件名:按天回滚 daily -->
+			<FileNamePattern>${log_home}/midjourney-netty-websocket.log.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
+			<!-- kb/mb/gb -->
+			<maxFileSize>128MB</maxFileSize>
+			<!-- 日志文件保留天数 -->
+			<maxHistory>14</maxHistory>
+			<totalSizeCap>10GB</totalSizeCap>
+		</rollingPolicy>
+		<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+			<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
+			<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} | [%magenta(%thread) %green(%5L)] %highlight(%-5level) %cyan(%logger{50}) : %msg %n</pattern>
+			<!-- 编码 -->
+			<charset>UTF-8</charset>
+		</encoder>
+	</appender>
+
+	<!-- 异步输出 -->
+	<appender name="ERROR_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
+		<!-- 不丢失日志.默认的. 如果discardingThreshold大于0,队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+		<discardingThreshold>0</discardingThreshold>
+		<queueSize>10240</queueSize>
+		<appender-ref ref="ERROR-APPENDER" />
+	</appender>
+
+	<!-- 异步输出 -->
+	<appender name="ROOT_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
+		<!-- 不丢失日志.默认的. 如果discardingThreshold大于0,队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
+		<discardingThreshold>0</discardingThreshold>
+		<queueSize>10240</queueSize>
+		<appender-ref ref="ROOT-APPENDER" />
+	</appender>
+
+	<root level="INFO">
+		<appender-ref ref="STDOUT" />
+		<appender-ref ref="ERROR_ASYNC" />
+		<appender-ref ref="ROOT_ASYNC" />
+	</root>
+
+	<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>
+</configuration>

+ 2 - 0
pom.xml

@@ -208,6 +208,8 @@
         <module>netflix-service</module>
         <module>netflix-web</module>
         <module>midjourney</module>
+        <module>midjourney-netty-websocket-spring-boot-starter</module>
+        <module>midjourney-netty-websocket-web</module>
     </modules>